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
script allocates a PTY; keifu cannot enable raw mode without one. The
timeout wrapper guarantees stray instances die even if you forget to quit.
- The
script PTY 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 a dump with explicit width/height before any
mouse command, 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 nc
invocations are too slow — send both clicks (plus the leading dump that
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 $PORT
Commands are processed after the event-poll tick, so responses can lag up to
~200 ms; wrap nc in timeout and don't interpret slowness as a hang.
A held-open nc may exit non-zero via timeout even after delivering the
response — check the output, not the exit code.
Injected input bypasses the terminal's input layer. keys/mouse
commands 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 in src/tui.rs. Changes there need a human in a
real terminal.
q only 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-up nc connection
must be refused. (pgrep -af keifu matches your own shell's command line —
don't trust it.)
Verification loop
dump and confirm the precondition is on screen (e.g. the row you'll click).
keys / mouse to act.
state + dump, then assert: grep the dump for expected text, compare
selected_index / mode / focused_pane in the state JSON.
- Quit, and read
/tmp/keifu.log for the tracing trail
(KEIFU_LOG=trace for 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.
1---2name: debug-tui3description: 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.4---56# Debugging the keifu TUI headlessly78keifu has a built-in remote-control server. You can run the real app without a9human at the terminal: send key/mouse input through the same code paths as real10input, dump the rendered screen as plain text, and read the app state as JSON.11The reliable workflow is always: **drive → dump → assert**, both to reproduce a12bug and to prove the fix.1314## Launch1516```bash17cargo build18PORT=7167 # pick a fresh port per run to avoid stale instances19timeout 120 script -qec "./target/debug/keifu --debug-listen 127.0.0.1:$PORT --log-file /tmp/keifu.log" /dev/null >/dev/null 2>&1 &20sleep 221```2223- `script` allocates a PTY; keifu cannot enable raw mode without one. The24 `timeout` wrapper guarantees stray instances die even if you forget to quit.25- The `script` PTY reports size 0x0, so the main loop skips real rendering.26 Consequence: pane layout for mouse hit-testing is only recorded when a render27 happens — **always send a `dump` with explicit width/height before any28 `mouse` command**, and give mouse coordinates in that dump's space.29- keifu operates on the repository of its working directory. To exercise30 staging/commit/push, launch it inside a throwaway repo (`mktemp -d` +31 `git init`), never the real working repo.3233## Protocol3435Newline-delimited JSON over TCP; each request line gets one JSON response line.3637```bash38printf '%s\n' '{"cmd":"state"}' | nc -q1 127.0.0.1 $PORT39```4041| Request | Effect |42| --- | --- |43| `{"cmd":"keys","keys":"j j <enter>"}` | Inject key input (normal keybinding layer) |44| `{"cmd":"mouse","kind":"click","x":5,"y":3}` | Click / `scroll_up` / `scroll_down` at 0-based cell |45| `{"cmd":"dump","width":110,"height":30}` | Render current state to plain text at that size |46| `{"cmd":"state"}` | Mode, focused pane, selection, HEAD, async status |4748For "feels slow" reports, use the log: ops over 10ms are written live as49`slow operation`, and quitting writes a per-op `perf summary` (count/avg/max).50Reproduce → quit → grep the log file.5152Every response is one JSON line. `dump` returns the screen as an escaped53string in the `screen` field — pipe through `jq -r .screen` to read it. For54single requests prefer `nc -q1` (closes after the response); only multi-line55batches need plain `nc` under `timeout`.5657Key token syntax: whitespace-separated; single chars as-is (uppercase implies58Shift); special keys `<enter> <esc> <tab> <backtab> <space> <up> <down> <left>59<right> <home> <end> <pgup> <pgdn> <backspace> <c-x>` (Ctrl+x). To type a word60in an input dialog, space-separate the letters: `c f i x <space> b u g <enter>`.6162Full protocol details: `docs/debugging.md`. Implementation: `src/debug_server.rs`.6364## Gotchas that will waste your time6566- **Double-click** = two clicks on the same cell within 400 ms. Separate `nc`67 invocations are too slow — send both clicks (plus the leading `dump` that68 records the layout) in ONE connection:6970 ```bash71 printf '%s\n%s\n%s\n%s\n' \72 '{"cmd":"dump","width":110,"height":30}' \73 '{"cmd":"mouse","kind":"click","x":60,"y":24}' \74 '{"cmd":"mouse","kind":"click","x":60,"y":24}' \75 '{"cmd":"state"}' | timeout 4 nc 127.0.0.1 $PORT76 ```7778- Commands are processed after the event-poll tick, so responses can lag up to79 ~200 ms; wrap `nc` in `timeout` and don't interpret slowness as a hang.80- A held-open `nc` may exit non-zero via `timeout` even after delivering the81 response — check the output, not the exit code.82- **Injected input bypasses the terminal's input layer.** `keys`/`mouse`83 commands go straight into the app, so they cannot verify anything that84 depends on terminal modes — e.g. mouse tracking escape sequences85 (?1000/?1002/?1003) set in `src/tui.rs`. Changes there need a human in a86 real terminal.87- **`q` only quits from the graph pane.** If another pane is focused or a88 popup is open (e.g. after a mouse click), `q`/`<esc>` first returns89 focus/closes the popup and the app keeps running. Send90 `{"cmd":"keys","keys":"q q"}` and confirm exit: a follow-up `nc` connection91 must be refused. (`pgrep -af keifu` matches your own shell's command line —92 don't trust it.)9394## Verification loop95961. `dump` and confirm the precondition is on screen (e.g. the row you'll click).972. `keys` / `mouse` to act.983. `state` + `dump`, then assert: grep the dump for expected text, compare99 `selected_index` / `mode` / `focused_pane` in the state JSON.1004. Quit, and read `/tmp/keifu.log` for the tracing trail101 (`KEIFU_LOG=trace` for more detail; useful for async diff-load issues).102103A fix is not verified until step 3 shows the corrected behavior on a dump that104previously showed the bug.