diff --git a/interfaces/tui/app.py b/interfaces/tui/app.py index 4ee5295..ce49523 100644 --- a/interfaces/tui/app.py +++ b/interfaces/tui/app.py @@ -185,6 +185,13 @@ class HendrikTUI: self.messages.append({"role": "system", "content": context}) log(self, "welcome", WELCOME_ART) + keycodes.enable_extended_keys() + try: + self._run_loop(stdscr) + finally: + keycodes.disable_extended_keys() + + def _run_loop(self, stdscr): while self.running: self.h, self.w = stdscr.getmaxyx() if self.h < 14 or self.w < 40: diff --git a/interfaces/tui/keycodes.py b/interfaces/tui/keycodes.py index 241b7d1..5793661 100644 --- a/interfaces/tui/keycodes.py +++ b/interfaces/tui/keycodes.py @@ -16,7 +16,9 @@ # menjadi satu set kode yang bisa dibandingkan di actions.py. import curses +import os import re +import sys # Waktu (ms) menunggu byte lanjutan setelah ESC di read_key() ESC_PEEK_MS = 30 @@ -110,7 +112,9 @@ def _parse_csi(data: bytes) -> int: # data contoh: # b'[12~' → F2 b'[21~' → F10 # b'[13;5u' → Ctrl+Enter b'[13;5~' → Ctrl+Enter - # b'[115;3u' → Alt+S (kitty) b'[115;5u' → Ctrl+S + # b'[27;5;13~' → Ctrl+Enter (xterm modifyOtherKeys) + # b'[115;3u' → Alt+S (kitty) b'[27;3;115~' → Alt+S (xterm modifyOtherKeys) + # b'[13u' → Enter (CSI-u) b'[13~' → F3 (legacy) # b'[A' / b'[B' → panah atas / bawah try: text = data.decode("ascii", "ignore") @@ -121,40 +125,61 @@ def _parse_csi(data: bytes) -> int: if text[1:] in _CSI_NAV: return _CSI_NAV[text[1:]] + # Format standar CSI-u: ESC[;u / ESC[;mod~ (legacy) m = re.match(r"^\[(\d+)(?:;(\d+))?([u~])$", text) - if not m: - # Navigasi dengan prefiks angka (misal [1;5A = Ctrl+Up) - m2 = re.match(r"^\[(?:\d+;)?(\d*)([ABCDHF])$", text) - if m2 and m2.group(2) in _CSI_NAV: - return _CSI_NAV[m2.group(2)] - return KEY_ESC + if m: + code = int(m.group(1)) + mod = _csi_mod(m.group(2)) + terminator = m.group(3) + else: + # Format legacy xterm modifyOtherKeys: ESC[27;;[u~] + m27 = re.match(r"^\[27;(\d+);(\d+)([u~])$", text) + if not m27: + # Navigasi dengan prefiks angka (misal [1;5A = Ctrl+Up) + m2 = re.match(r"^\[(?:\d+;)?(\d*)([ABCDHF])$", text) + if m2 and m2.group(2) in _CSI_NAV: + return _CSI_NAV[m2.group(2)] + return KEY_ESC + code = int(m27.group(2)) + mod = _csi_mod(m27.group(1)) + terminator = m27.group(3) - code = int(m.group(1)) - mod = _csi_mod(m.group(2)) - terminator = m.group(3) + result = _map_modified_key(code, mod, terminator) + if result is not None: + return result + return KEY_ESC + + +def _map_modified_key(code: int, mod, terminator: str) -> int | None: + # Enter (code 13) khusus karena bentrok dengan F3: + # * Ctrl+Enter → KEY_CTRL_ENTER (\x1b[13;5u / \x1b[13;5~ / \x1b[27;5;13~) + # * \x1b[13~ tanpa modifier → F3 legacy + # * selain itu (Enter CSI-u/kitty/modifier lain) → Enter (13) + if code == 13: + if mod["ctrl"]: + return KEY_CTRL_ENTER + if not (mod["shift"] or mod["alt"]) and terminator == "~": + return _CSI_F_KEYS["13"] # F3 + return 13 # F1-F12 dengan/tanpa modifier - if terminator in ("u", "~") and str(code) in _CSI_F_KEYS: + if str(code) in _CSI_F_KEYS: return _CSI_F_KEYS[str(code)] - # Enter + Ctrl → Ctrl+Enter - if code == 13 and mod["ctrl"] and terminator in ("u", "~"): - return KEY_CTRL_ENTER - - # CSI-u untuk tombol printable (kitty protocol): + # CSI-u untuk tombol printable (kitty protocol / xterm modifyOtherKeys): # Ctrl+ → kode ASCII murni (misal 's' → 19) # Alt+ → KEY_ALT() - if terminator == "u" and 32 <= code <= 126: + if terminator in ("u", "~") and 32 <= code <= 126: ch = chr(code).lower() if mod["ctrl"]: if 97 <= ord(ch) <= 122: return ord(ch) & 0x1f - return KEY_ESC + return None if mod["alt"]: return KEY_ALT(ch) - return KEY_ESC + return None - return KEY_ESC + return None def read_key(stdscr, timeout_ms: int = -1) -> int: @@ -203,3 +228,23 @@ def alt_key_char(key: int) -> str | None: if not is_alt_key(key): return None return chr(key - _KEY_ALT_BASE).lower() + + +# -------------------------------------------------------------------- protokol keyboard modern --- +# Supaya Ctrl+Enter (dan kombinasi Alt/Ctrl lain) punya escape sequence sendiri +# (bukan jatuh jadi \r / ESC+huruf), aktifkan protokol keyboard di terminal: +# * \x1b[>1u → kitty keyboard protocol (kitty, foot, WezTerm, iTerm2) +# * \x1b[>4;2m → xterm modifyOtherKeys mode 2 (xterm) +# Terminal yang tidak mendukung akan meng-ignore sekuen ini dengan aman. +ENABLE_EXTENDED_KEYS = b"\x1b[>1u\x1b[>4;2m" +DISABLE_EXTENDED_KEYS = b"\x1b[<1u\x1b[>4m" + + +def enable_extended_keys(): + os.write(sys.stdout.fileno(), ENABLE_EXTENDED_KEYS) + sys.stdout.flush() + + +def disable_extended_keys(): + os.write(sys.stdout.fileno(), DISABLE_EXTENDED_KEYS) + sys.stdout.flush()