hendrik/tui/input.py

160 lines
5.2 KiB
Python

# input.py — Keyboard handling dan workspace popup.
# handle_key() adalah dispatch besar yang menerjemahkan
# key code curses menjadi aksi pada state app.
import curses
import os
from .agent import submit, log
def _build_visual(buffer, max_chars):
# Build list of (logical_line_idx, start_col) for each visual line.
visual = []
for i, line in enumerate(buffer):
if not line:
visual.append((i, 0))
else:
for start in range(0, max(len(line), 1), max_chars):
visual.append((i, start))
return visual
def _find_visual(visual, logical_line, col):
# Find visual line index for given logical line and column.
best = 0
for idx, (li, start) in enumerate(visual):
if li == logical_line:
best = idx
if start <= col:
break
return best
def handle_key(app, stdscr, key):
max_chars = app.w - 6 # usable width in input box
visual = _build_visual(app.input_buffer, max_chars)
cur_visual = _find_visual(visual, app.input_line, app.input_col)
processing = app.processing
# -- Always allowed (even during processing) --
if key == 3: # Ctrl+C → exit
app.running = False
elif key == curses.KEY_PPAGE:
app.scroll = max(0, app.scroll - (app.h - 10) // 2)
elif key == curses.KEY_NPAGE:
app.scroll += (app.h - 10) // 2
elif key == curses.KEY_RESIZE:
pass
# -- Blocked during processing --
elif processing:
pass # ignore all other keys while processing
# -- Ctrl shortcuts --
elif key == 4: # Ctrl+D → submit query ke LLM
submit(app, stdscr)
elif key == 23: # Ctrl+W → popup ganti workspace
workspace_popup(app, stdscr)
elif key == 12: # Ctrl+L → clear chat log
app.log.clear()
# -- Enter: split logical line at cursor position --
elif key in (curses.KEY_ENTER, 10, 13):
line = app.input_buffer[app.input_line]
left = line[:app.input_col]
right = line[app.input_col:]
app.input_buffer[app.input_line] = left
app.input_buffer.insert(app.input_line + 1, right)
app.input_line += 1
app.input_col = 0
# -- Backspace: hapus karakter sebelumnya atau gabung baris --
elif key in (curses.KEY_BACKSPACE, 127):
if app.input_col > 0:
line = app.input_buffer[app.input_line]
app.input_buffer[app.input_line] = (
line[: app.input_col - 1] + line[app.input_col :]
)
app.input_col -= 1
elif app.input_line > 0:
carry = app.input_buffer.pop(app.input_line)
app.input_line -= 1
app.input_col = len(app.input_buffer[app.input_line])
app.input_buffer[app.input_line] += carry
# -- Navigation arrows --
elif key == curses.KEY_UP:
if cur_visual > 0:
prev_li, prev_start = visual[cur_visual - 1]
app.input_line = prev_li
app.input_col = min(app.input_col, len(app.input_buffer[prev_li]))
elif key == curses.KEY_DOWN:
if cur_visual < len(visual) - 1:
next_li, next_start = visual[cur_visual + 1]
app.input_line = next_li
app.input_col = min(app.input_col, len(app.input_buffer[next_li]))
elif key == curses.KEY_LEFT:
if app.input_col > 0:
app.input_col -= 1
elif app.input_line > 0:
app.input_line -= 1
app.input_col = len(app.input_buffer[app.input_line])
elif key == curses.KEY_RIGHT:
if app.input_col < len(app.input_buffer[app.input_line]):
app.input_col += 1
elif app.input_line < len(app.input_buffer) - 1:
app.input_line += 1
app.input_col = 0
elif key == curses.KEY_HOME:
app.input_col = 0
elif key == curses.KEY_END:
app.input_col = len(app.input_buffer[app.input_line])
# -- Tab: insert 4 spasi --
elif key == 9:
line = app.input_buffer[app.input_line]
app.input_buffer[app.input_line] = (
line[: app.input_col] + " " + line[app.input_col :]
)
app.input_col += 4
# -- Printable characters --
elif 32 <= key <= 255:
ch = chr(key)
line = app.input_buffer[app.input_line]
app.input_buffer[app.input_line] = (
line[: app.input_col] + ch + line[app.input_col :]
)
app.input_col += 1
def workspace_popup(app, stdscr):
# Overlay window kecil di tengah layar untuk input path workspace
pw = min(60, app.w - 4)
ph = 3
px = (app.w - pw) // 2
py = app.h // 2 - 1
win = curses.newwin(ph, pw, py, px)
win.box()
win.addstr(0, 2, " Workspace path: ")
win.addstr(1, 2, " " * (pw - 4))
curses.echo() # tampilkan input user
ws = win.getstr(1, 2, pw - 5).decode("utf-8")
curses.noecho()
del win
ws = ws.strip()
if ws:
resolved = os.path.abspath(ws)
if os.path.isdir(resolved):
os.chdir(resolved)
log(app, "system", f"Workspace \u2192 {resolved}")
else:
log(app, "error", f"Invalid directory: {resolved}")
stdscr.touchwin()
stdscr.refresh()