Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,34 @@
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit import print_formatted_text as _pt_print
from prompt_toolkit.formatted_text import ANSI as _PT_ANSI


def _register_prompt_toolkit_ignored_terminal_sequences() -> None:
"""Teach prompt_toolkit to consume terminal focus reports.

Ghostty/macOS tab or window navigation can send VT100 focus events
(``CSI I`` / ``CSI O``) to the foreground application. prompt_toolkit does
not map those sequences by default, so its parser falls back to literal
key presses (ESC, ``[``, ``I``/``O``), which inserts ``[I``/``[O`` into the
prompt after the ESC byte is handled.

Register them as ``Keys.Ignore`` at the VT100 parser table so they are
swallowed before key bindings or the input buffer ever see them. This is
intentionally parser-level rather than a post-hoc text sanitizer.
"""
try:
from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES
from prompt_toolkit.keys import Keys

ANSI_SEQUENCES.setdefault("\x1b[I", Keys.Ignore) # focus in
ANSI_SEQUENCES.setdefault("\x1b[O", Keys.Ignore) # focus out
except Exception:
# Defensive: never make CLI startup depend on prompt_toolkit internals.
pass


_register_prompt_toolkit_ignored_terminal_sequences()

try:
from prompt_toolkit.cursor_shapes import CursorShape
_STEADY_CURSOR = CursorShape.BLOCK # Non-blinking block cursor
Expand Down Expand Up @@ -9359,6 +9387,12 @@ def run(self):

# Key bindings for the input area
kb = KeyBindings()
from prompt_toolkit.keys import Keys

@kb.add(Keys.Ignore, eager=True)
def handle_ignored_terminal_sequence(event):
"""Consume parser-level ignored terminal sequences before self-insert."""
return None

@kb.add('enter')
def handle_enter(event):
Expand Down Expand Up @@ -9914,8 +9948,6 @@ def _start_recording():

threading.Thread(target=_start_recording, daemon=True).start()
event.app.invalidate()
from prompt_toolkit.keys import Keys

@kb.add(Keys.BracketedPaste, eager=True)
def handle_paste(event):
"""Handle terminal paste — detect clipboard images.
Expand Down
38 changes: 38 additions & 0 deletions tests/cli/test_cli_terminal_shortcuts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Regression tests for terminal navigation/focus escape sequences.

Ghostty/macOS window and tab navigation can deliver terminal focus reports
(CSI I / CSI O) to the running TUI. These must be consumed by the input parser,
not inserted into the prompt buffer and cleaned up later.
"""

from prompt_toolkit.input.vt100_parser import Vt100Parser
from prompt_toolkit.keys import Keys

from cli import _register_prompt_toolkit_ignored_terminal_sequences


def _parse_keys(data: str):
events = []
parser = Vt100Parser(events.append)
parser.feed_and_flush(data)
return [(event.key, event.data) for event in events]


def test_focus_events_are_parser_level_ignored_before_prompt_buffer():
_register_prompt_toolkit_ignored_terminal_sequences()

assert _parse_keys("\x1b[O\x1b[Ihello") == [
(Keys.Ignore, "\x1b[O"),
(Keys.Ignore, "\x1b[I"),
("h", "h"),
("e", "e"),
("l", "l"),
("l", "l"),
("o", "o"),
]


def test_regular_escape_shortcuts_still_parse_normally():
_register_prompt_toolkit_ignored_terminal_sequences()

assert _parse_keys("\x1bg") == [(Keys.Escape, "\x1b"), ("g", "g")]