From ef597faf1cd06d905572c959d23f0a2cf7afdd5e Mon Sep 17 00:00:00 2001 From: haseeb-heaven <11544739+haseeb-heaven@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:15:12 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20Esc/Ctrl-C=20sh?= =?UTF-8?q?ortcut=20hints=20to=20TUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added explicit handling of Ctrl-C (\x03) in raw terminal input mode and added visible keyboard shortcut hints to the TUI selector footer. --- .jules/palette.md | 3 + libs/terminal_ui.py | 5 + libs/terminal_ui.py.orig | 198 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 .jules/palette.md create mode 100644 libs/terminal_ui.py.orig diff --git a/.jules/palette.md b/.jules/palette.md new file mode 100644 index 0000000..a350ea6 --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,3 @@ +## 2024-05-01 - Added explicit Esc/Ctrl-C shortcut hints +**Learning:** In terminal UI environments handling raw input, standard interrupt bytes like `\x03` must be explicitly caught to trigger `KeyboardInterrupt`, and explicit visible shortcut hints prevent keyboard traps. +**Action:** Always add explicit shortcut hints ('Esc/Ctrl-C to cancel') and explicitly map standard interrupt bytes to `KeyboardInterrupt` in raw mode loops. \ No newline at end of file diff --git a/libs/terminal_ui.py b/libs/terminal_ui.py index 47b8b3b..ca690f7 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'} @@ -68,6 +72,7 @@ def _render_selector(self, title, options, selected_index, help_text, default): table.add_row(marker, label, style=style) footer = help_text or 'Use Up/Down arrows and Enter to select.' + footer += '\n[dim]Esc/Ctrl-C to cancel[/dim]' 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) diff --git a/libs/terminal_ui.py.orig b/libs/terminal_ui.py.orig new file mode 100644 index 0000000..47b8b3b --- /dev/null +++ b/libs/terminal_ui.py.orig @@ -0,0 +1,198 @@ +from argparse import Namespace +import os +import sys + +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Prompt +from rich.table import Table + +from libs.utility_manager import UtilityManager + + +class TerminalUI: + def __init__(self): + self.console = Console() + self.utility_manager = UtilityManager() + + def _read_key(self): + if os.name == 'nt': + import msvcrt + key = msvcrt.getwch() + if key in ('\x00', '\xe0'): + extended = msvcrt.getwch() + mapping = {'H': 'up', 'P': 'down', 'K': 'left', 'M': 'right'} + return mapping.get(extended, extended) + if key == '\r': + return 'enter' + if key == '\x1b': + return 'escape' + return key + + import termios + import tty + + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(fd) + key = sys.stdin.read(1) + if key == '\x1b': + next_chars = sys.stdin.read(2) + mapping = {'[A': 'up', '[B': 'down', '[D': 'left', '[C': 'right'} + return mapping.get(next_chars, 'escape') + if key in ('\r', '\n'): + return 'enter' + return key + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + + def _render_selector(self, title, options, selected_index, help_text, default): + self.utility_manager.clear_screen() + visible_rows = max(8, min(14, self.console.size.height - 10)) + start_index = max(0, selected_index - visible_rows // 2) + end_index = min(len(options), start_index + visible_rows) + start_index = max(0, end_index - visible_rows) + + table = Table(show_header=True, header_style='bold cyan') + table.add_column('', width=2) + table.add_column('Value', overflow='fold') + + for index in range(start_index, end_index): + option = options[index] + marker = '>' if index == selected_index else ' ' + label = option + if option == default: + label += ' (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.' + 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) + self.console.print(f'Selected: [bold]{options[selected_index]}[/bold]') + + 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() + if answer in options: + return answer + for option in options: + if option.lower() == answer.lower(): + return option + return default_choice + + try: + selected_index = options.index(default) + except ValueError: + selected_index = 0 + + while True: + self._render_selector(title, options, selected_index, help_text, default) + key = self._read_key() + + if key in ('up', 'k'): + selected_index = (selected_index - 1) % len(options) + elif key in ('down', 'j'): + selected_index = (selected_index + 1) % len(options) + elif key == 'enter': + return options[selected_index] + elif key == 'escape': + raise KeyboardInterrupt('Selection cancelled by user.') + elif isinstance(key, str) and len(key) == 1: + lowered = key.lower() + for index, option in enumerate(options): + if option.lower().startswith(lowered): + selected_index = index + break + + def _select_boolean(self, title, default=False): + default_choice = 'yes' if default else 'no' + choice = self._select_option(title, ['yes', 'no'], default_choice, 'Use Up/Down arrows and Enter to choose.') + return choice == 'yes' + + def select_mode(self, default_mode='code'): + return self._select_option('Mode', ['code', 'chat', 'script', 'command', 'vision'], default_mode) + + def select_model(self, default_model=None): + models = self.utility_manager.list_available_models() + default_model = default_model or self.utility_manager.get_default_model_name() + if default_model not in models: + default_model = models[0] + return self._select_option('Model', models, default_model, 'Use Up/Down arrows, Enter, or type the first letter to jump.') + + def select_language(self, default_lang='python'): + return self._select_option('Language', ['python', 'javascript'], default_lang) + + def select_boolean(self, title, default=False): + return self._select_boolean(title, default=default) + + def interactive_settings(self, interpreter): + current_model = getattr(interpreter, "INTERPRETER_MODEL_LABEL", None) or getattr(interpreter, "INTERPRETER_MODEL", None) + current_mode = getattr(interpreter, "INTERPRETER_MODE", "code") + current_lang = getattr(interpreter, "INTERPRETER_LANGUAGE", "python") + + mode = self.select_mode(current_mode) + model = self.select_model(current_model) + language = self.select_language(current_lang) + display_code = self.select_boolean('Display generated code automatically?', default=getattr(interpreter, "DISPLAY_CODE", False)) + execute_code = self.select_boolean('Execute generated code automatically?', default=getattr(interpreter, "EXECUTE_CODE", False)) + save_code = self.select_boolean('Save generated output automatically?', default=getattr(interpreter, "SAVE_CODE", False)) + history = self.select_boolean('Enable history memory?', default=getattr(interpreter, "INTERPRETER_HISTORY", False)) + + return { + "mode": mode, + "model": model, + "language": language, + "display_code": display_code, + "execute_code": execute_code, + "save_code": save_code, + "history": history, + } + + def launch(self, args): + mode = self.select_mode(args.mode or 'code') + model = self.select_model(args.model or self.utility_manager.get_default_model_name()) + language = self.select_language(args.lang or 'python') + + self.utility_manager.clear_screen() + self.console.print( + Panel.fit( + f"Mode: [bold]{mode}[/bold] | Model: [bold]{model}[/bold] | Language: [bold]{language}[/bold]", + title='Interpreter Session', + border_style='blue', + ) + ) + + display_code = args.display_code + if mode in ['code', 'script', 'command'] and not display_code: + display_code = self._select_boolean('Display generated code automatically?', default=True) + + execute_code = args.exec + if mode == 'code' and not execute_code: + execute_code = self._select_boolean('Execute generated code automatically?', default=False) + + save_code = args.save_code + if mode in ['code', 'script', 'command'] and not save_code: + save_code = self._select_boolean('Save generated output automatically?', default=False) + + history = args.history + if not history: + history = self._select_boolean('Enable history memory?', default=False) + + return Namespace( + exec=execute_code, + save_code=save_code, + mode=mode, + model=model, + display_code=display_code, + lang=language, + file=args.file, + history=history, + unsafe=getattr(args, "unsafe", False), + upgrade=args.upgrade, + cli=args.cli, + tui=args.tui, + )