Skip to content

feat: implement the Picker (Bubble Tea v2, variant D) - #38

Merged
hammadmajid merged 11 commits into
mainfrom
feat/picker
Sep 16, 2026
Merged

hammadmajid merged 11 commits into
mainfrom
feat/picker

Conversation

@hammadmajid

Copy link
Copy Markdown
Contributor

What

Implements the Picker: internal/picker, built on Bubble Tea v2 (charm.land/bubbletea/v2 v2.0.9, charm.land/lipgloss/v2 v2.0.6, charm.land/bubbles/v2 v2.2.1, github.com/sahilm/fuzzy v0.1.3, per the bubbletea-stack research).

  • Run(rows []Row, status StatusFunc, opts Options) (Row, bool, error). rows must already be in History order; the Picker never computes that order itself. Draws on /dev/tty via tea.OpenTTY, falling back to stderr; stdout is never written.
  • Row carries a Project (Kind, Name, absolute Path), LastVisit, and Visits. StatusFunc is called once per Row via one tea.Cmd each, fanned out through tea.Batch and bounded by a semaphore of 8, keyed by Project path.
  • Options carries Vim and an initial Query.
  • Look: the accepted "variant D, grouped + preview" prototype — filter line on top, left pane rows grouped under bold Kind headers (Kinds ordered by first appearance), selected row gets a caret and accent name (no background highlight), right pane preview box, footer with rule/legend/keys and the match count. Status glyph cluster and the light/dark palette match the accepted look exactly.
  • Keys: default (typing filters, arrows/ctrl+p/ctrl+n move, enter chooses, esc cancels, ctrl+u clears) and vim (list focused on open, j/k move, g/G jump to the ends, f or / focuses the filter, esc in the filter returns to the list keeping the query, esc/q on the list cancels).
  • Narrow-terminal degradation: list width capped at 55% of the terminal, preview dropped under 30 remaining columns; relative time compresses to its short form before names truncate to a floor of 8; status cluster never shrinks; under 15 rows the legend line drops, under 10 the keys line too. Recomputed on every tea.WindowSizeMsg without losing state.
  • Empty History renders the "run cdd scan to seed History" hint.

Split across picker.go (public API, Run), model.go (Model, Init, grouping/filtering), update.go (key maps), view.go (rendering), styles.go (palette, glyphs), relative.go (relative-time formatting), layout.go (width/height degradation) — every file well under 300 lines.

Why

Closes #20.

Testing

go build ./... && go vet ./... && go test ./... all pass. Tests are table-driven, stdlib-only, in package picker_test, and exercise: Model.Update with tea.KeyPressMsg and status messages via a fake StatusFunc, Kind-group ordering, fuzzy filtering, relative-time formatting, and the width/height degradation rules — all without a real terminal.

A handful of small helpers (Model.Chosen, RelativeTime/RelativeTimeShort, Layout/ComputeLayout) were exported specifically so the external test package could exercise them directly, per this repo's testing convention.

Notes for the reviewer

  • No CLI wiring yet: this ticket is the Picker package itself. internal/jump (rows already ordered) and the cli/jump command that calls picker.Run and prints the chosen path are follow-ups (the ticket notes #27/#21 depend on this).
  • [keys] vim config plumbing (config.toml) is not part of this ticket; Options.Vim is ready for a caller to set it.
  • I did not manually verify the rendered output against a real TTY (no interactive terminal available in this environment); the look was built directly from the accepted prototype's source and the resolution issue's exact colours/glyphs/spacing rules, and is covered by the Update/layout/formatting tests instead.

🤖 Generated with Claude Code

hammadmajid and others added 11 commits September 17, 2026 00:48
charm.land/bubbletea/v2 v2.0.9, charm.land/lipgloss/v2 v2.0.6,
charm.land/bubbles/v2 v2.2.1, github.com/sahilm/fuzzy v0.1.3, per the
bubbletea-stack research (the github.com/charmbracelet import paths fail
with a module mismatch on the v2 line).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Row carries a Project (Kind, Name, absolute Path), LastVisit and Visits.
Options carries Vim and an initial Query. Run's rows arrive already in
History order; the Picker never computes that order itself.

Init fires one tea.Cmd per Row through tea.Batch, bounded by a semaphore
of 8, keyed by Project path so results land correctly even as fuzzy
filtering reorders the visible rows. Rows are grouped by Kind, ordered
by first appearance among the currently visible rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Default key map: typing filters, up/down and ctrl+p/ctrl+n move, enter
chooses, esc cancels, ctrl+u clears. Vim key map: the list is focused on
open, j/k move, g/G jump to the ends, f or / focuses the filter, esc in
the filter returns to the list keeping the query, esc or q on the list
cancels, enter chooses from either mode.

Tests drive Update with tea.KeyPressMsg and a fake StatusFunc, and check
Kind-group ordering and fuzzy filtering through the exported Model.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RelativeTime renders the long form used in the row and the preview pane
("2 weeks ago", "yesterday"). RelativeTimeShort renders the compressed
form narrow terminals fall back to (now, 5m, 2h, 3d, 2w, 3mo, 1y). Both
return "" for a zero time (never visited).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ComputeLayout sizes the list to its natural content width (caret,
longest name, status cluster, relative time), capped at 55% of the
terminal, and drops the preview pane once the remainder falls under 30
columns. Below the list's natural width, relative time compresses to
its short form first, then names truncate with "..." to a floor of 8;
the status cluster never shrinks. Under 15 rows the footer's legend
line is dropped, under 10 the keys line too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lipgloss.LightDark palette (muted, green, yellow, blue, red, accent,
rule) selected from tea.RequestBackgroundColor. Status glyph cluster
(clean/modified, untracked, ahead/behind, unknown, not-a-repo, loading),
its plain-text width for column alignment, the legend line, the keys
line, and fuzzy-match highlighting (accent bold underlined).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Filter line with the accent prompt; left pane rows grouped under bold
Kind headers with a rule, selected row gets a caret and accent name, no
background highlight; right pane preview box (kind/name, path, branch,
status words, sync, last visit absolute plus relative, visits); footer
rule, legend and keys line with the match count. Empty History renders
the "run cdd scan to seed History" hint instead of the list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The preview column was joined against a block whose first line was the
filter line, so the box's top border landed on the "❯ type to filter"
row instead of aligning with the list body. Prepend a leading newline
to the preview column, matching the accepted prototype's
`"\n" + box.Render(...)`, so it renders under a matching blank line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Layout.ListHeight was only consumed by the preview box's Height; the
list itself drew every row, pushing the footer off screen once rows
exceeded the terminal height and leaving a cursor past the fold
invisible. Port the prototype's windowing: track the cursor's line
index (counting Kind header lines), scroll so it stays on screen, and
emit exactly ListHeight lines.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The unselected caret was "  " + " " (3 columns) but the selected
caret was "›" + " " (2 columns), so every column on the selected row
shifted one place left as the cursor moved. Use a fixed 3-column
gutter ("   " / " › ") on both branches, matching the spec and the
accepted prototype.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TestModel_Update_StatusResult asserted nothing about the result,
ending with a comment claiming "arrival without panic is the
assertion". Rewrite it with a fake StatusFunc that returns a distinct
git.Status per row's path, feed the resulting messages through Update
in reverse order, and assert the rendered View shows the right glyph
on the right row, proving the keying is by Project path and not by
arrival index.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@hammadmajid
hammadmajid merged commit c5f6c32 into main Sep 16, 2026
2 of 3 checks passed
@hammadmajid
hammadmajid deleted the feat/picker branch September 16, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement the Picker

1 participant