From 67ff380e37632e4ec80f50fb63a2b15e9ca8f78d Mon Sep 17 00:00:00 2001 From: haseeb-heaven <11544739+haseeb-heaven@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:04:40 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20[UX=20improvement]=20?= =?UTF-8?q?Fix=20terminal=20TUI=20keyboard=20trap=20and=20provide=20clear?= =?UTF-8?q?=20exit=20hints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/palette.md | 3 +++ libs/terminal_ui.py | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 .jules/palette.md diff --git a/.jules/palette.md b/.jules/palette.md new file mode 100644 index 0000000..0606e86 --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,3 @@ +## 2024-05-24 - Handle Terminal Raw Mode Keyboard Traps +**Learning:** When creating terminal UIs with raw mode, `\x03` (Ctrl-C) must be explicitly trapped and raised as `KeyboardInterrupt`, otherwise it creates a keyboard trap where standard interrupt keys are ignored. It's also important to explicitly format choices in `rich` Prompts while escaping markup to avoid fallback issues. +**Action:** Always add explicit keyboard interrupt handling for `\x03` and clear exit instructions ('Esc/Ctrl-C to cancel') in terminal UI raw modes. diff --git a/libs/terminal_ui.py b/libs/terminal_ui.py index 47b8b3b..4b37d81 100644 --- a/libs/terminal_ui.py +++ b/libs/terminal_ui.py @@ -27,6 +27,8 @@ def _read_key(self): return 'enter' if key == '\x1b': return 'escape' + if key == '\x03': + raise KeyboardInterrupt return key import termios @@ -37,6 +39,8 @@ def _read_key(self): try: tty.setraw(fd) key = sys.stdin.read(1) + if key == '\x03': + raise KeyboardInterrupt if key == '\x1b': next_chars = sys.stdin.read(2) mapping = {'[A': 'up', '[B': 'down', '[D': 'left', '[C': 'right'} @@ -67,7 +71,8 @@ def _render_selector(self, title, options, selected_index, help_text, default): style = 'bold green' if index == selected_index else '' table.add_row(marker, label, style=style) - footer = help_text or 'Use Up/Down arrows and Enter to select.' + base_text = help_text or 'Use Up/Down arrows and Enter to select.' + footer = f"{base_text} Esc/Ctrl-C to cancel." self.console.print(Panel.fit(footer, title='Interpreter TUI', border_style='green')) self.console.print(f"[bold cyan]{title}[/bold cyan]") self.console.print(table) @@ -76,7 +81,7 @@ def _render_selector(self, title, options, selected_index, help_text, default): def _select_option(self, title, options, default, help_text=None): if not sys.stdin.isatty(): default_choice = default if default in options else options[0] - answer = Prompt.ask(f"{title}", default=default_choice).strip() + answer = Prompt.ask(f"{title} \\[{'|'.join(options)}]", default=default_choice).strip() if answer in options: return answer for option in options: