forked from genewildish/Mainline
Add mouse wheel and keyboard scrolling support to REPL
- Add scroll_offset to REPLState (max 50 lines) - Modify _render_repl() to use manual scroll position - Add scroll_output(delta) method for scroll control - Add PageUp/PageDown keyboard support (scroll 10 lines) - Add mouse wheel support via SGR mouse tracking - Update HUD to show scroll percentage (like vim) and position - Reset scroll when new output arrives - Add tests for scroll functionality
This commit is contained in:
@@ -157,6 +157,9 @@ class TerminalDisplay:
|
||||
def cleanup(self) -> None:
|
||||
from engine.terminal import CURSOR_ON
|
||||
|
||||
# Disable mouse tracking if enabled
|
||||
self.disable_mouse_tracking()
|
||||
|
||||
# Restore normal terminal mode if raw mode was enabled
|
||||
self.set_raw_mode(False)
|
||||
|
||||
@@ -174,6 +177,24 @@ class TerminalDisplay:
|
||||
"""Request quit (e.g., when Ctrl+C is pressed)."""
|
||||
self._quit_requested = True
|
||||
|
||||
def enable_mouse_tracking(self) -> None:
|
||||
"""Enable SGR mouse tracking mode."""
|
||||
try:
|
||||
# SGR mouse mode: \x1b[?1006h
|
||||
sys.stdout.write("\x1b[?1006h")
|
||||
sys.stdout.flush()
|
||||
except (OSError, AttributeError):
|
||||
pass # Terminal might not support mouse tracking
|
||||
|
||||
def disable_mouse_tracking(self) -> None:
|
||||
"""Disable SGR mouse tracking mode."""
|
||||
try:
|
||||
# Disable SGR mouse mode: \x1b[?1006l
|
||||
sys.stdout.write("\x1b[?1006l")
|
||||
sys.stdout.flush()
|
||||
except (OSError, AttributeError):
|
||||
pass
|
||||
|
||||
def set_raw_mode(self, enable: bool = True) -> None:
|
||||
"""Enable/disable raw terminal mode for input capture.
|
||||
|
||||
@@ -192,7 +213,11 @@ class TerminalDisplay:
|
||||
# Set raw mode
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
self._raw_mode_enabled = True
|
||||
# Enable mouse tracking
|
||||
self.enable_mouse_tracking()
|
||||
elif not enable and self._raw_mode_enabled:
|
||||
# Disable mouse tracking
|
||||
self.disable_mouse_tracking()
|
||||
# Restore original terminal settings
|
||||
if self._original_termios:
|
||||
termios.tcsetattr(
|
||||
@@ -223,16 +248,35 @@ class TerminalDisplay:
|
||||
char = sys.stdin.read(1)
|
||||
|
||||
if char == "\x1b": # Escape sequence
|
||||
# Read next character to determine key
|
||||
seq = sys.stdin.read(2)
|
||||
if seq == "[A":
|
||||
keys.append("up")
|
||||
elif seq == "[B":
|
||||
keys.append("down")
|
||||
elif seq == "[C":
|
||||
keys.append("right")
|
||||
elif seq == "[D":
|
||||
keys.append("left")
|
||||
# Read next characters to determine key
|
||||
# Try to read up to 10 chars for longer sequences
|
||||
seq = sys.stdin.read(10)
|
||||
|
||||
# PageUp: \x1b[5~
|
||||
if seq.startswith("[5~"):
|
||||
keys.append("page_up")
|
||||
# PageDown: \x1b[6~
|
||||
elif seq.startswith("[6~"):
|
||||
keys.append("page_down")
|
||||
# Arrow keys: \x1b[A, \x1b[B, etc.
|
||||
elif seq.startswith("["):
|
||||
if seq[1] == "A":
|
||||
keys.append("up")
|
||||
elif seq[1] == "B":
|
||||
keys.append("down")
|
||||
elif seq[1] == "C":
|
||||
keys.append("right")
|
||||
elif seq[1] == "D":
|
||||
keys.append("left")
|
||||
else:
|
||||
# Unknown escape sequence
|
||||
keys.append("escape")
|
||||
# Mouse events: \x1b[<B;X;Ym or \x1b[<B;X;YM
|
||||
elif seq.startswith("[<"):
|
||||
mouse_seq = "\x1b" + seq
|
||||
mouse_data = self._parse_mouse_event(mouse_seq)
|
||||
if mouse_data:
|
||||
keys.append(mouse_data)
|
||||
else:
|
||||
# Unknown escape sequence
|
||||
keys.append("escape")
|
||||
@@ -248,8 +292,6 @@ class TerminalDisplay:
|
||||
keys.append("ctrl_c")
|
||||
elif char == "\x04": # Ctrl+D
|
||||
keys.append("ctrl_d")
|
||||
elif char == "\x1b": # Escape
|
||||
keys.append("escape")
|
||||
elif char.isprintable():
|
||||
keys.append(char)
|
||||
except OSError:
|
||||
@@ -257,6 +299,40 @@ class TerminalDisplay:
|
||||
|
||||
return keys
|
||||
|
||||
def _parse_mouse_event(self, data: str) -> str | None:
|
||||
"""Parse SGR mouse event sequence.
|
||||
|
||||
Format: \x1b[<B;X;Ym (release) or \x1b[<B;X;YM (press)
|
||||
B = button number (0=left, 1=middle, 2=right, 64=wheel up, 65=wheel down)
|
||||
X, Y = coordinates (1-indexed)
|
||||
|
||||
Returns:
|
||||
Mouse event string like "mouse:64:10:5" or None if not a mouse event
|
||||
"""
|
||||
if not data.startswith("\x1b[<"):
|
||||
return None
|
||||
|
||||
# Find the ending 'm' or 'M'
|
||||
end_pos = data.rfind("m")
|
||||
if end_pos == -1:
|
||||
end_pos = data.rfind("M")
|
||||
if end_pos == -1:
|
||||
return None
|
||||
|
||||
inner = data[3:end_pos] # Remove \x1b[< and trailing m/M
|
||||
parts = inner.split(";")
|
||||
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
button = int(parts[0])
|
||||
x = int(parts[1]) - 1 # Convert to 0-indexed
|
||||
y = int(parts[2]) - 1
|
||||
return f"mouse:{button}:{x}:{y}"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def is_raw_mode_enabled(self) -> bool:
|
||||
"""Check if raw mode is currently enabled."""
|
||||
return self._raw_mode_enabled
|
||||
|
||||
Reference in New Issue
Block a user