tui: spike a vim layer — ':' command line, counts, motions (#9) - #16
Merged
Conversation
Explores the modal UI question in #9 by building the two pieces the issue suggested judging from: a ':' command line and vim motions in the traffic list. Both are confined to the traffic pane, so a count or a ':' can never reach a target app that is itself running vim. The ':' command line is the part that stands on its own, independent of modality: it is a home for actions that do not deserve a keybinding (:sort size, :export flagged). Every command calls the same model method as its key, so a command and its key cannot diverge. The command table carries its own help text and handler on one record, so the help overlay renders from the dispatcher itself rather than a parallel list. Motions: {count} prefixes, gg/G, {count}G, and }/{ to jump between hosts. Counts and gg are a prefix and a sequence, so they are built into the pane rather than bound through the keymap; } and { are single keys and so are ordinary, rebindable bindings, per #8's "a vim layer is a different keymap, not a second dispatch path". A pending count shows in the traffic header so a half-typed motion is never invisible, and esc discards it. The help overlay grew enough that it could scroll its own title off a short terminal, so it now clips with a hint. Claude-Session: https://claude.ai/code/session_01P151ygkx2NBLDtnVnUghRN
Correctness
- A pending count or a lone 'g' leaked out of the traffic pane. onKey returns
early for the leader, a leader sequence, and terminal focus, all before the
reset, so a count typed and abandoned survived a trip to the target app and
then silently multiplied a later j. clearMotion() now runs on every path that
takes focus off the list.
- ':filter' clamped an out-of-range selection to the LAST match while '/' sends
it to the first, so the two disagreed about where the cursor lands — under a
heading claiming a command and its key do the same thing. ':filter' now
follows onFilterKey.
- ctrl+q was swallowed by the ':' input. Every other modal honours it; this was
the one place in the app where the quit key did nothing.
- ':flag' reported nothing and skipped the re-clamp its siblings do, so under
'F' it could unflag the selected row out of the list and leave the selection
past the end, with no status to say anything had happened.
- truncate() sliced bytes, so any multibyte rune at the cut emitted invalid
UTF-8. It counts runes now.
- A flow pausing while ':' or '/' was open left the input focused under a
prompt reading "[e]dit [f]orward [d]rop" — those keys went into the text box
while a real client stayed blocked. Pausing now closes both inputs.
Discoverability
The overlay is 84 lines and the ':' section starts at line 62, so the previous
commit's clip-with-a-hint guaranteed the new command documentation was the
first thing cut — on every realistic terminal. The overlay scrolls now
(j/k/G/gg/pgup/pgdn), which is what makes a table that documents itself
actually reachable. {count}gg also works, matching vim.
Footguns
- ':flagged' wrote every flagged request and response body to disk, unprompted,
under the name that reads like the 'F' view toggle. It now toggles the view;
the write is spelled ':export flagged' in full, so the destructive one is
never the shorter thing to type.
- docs/configuration.md's "vim-ish list movement" example bound g and G in the
traffic context, which silently disables gg/G/{count}G — the motions this
branch adds. Retargeted the examples and documented the conflict.
- The keybindings doc said ":filter and f ... are aliases", which reads as a
claim about pane keys. In the traffic pane 'f' FORWARDS a paused flow. Recast
as command abbreviations with an explicit warning.
- The ':' binding description carried its own hardcoded command list, already
missing four commands — the exact parallel list the table exists to prevent.
Claude-Session: https://claude.ai/code/session_01P151ygkx2NBLDtnVnUghRN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Spike for #9. The issue suggested building
:commands plus counts/motions first and judging from there, so that is what this is — a working layer to try, not a commitment to full modality.Everything here is confined to the traffic pane, so a count or a
:can never reach a target app that is itself running vim. The left pane is untouched.The
:command lineThe part of #9 that stands on its own, independent of modality — a home for actions that never earned a keybinding:
Every command calls the same model method as its key, so
:curlandccannot diverge. The command table carries its name, help text, aliases, and handler on one record, so the help overlay renders from the dispatcher itself rather than a parallel list — the same principle as the keymap-driven help from #8, applied to commands.Commands stay reachable when the matching key is unbound, which is rather the point of having a command line. That required scoping one existing help assertion to the keymap-derived sections.
Motions
{count}5j,3k,2}gg/G{count}G12G}/{Counts are a prefix and
ggis a sequence, so neither is a single key the keymap can name — those are built into the pane.}and{are single keys, so they are ordinary rebindable bindings (flow.host-next/flow.host-prev), honouring #8's "a vim layer is a different keymap rather than a second dispatch path".A pending count or a lone
grenders in the traffic header, so a half-typed motion is never invisible;escdiscards it. User bindings always win — the motion path only sees keys the keymap does not claim.Also
lipgloss.Placecentres but does not clip). It now clips with a… N more lineshint. Pre-existing, but this change made it likely.textinput.Modelhas no blink context, soFocus()panics on it — inputs must come from their constructors, asNew()does.Judging it
Answering the issue's open questions from having built it:
gg/G,}/{and:all coexist with the existing plain keys, and there is still one dispatch path. The maintenance cost the issue worried about did not materialise, because none of this is a second input model — it is a prefix, two sequences, two bindings, and a command line.dd/yyoperators, and normal/insert in the editor and repeater. Operators need a pending-operator state, and an insert mode means the editor stops being always-insert. That is where two divergent input paths start, and it is a separate decision.:earns its place regardless. It pairs with Advanced filter syntax in the flow list #12 as the issue predicted —:filterand/already share a grammar here.Suggest merging this and deciding operators/insert-mode separately, rather than treating #9 as one all-or-nothing change.
Testing
go build ./...,go vet ./...,go test ./...all pass;-raceclean ontuiandconfig. New tests cover counted motions,gg/G/{count}G, host jumps (including empty list, clamping, and no-wrap at both ends), esc discarding a count, alias resolution, and command-table hygiene (no duplicate name/alias, every entry has a handler and a description).Docs:
docs/keybindings.mdgains motion and command sections;docs/configuration.mdlists the two new actions and notes why motions and:are the one part of the overlay that is not keymap-derived.Closes #9 as an exploration — the follow-on operator/insert-mode question is left open.