# 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()