Fix scroll with custom scroll_offset mechanism
Window.vertical_scroll is read-only in prompt_toolkit. Use custom scroll_offset to track position and slice output lines. - scroll_offset=0 shows newest content (auto-scroll) - Page Up increases offset (scroll into history) - Page Down decreases offset (towards newest) - Home/End jump to top/bottom Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
92e663d6a2
commit
f58197c83f
1 changed files with 32 additions and 25 deletions
|
|
@ -104,12 +104,26 @@ class OutputBuffer:
|
|||
if len(self.lines) > self.max_lines:
|
||||
self.lines = self.lines[-self.max_lines:]
|
||||
|
||||
def get_formatted_text(self) -> FormattedText:
|
||||
"""Get formatted text for display - all lines."""
|
||||
def get_formatted_text(self, scroll_offset: int = 0) -> FormattedText:
|
||||
"""Get formatted text for display.
|
||||
|
||||
scroll_offset: 0 = show bottom, positive = scroll up N lines
|
||||
"""
|
||||
result = []
|
||||
|
||||
if scroll_offset == 0:
|
||||
# Show all lines (newest at bottom)
|
||||
for style, text in self.lines:
|
||||
result.append((f"class:{style}", text))
|
||||
result.append(("", "\n"))
|
||||
else:
|
||||
# Show lines up to scroll_offset from end
|
||||
end_idx = len(self.lines) - scroll_offset
|
||||
if end_idx > 0:
|
||||
for style, text in self.lines[:end_idx]:
|
||||
result.append((f"class:{style}", text))
|
||||
result.append(("", "\n"))
|
||||
|
||||
return FormattedText(result)
|
||||
|
||||
def clear(self):
|
||||
|
|
@ -184,40 +198,38 @@ class TUIConsole:
|
|||
def handle_ctrl_l(event):
|
||||
"""Handle Ctrl+L - clear output."""
|
||||
self.output.clear()
|
||||
self.user_scrolled = False
|
||||
self.scroll_offset = 0
|
||||
|
||||
# Track scroll offset (0 = bottom, positive = lines from bottom)
|
||||
self.scroll_offset = 0
|
||||
|
||||
@kb.add("pageup")
|
||||
def handle_page_up(event):
|
||||
"""Scroll output up."""
|
||||
self.output_window.vertical_scroll = max(0, self.output_window.vertical_scroll - 10)
|
||||
self.user_scrolled = True
|
||||
max_offset = max(0, len(self.output.lines) - 5)
|
||||
self.scroll_offset = min(self.scroll_offset + 10, max_offset)
|
||||
|
||||
@kb.add("pagedown")
|
||||
def handle_page_down(event):
|
||||
"""Scroll output down."""
|
||||
self.output_window.vertical_scroll += 10
|
||||
# Check if at bottom
|
||||
if self.output_window.vertical_scroll >= len(self.output.lines) - 5:
|
||||
self.user_scrolled = False
|
||||
self.scroll_offset = max(0, self.scroll_offset - 10)
|
||||
|
||||
@kb.add("end")
|
||||
def handle_end(event):
|
||||
"""Scroll to bottom."""
|
||||
self.output_window.vertical_scroll = 999999
|
||||
self.user_scrolled = False
|
||||
self.scroll_offset = 0
|
||||
|
||||
@kb.add("home")
|
||||
def handle_home(event):
|
||||
"""Scroll to top."""
|
||||
self.output_window.vertical_scroll = 0
|
||||
self.user_scrolled = True
|
||||
self.scroll_offset = max(0, len(self.output.lines) - 5)
|
||||
|
||||
# Track if user manually scrolled
|
||||
self.user_scrolled = False
|
||||
# Output control - uses scroll_offset to show correct portion
|
||||
def get_visible_output():
|
||||
return self.output.get_formatted_text(self.scroll_offset)
|
||||
|
||||
# Output control
|
||||
output_control = FormattedTextControl(
|
||||
text=lambda: self.output.get_formatted_text(),
|
||||
text=get_visible_output,
|
||||
focusable=False,
|
||||
)
|
||||
|
||||
|
|
@ -226,8 +238,6 @@ class TUIConsole:
|
|||
content=output_control,
|
||||
wrap_lines=True,
|
||||
)
|
||||
# Start scrolled to bottom
|
||||
self.output_window.vertical_scroll = 999999
|
||||
|
||||
# Separator line with status (shows scroll hint if not at bottom)
|
||||
def get_separator():
|
||||
|
|
@ -300,9 +310,6 @@ class TUIConsole:
|
|||
"""Invalidate the app to trigger redraw."""
|
||||
if self.app:
|
||||
try:
|
||||
# Auto-scroll to bottom only if user hasn't manually scrolled up
|
||||
if hasattr(self, 'output_window') and not getattr(self, 'user_scrolled', False):
|
||||
self.output_window.vertical_scroll = 999999
|
||||
self.app.invalidate()
|
||||
except Exception:
|
||||
pass
|
||||
|
|
|
|||
Loading…
Reference in a new issue