Fixing Ctrl Enter

This commit is contained in:
Dita Aji Pratama 2026-08-13 12:33:23 +07:00
parent 6976348c74
commit 6256501b7d
2 changed files with 72 additions and 20 deletions

View File

@ -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:

View File

@ -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[<code>;<mod>u / ESC[<code>;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;<mod>;<code>[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+<huruf> → kode ASCII murni (misal 's' → 19)
# Alt+<huruf> → KEY_ALT(<huruf>)
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()