You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
The picker's invitation is drawn under the alternate screen, so TAB is undiscoverable #359
dl's interactive fuzzy picker prints its invitation line to stdout before opening
skim (render_select in rust/dl/src/commands.rs):
Select workspace (type to filter):
Select workspaces (type to filter, TAB to mark several):
skim then enters the alternate screen (ESC [ ? 1049 h), which replaces the
visible screen wholesale. The line is therefore invisible for the entire time the
picker is open, and only reappears once the picker has exited. Verified on a pty.
The comment above that println! claims the line is "the only place TAB is
discoverable". In practice TAB is discoverable nowhere the user can see while
picking — so for dl rm / dl stop / dl up / dl code / dl dotfiles,
multi-select exists and is undiscoverable.
The fix
skim 0.20.5 has header: Option<String> on SkimOptions, drawn inside the
picker as a sticky header. Under the reverse layout this repo asks for, src/model/mod.rs splits the window query_status / query / status / header /
selection — the header lands directly above the match list. Unlike reverse, the
field is read straight off the options by Header::with_options, so it works
without SkimOptions::build.
Seams
rust/dl/src/select.rs::skim_options(arity) — already takes the Arity that
decides the wording; the options are the picker's shape.
rust/dl/tests/picker.rs — the pty test: it renders the escape stream into a
20x100 grid and already models the alternate-screen switch, so what it asserts on
is the picker's own screen. The red test asserts the invitation is visible
there, and that the Several wording names TAB, for both arities.
Constraint
rust/dl/tests/read_side.rs::a_verb_with_no_workspace_opens_the_selector_and_no_terminal_picks_nothing
pins the exact invitation strings on stdout for the no-terminal case. Wording must
not change. Whether the stdout print stays unconditional (redundant with the header)
or becomes the no-terminal path only is the design decision to make and justify.
breadcrumb: Building: the picker's invitation drawn inside the picker, so the line that explains the rows — and, for the multi-pick verbs, the only mention of TAB — is on the screen the user is actually looking at while choosing.
Seams I will test at:
The picker's options, where the wording and the arity already meet — the same function that already pins layout/multi/no_sort. A unit test there says the header text exists and that the several-arity wording names TAB.
The picker's own screen on a real pty, which is the only seam that can tell a spelled-right option from one that draws something. This is the red test: the invitation must be visible on the picker's screen for both arities. An options-only test cannot fail for the reason this ticket exists.
Design decision to settle in the first slice: whether the stdout print stays as well (redundant once the header exists) or becomes the no-terminal path only. Wording is fixed either way.
breadcrumb: PR #360. The invitation is skim's sticky header now, drawn by the picker itself directly above the matches, so the sentence — and TAB with it — is on the screen the user is looking at while choosing.
Design settled as (b): the text is shown only where it can be read. stdout keeps it for the run that has no picker to put it on, and nothing else. Option (a) — print it as well as showing it — was rejected not for the duplicate but for what the duplicate is: a line written onto a screen known to be about to cover it, which then sits in the scrollback after the pick describing a choice already made. No new visibility was needed for (b); the no-terminal arm of the pick result already means "there was no picker", and the wording lives in one function feeding both destinations so they cannot drift. The no-terminal expectation is unchanged byte for byte — only its comment moved.
Red step confirmed on the pty before the fix: the picker's own screen showed the prompt, the counter and the three rows, and no invitation anywhere on it.
wf-review: Comment — no blocking findings. Two majors advised before merge (a comment this PR falsifies at tests/picker.rs:316-319; design decision (b) pinned by no test at commands.rs:1129-1140). Full two-axis report: #360 (review)
breadcrumb: Review found the design decision pinned by nothing — reverting to option (a) left the whole crate green, because a grid that models the alternate-screen switch by blanking itself structurally cannot see a line written before the switch. The screen the picker covers is now kept alongside the screen it draws, and the picker test asserts both halves: the invitation is on the picker's own screen, and on no other. Confirmed by mutation, not by reasoning — restoring the print turns it red and names the offending bytes.
The consequence worth remembering: for this picker, a regression will not look like a missing header. It will look like a print that came back, with the header still correct and everything still passing. That is why the negative half is the half with teeth.
Activity
blooop commentedon Aug 22, 2026
breadcrumb: Building: the picker's invitation drawn inside the picker, so the line that explains the rows — and, for the multi-pick verbs, the only mention of TAB — is on the screen the user is actually looking at while choosing.
Seams I will test at:
layout/multi/no_sort. A unit test there says the header text exists and that the several-arity wording names TAB.Design decision to settle in the first slice: whether the stdout print stays as well (redundant once the header exists) or becomes the no-terminal path only. Wording is fixed either way.
blooop commentedon Aug 22, 2026
breadcrumb: PR #360. The invitation is skim's sticky header now, drawn by the picker itself directly above the matches, so the sentence — and TAB with it — is on the screen the user is looking at while choosing.
Design settled as (b): the text is shown only where it can be read. stdout keeps it for the run that has no picker to put it on, and nothing else. Option (a) — print it as well as showing it — was rejected not for the duplicate but for what the duplicate is: a line written onto a screen known to be about to cover it, which then sits in the scrollback after the pick describing a choice already made. No new visibility was needed for (b); the no-terminal arm of the pick result already means "there was no picker", and the wording lives in one function feeding both destinations so they cannot drift. The no-terminal expectation is unchanged byte for byte — only its comment moved.
Red step confirmed on the pty before the fix: the picker's own screen showed the prompt, the counter and the three rows, and no invitation anywhere on it.
blooop commentedon Aug 22, 2026
breadcrumb: CI green on #360 — every check passing, including the coverage gates. Stopping here for review; not merged, version untouched.
blooop commentedon Aug 22, 2026
wf-review: Comment — no blocking findings. Two majors advised before merge (a comment this PR falsifies at
tests/picker.rs:316-319; design decision (b) pinned by no test atcommands.rs:1129-1140). Full two-axis report: #360 (review)blooop commentedon Aug 22, 2026
breadcrumb: Review found the design decision pinned by nothing — reverting to option (a) left the whole crate green, because a grid that models the alternate-screen switch by blanking itself structurally cannot see a line written before the switch. The screen the picker covers is now kept alongside the screen it draws, and the picker test asserts both halves: the invitation is on the picker's own screen, and on no other. Confirmed by mutation, not by reasoning — restoring the print turns it red and names the offending bytes.
The consequence worth remembering: for this picker, a regression will not look like a missing header. It will look like a print that came back, with the header still correct and everything still passing. That is why the negative half is the half with teeth.