Compare commits
2 Commits
4572fe6b60
...
6256501b7d
| Author | SHA1 | Date | |
|---|---|---|---|
| 6256501b7d | |||
| 6976348c74 |
@ -185,6 +185,13 @@ class HendrikTUI:
|
|||||||
self.messages.append({"role": "system", "content": context})
|
self.messages.append({"role": "system", "content": context})
|
||||||
log(self, "welcome", WELCOME_ART)
|
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:
|
while self.running:
|
||||||
self.h, self.w = stdscr.getmaxyx()
|
self.h, self.w = stdscr.getmaxyx()
|
||||||
if self.h < 14 or self.w < 40:
|
if self.h < 14 or self.w < 40:
|
||||||
|
|||||||
@ -16,7 +16,9 @@
|
|||||||
# menjadi satu set kode yang bisa dibandingkan di actions.py.
|
# menjadi satu set kode yang bisa dibandingkan di actions.py.
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
# Waktu (ms) menunggu byte lanjutan setelah ESC di read_key()
|
# Waktu (ms) menunggu byte lanjutan setelah ESC di read_key()
|
||||||
ESC_PEEK_MS = 30
|
ESC_PEEK_MS = 30
|
||||||
@ -110,7 +112,9 @@ def _parse_csi(data: bytes) -> int:
|
|||||||
# data contoh:
|
# data contoh:
|
||||||
# b'[12~' → F2 b'[21~' → F10
|
# b'[12~' → F2 b'[21~' → F10
|
||||||
# b'[13;5u' → Ctrl+Enter b'[13;5~' → Ctrl+Enter
|
# 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
|
# b'[A' / b'[B' → panah atas / bawah
|
||||||
try:
|
try:
|
||||||
text = data.decode("ascii", "ignore")
|
text = data.decode("ascii", "ignore")
|
||||||
@ -121,40 +125,61 @@ def _parse_csi(data: bytes) -> int:
|
|||||||
if text[1:] in _CSI_NAV:
|
if text[1:] in _CSI_NAV:
|
||||||
return _CSI_NAV[text[1:]]
|
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)
|
m = re.match(r"^\[(\d+)(?:;(\d+))?([u~])$", text)
|
||||||
if not m:
|
if m:
|
||||||
# Navigasi dengan prefiks angka (misal [1;5A = Ctrl+Up)
|
code = int(m.group(1))
|
||||||
m2 = re.match(r"^\[(?:\d+;)?(\d*)([ABCDHF])$", text)
|
mod = _csi_mod(m.group(2))
|
||||||
if m2 and m2.group(2) in _CSI_NAV:
|
terminator = m.group(3)
|
||||||
return _CSI_NAV[m2.group(2)]
|
else:
|
||||||
return KEY_ESC
|
# 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))
|
result = _map_modified_key(code, mod, terminator)
|
||||||
mod = _csi_mod(m.group(2))
|
if result is not None:
|
||||||
terminator = m.group(3)
|
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
|
# 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)]
|
return _CSI_F_KEYS[str(code)]
|
||||||
|
|
||||||
# Enter + Ctrl → Ctrl+Enter
|
# CSI-u untuk tombol printable (kitty protocol / xterm modifyOtherKeys):
|
||||||
if code == 13 and mod["ctrl"] and terminator in ("u", "~"):
|
|
||||||
return KEY_CTRL_ENTER
|
|
||||||
|
|
||||||
# CSI-u untuk tombol printable (kitty protocol):
|
|
||||||
# Ctrl+<huruf> → kode ASCII murni (misal 's' → 19)
|
# Ctrl+<huruf> → kode ASCII murni (misal 's' → 19)
|
||||||
# Alt+<huruf> → KEY_ALT(<huruf>)
|
# Alt+<huruf> → KEY_ALT(<huruf>)
|
||||||
if terminator == "u" and 32 <= code <= 126:
|
if terminator in ("u", "~") and 32 <= code <= 126:
|
||||||
ch = chr(code).lower()
|
ch = chr(code).lower()
|
||||||
if mod["ctrl"]:
|
if mod["ctrl"]:
|
||||||
if 97 <= ord(ch) <= 122:
|
if 97 <= ord(ch) <= 122:
|
||||||
return ord(ch) & 0x1f
|
return ord(ch) & 0x1f
|
||||||
return KEY_ESC
|
return None
|
||||||
if mod["alt"]:
|
if mod["alt"]:
|
||||||
return KEY_ALT(ch)
|
return KEY_ALT(ch)
|
||||||
return KEY_ESC
|
return None
|
||||||
|
|
||||||
return KEY_ESC
|
return None
|
||||||
|
|
||||||
|
|
||||||
def read_key(stdscr, timeout_ms: int = -1) -> int:
|
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):
|
if not is_alt_key(key):
|
||||||
return None
|
return None
|
||||||
return chr(key - _KEY_ALT_BASE).lower()
|
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()
|
||||||
|
|||||||
@ -208,7 +208,7 @@ def draw_menubar(app, stdscr):
|
|||||||
pass
|
pass
|
||||||
x += len(text)
|
x += len(text)
|
||||||
|
|
||||||
model = f" {app.llm.model} "
|
model = " F10:menu " # sementara: petunjuk tombol, bukan nama model
|
||||||
try:
|
try:
|
||||||
stdscr.addstr(y, max(0, app.w - len(model)), model, base_attr)
|
stdscr.addstr(y, max(0, app.w - len(model)), model, base_attr)
|
||||||
except curses.error:
|
except curses.error:
|
||||||
|
|||||||
@ -21,7 +21,6 @@ C_STATUS_PROC = theme.C_STATUS_PROC
|
|||||||
C_ERROR = theme.C_ERROR
|
C_ERROR = theme.C_ERROR
|
||||||
C_INPUT_BORDER = theme.C_INPUT_BORDER
|
C_INPUT_BORDER = theme.C_INPUT_BORDER
|
||||||
C_STATUS_INFO = theme.C_STATUS_INFO
|
C_STATUS_INFO = theme.C_STATUS_INFO
|
||||||
C_HINT_DISABLED = theme.C_HINT_DISABLED
|
|
||||||
C_WELCOME = theme.C_WELCOME
|
C_WELCOME = theme.C_WELCOME
|
||||||
C_TOOL_CALL = theme.C_TOOL_CALL
|
C_TOOL_CALL = theme.C_TOOL_CALL
|
||||||
|
|
||||||
@ -50,26 +49,16 @@ def draw(app, stdscr):
|
|||||||
|
|
||||||
|
|
||||||
def draw_header(app, stdscr):
|
def draw_header(app, stdscr):
|
||||||
# Baris 1: menu bar (dengan mnemonic + model di kanan)
|
# Baris 1: menu bar (dengan mnemonic + petunjuk F10 di kanan)
|
||||||
draw_menubar(app, stdscr)
|
draw_menubar(app, stdscr)
|
||||||
|
|
||||||
# Baris 2: shortcut hints kontekstual sesuai status processing
|
|
||||||
if app.processing:
|
|
||||||
hints = " F10:menu Esc:cancel "
|
|
||||||
else:
|
|
||||||
hints = (" F10:menu ^Enter:send ^N:new ^O:open F2:rename "
|
|
||||||
"F4:model F6:ws ^C:exit ")
|
|
||||||
full_line = hints.ljust(app.w)
|
|
||||||
attr2 = curses.color_pair(C_HINT_DISABLED)
|
|
||||||
stdscr.addstr(1, 0, full_line[:app.w], attr2)
|
|
||||||
|
|
||||||
|
|
||||||
def draw_chat(app, stdscr):
|
def draw_chat(app, stdscr):
|
||||||
# Area chat — dari baris 2 sampai baris (h - 11).
|
# Area chat — dari baris 2 sampai baris (h - 11).
|
||||||
# Bisa di-scroll dengan Page Up / Page Down.
|
# Bisa di-scroll dengan Page Up / Page Down.
|
||||||
# app.log berisi daftar item (role, text, time) untuk display.
|
# app.log berisi daftar item (role, text, time) untuk display.
|
||||||
h, w = app.h, app.w
|
h, w = app.h, app.w
|
||||||
chat_top = 2
|
chat_top = 1
|
||||||
chat_h = h - 11
|
chat_h = h - 11
|
||||||
if chat_h <= 0:
|
if chat_h <= 0:
|
||||||
return
|
return
|
||||||
@ -355,7 +344,7 @@ def draw_input(app, stdscr):
|
|||||||
|
|
||||||
|
|
||||||
def draw_status(app, stdscr):
|
def draw_status(app, stdscr):
|
||||||
# Status bar di baris h-9: mode, workspace, session
|
# Status bar di baris h-9: mode, model, workspace, session
|
||||||
h, w = app.h, app.w
|
h, w = app.h, app.w
|
||||||
y = h - 9
|
y = h - 9
|
||||||
ws = get_current_workspace()
|
ws = get_current_workspace()
|
||||||
@ -364,18 +353,19 @@ def draw_status(app, stdscr):
|
|||||||
if app.current_session:
|
if app.current_session:
|
||||||
session_tag = f" {app.current_session.name} "
|
session_tag = f" {app.current_session.name} "
|
||||||
|
|
||||||
|
model = app.llm.model
|
||||||
mode = " PROCESSING " if app.processing else " READY "
|
mode = " PROCESSING " if app.processing else " READY "
|
||||||
|
|
||||||
# Format: [MODE] workspace session (menggunakan spasi sebagai pemisah)
|
# Format: [MODE] model workspace session (menggunakan spasi sebagai pemisah)
|
||||||
status_text = f"{mode} {ws} {session_tag}"
|
status_text = f"{mode} {model} {ws} {session_tag}"
|
||||||
|
|
||||||
# Jika terlalu panjang, potong bagian workspace-nya saja agar tetap readable
|
# Jika terlalu panjang, potong bagian workspace-nya saja agar tetap readable
|
||||||
ws_display = ws
|
ws_display = ws
|
||||||
if len(status_text) > w:
|
if len(status_text) > w:
|
||||||
max_ws_len = w - len(mode) - len(session_tag) - 2
|
max_ws_len = w - len(mode) - len(model) - len(session_tag) - 3
|
||||||
if len(ws) > max_ws_len:
|
if len(ws) > max_ws_len:
|
||||||
ws_display = ".." + ws[-(max_ws_len - 2):]
|
ws_display = ".." + ws[-(max_ws_len - 2):]
|
||||||
status_text = f"{mode} {ws_display} {session_tag}"
|
status_text = f"{mode} {model} {ws_display} {session_tag}"
|
||||||
|
|
||||||
# Gambar background dasar
|
# Gambar background dasar
|
||||||
full_status = status_text.ljust(w)[:w]
|
full_status = status_text.ljust(w)[:w]
|
||||||
@ -385,14 +375,18 @@ def draw_status(app, stdscr):
|
|||||||
mode_attr = curses.color_pair(C_STATUS_READY) if not app.processing else curses.color_pair(C_STATUS_PROC)
|
mode_attr = curses.color_pair(C_STATUS_READY) if not app.processing else curses.color_pair(C_STATUS_PROC)
|
||||||
stdscr.addstr(y, 0, mode, mode_attr | curses.A_BOLD)
|
stdscr.addstr(y, 0, mode, mode_attr | curses.A_BOLD)
|
||||||
|
|
||||||
# Highlight Workspace dan Session dengan warna Putih-Bold
|
# Highlight Model, Workspace dan Session dengan warna Putih-Bold
|
||||||
highlight_attr = curses.color_pair(C_STATUS_INFO) | curses.A_BOLD
|
highlight_attr = curses.color_pair(C_STATUS_INFO) | curses.A_BOLD
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Mode sudah digambar, kita cari posisi setelah mode
|
# Mode sudah digambar, kita cari posisi setelah mode
|
||||||
ws_start = len(mode) + 1 # melewati mode + 1 spasi
|
model_start = len(mode) + 1 # melewati mode + 1 spasi
|
||||||
|
ws_start = len(mode) + len(model) + 2
|
||||||
ws_len = len(ws_display)
|
ws_len = len(ws_display)
|
||||||
|
|
||||||
|
# Gambar Model
|
||||||
|
stdscr.addstr(y, model_start, model, highlight_attr)
|
||||||
|
|
||||||
# Gambar Workspace
|
# Gambar Workspace
|
||||||
stdscr.addstr(y, ws_start, ws_display, highlight_attr)
|
stdscr.addstr(y, ws_start, ws_display, highlight_attr)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user