diff --git a/.jules/palette.md b/.jules/palette.md new file mode 100644 index 0000000..5fe965a --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,3 @@ +## 2024-07-04 - Terminal UI Keyboard Traps +**Learning:** In terminal raw mode (TTY), reading standard escape sequences natively intercepts `\x03` (Ctrl-C) as standard text, causing keyboard traps. +**Action:** When handling raw input, explicitly catch the `\x03` byte and raise `KeyboardInterrupt` to ensure users can exit the interface, and update prompt messages to explicitly inform the user of standard exit choices like "Ctrl-C to cancel" rather than "Esc" to prevent sequence hanging. diff --git a/libs/terminal_ui.py b/libs/terminal_ui.py index 47b8b3b..f114abe 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('Selection cancelled by user.') 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('Selection cancelled by user.') if key == '\x1b': next_chars = sys.stdin.read(2) mapping = {'[A': 'up', '[B': 'down', '[D': 'left', '[C': 'right'} @@ -67,7 +71,7 @@ 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.' + footer = help_text or 'Use Up/Down arrows and Enter to select. 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 +80,8 @@ 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() + options_str = '|'.join(str(opt) for opt in options) + answer = Prompt.ask(f"{title} \\[{options_str}]", default=default_choice).strip() if answer in options: return answer for option in options: