name: debug-tui description: Drive and debug the real keifu TUI autonomously via its remote-control debug server (--debug-listen) — launch headlessly, inject keys/mouse, dump the rendered screen as text, and inspect app state. Use this whenever a change affects TUI behavior, rendering, keybindings, mouse handling, focus, scrolling, or async loading states, when reproducing a user-reported UI bug, or when you need to confirm "does it actually work on screen" — cargo test alone cannot verify what the user sees. Reproduce the issue through this workflow before fixing, and re-verify after.
Debugging the keifu TUI headlessly
keifu has a built-in remote-control server. You can run the real app without a human at the terminal: send key/mouse input through the same code paths as real input, dump the rendered screen as plain text, and read the app state as JSON. The reliable workflow is always: drive → dump → assert, both to reproduce a bug and to prove the fix.
Launch
cargo build
PORT=7167 # pick a fresh port per run to avoid stale instances
timeout 120 script -qec "./target/debug/keifu --debug-listen 127.0.0.1:$PORT --log-file /tmp/keifu.log" /dev/null >/dev/null 2>&1 &
sleep 2
scriptallocates a PTY; keifu cannot enable raw mode without one. Thetimeoutwrapper guarantees stray instances die even if you forget to quit.- The
scriptPTY reports size 0x0, so the main loop skips real rendering. Consequence: pane layout for mouse hit-testing is only recorded when a render happens — always send adumpwith explicit width/height before anymousecommand, and give mouse coordinates in that dump's space. - keifu operates on the repository of its working directory. To exercise
staging/commit/push, launch it inside a throwaway repo (
mktemp -d+git init), never the real working repo.
Protocol
Newline-delimited JSON over TCP; each request line gets one JSON response line.
printf '%s\n' '{"cmd":"state"}' | nc -q1 127.0.0.1 $PORT
| Request | Effect |
|---|---|
{"cmd":"keys","keys":"j j <enter>"} |
Inject key input (normal keybinding layer) |
{"cmd":"mouse","kind":"click","x":5,"y":3} |
Click / scroll_up / scroll_down at 0-based cell |
{"cmd":"dump","width":110,"height":30} |
Render current state to plain text at that size |
{"cmd":"state"} |
Mode, focused pane, selection, HEAD, async status |
For "feels slow" reports, use the log: ops over 10ms are written live as
slow operation, and quitting writes a per-op perf summary (count/avg/max).
Reproduce → quit → grep the log file.
Every response is one JSON line. dump returns the screen as an escaped
string in the screen field — pipe through jq -r .screen to read it. For
single requests prefer nc -q1 (closes after the response); only multi-line
batches need plain nc under timeout.
Key token syntax: whitespace-separated; single chars as-is (uppercase implies
Shift); special keys <enter> <esc> <tab> <backtab> <space> <up> <down> <left> <right> <home> <end> <pgup> <pgdn> <backspace> <c-x> (Ctrl+x). To type a word
in an input dialog, space-separate the letters: c f i x <space> b u g <enter>.
Full protocol details: docs/debugging.md. Implementation: src/debug_server.rs.
Gotchas that will waste your time
Double-click = two clicks on the same cell within 400 ms. Separate
ncinvocations are too slow — send both clicks (plus the leadingdumpthat records the layout) in ONE connection:printf '%s\n%s\n%s\n%s\n' \ '{"cmd":"dump","width":110,"height":30}' \ '{"cmd":"mouse","kind":"click","x":60,"y":24}' \ '{"cmd":"mouse","kind":"click","x":60,"y":24}' \ '{"cmd":"state"}' | timeout 4 nc 127.0.0.1 $PORTCommands are processed after the event-poll tick, so responses can lag up to ~200 ms; wrap
ncintimeoutand don't interpret slowness as a hang.A held-open
ncmay exit non-zero viatimeouteven after delivering the response — check the output, not the exit code.Injected input bypasses the terminal's input layer.
keys/mousecommands go straight into the app, so they cannot verify anything that depends on terminal modes — e.g. mouse tracking escape sequences (?1000/?1002/?1003) set insrc/tui.rs. Changes there need a human in a real terminal.qonly quits from the graph pane. If another pane is focused or a popup is open (e.g. after a mouse click),q/<esc>first returns focus/closes the popup and the app keeps running. Send{"cmd":"keys","keys":"q q"}and confirm exit: a follow-upncconnection must be refused. (pgrep -af keifumatches your own shell's command line — don't trust it.)
Verification loop
dumpand confirm the precondition is on screen (e.g. the row you'll click).keys/mouseto act.state+dump, then assert: grep the dump for expected text, compareselected_index/mode/focused_panein the state JSON.- Quit, and read
/tmp/keifu.logfor the tracing trail (KEIFU_LOG=tracefor more detail; useful for async diff-load issues).
A fix is not verified until step 3 shows the corrected behavior on a dump that previously showed the bug.