Skip to content

The picker's invitation is drawn under the alternate screen, so TAB is undiscoverable #359

Closed

Description

@blooop

The defect

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.

Activity

  1. self-assigned this
    on Aug 22, 2026
  2. blooop commented on Aug 22, 2026

    @blooop
    OwnerAuthor

    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.

  3. blooop commented on Aug 22, 2026

    @blooop
    OwnerAuthor

    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.

  4. blooop commented on Aug 22, 2026

    @blooop
    OwnerAuthor

    breadcrumb: CI green on #360 — every check passing, including the coverage gates. Stopping here for review; not merged, version untouched.

  5. blooop commented on Aug 22, 2026

    @blooop
    OwnerAuthor

    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)

  6. blooop commented on Aug 22, 2026

    @blooop
    OwnerAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions