# Native Picker Engine

This note tracks the native picker engine. Native is the only picker backend.

The fzf compatibility surface for the native engine is tracked in
[native-picker-parity.md](native-picker-parity.md).

## What This Covers

- `internal/ui/picker` is the backend-neutral contract for native picker rows,
  actions, filtering, and typed-query prompts.
- `internal/ui/projmuxpicker` is the projmux-specific native picker surface for
  frame, redraw updates, theme tokens, ANSI width/truncation,
  prompt/footer/list rendering, and preview pane layout. The POC keeps `picker`
  responsible for backend routing, keyboard input, filtering, preview command
  execution, and result contracts, while moving visual composition into
  `projmuxpicker` so projmux can evolve a native picker design without coupling
  every visual tweak to the compatibility option/result shape.
  Built-in fallback colors are centralized as semantic tokens in
  `internal/theme/palette.go`; see [theme-palette.md](theme-palette.md) for the
  current inventory and truecolor to tmux 256-color mapping policy.
- `internal/ui/pickercompat` remains an internal compatibility mapper between
  the old option/result shape and the backend-neutral `picker.Options`
  contract. It is not a runtime backend. App code should
  describe picker intent as rows, actions, preview commands, and initial focus,
  then route through the native picker.
- Settings > Labs remains available for experimental settings, but picker
  backend selection has been retired.
- Picker flows covered by the native path include AI picker/settings, shell
  update prompt, settings hub sections, switch settings/add-pin, the main
  project switcher list, recent sessions, and notify sidebar.
- The native picker supports ranked fuzzy search/filter, arrow-key selection in
  normal CSI and tmux application-cursor modes, Enter, Esc, Ctrl-C, Backspace,
  Ctrl-U, Ctrl-W, PageUp/PageDown, Home/End, modified CSI keys, custom expect
  keys such as Ctrl-X/Alt-P, printable expect keys such as notify `a`/`x`, control
  expect keys such as notify `Ctrl-X`, `start:pos(N)` initial focus, preview
  command output, preview cycle command bindings, and sidebar focus command
  bindings.
- FZF-style movement keys are supported for native selection: `Ctrl-N` and
  `Ctrl-J` move down, while `Ctrl-P` and `Ctrl-K` move up unless the app claims
  the key as a custom action. Up/down-family movement wraps at list boundaries;
  PageUp/PageDown and Home/End remain clamped or explicit jumps.
- Typed-query prompts support cursor-aware insertion/deletion with a visible
  prompt cursor, Left/Right, Ctrl-A/E, Delete, Backspace, Ctrl-U, and Ctrl-W
  for settings path entry.
- The native key parser keeps legacy parser-fixture coverage for app-specific
  modified-key escapes, plus generic modified forms such as `ESC [ 115 ; 7 u`
  for `Ctrl-Alt-S`. This is backend parser parity, not a product fallback
  route.
- Native interactive picker screens use an alternate screen lifecycle to better
  match fzf fullscreen behavior and restore the tmux pane after exit. Frame
  updates and screen exit both return to column 0 before emitting terminal
  control sequences, and screen exit resets styles plus clears the alternate
  buffer from the home cursor before restore. This keeps restore/update escapes
  and selected-session handoff output from visually trailing the bottom border
  in tmux/script captures. Native also gives real TTY screen restore a short
  settle window before returning to callers that may immediately draw a tmux
  session.
- Native interactive picker screens render inside a full-screen border frame to
  match the app's fzf `--height 100% --border` surface more closely.
- Popup-toggle commands use tmux `display-popup -B` when the native backend is
  active so the native picker owns the visible frame and does not double-draw
  with tmux's outer popup border.
- Native Alt-1 sidebar popups use the same responsive width calculation as fzf
  with a smaller native-only minimum, so the borderless native frame is not
  wider than the existing fzf sidebar surface on normal terminals.
- Native Alt-2 notify sidebar popups keep the fzf baseline width contract
  (`24%`, minimum `64`) while still letting the native picker own the frame.
- Native sidebar popups reserve two rows at the bottom for the tmux statusbar
  area instead of using the full client height.
- Simple native lists use the available terminal height after header, prompt,
  footer, and preview reservations instead of a fixed page-sized viewport.
- Navigation-only native lists mirror fzf `--disabled --no-input`: the input
  prompt is hidden, printable non-action keys do not alter the query, and
  expect/action keys still work.
- Native frame content now uses the full inner border width so prompt/list/footer
  separators reach the right border like fzf.
- Native frames can render an optional picker-owned titlebar row below the top
  border when `picker.Options.Title` is set; empty titles keep the default frame
  unchanged. Non-empty titles use a distinct neutral titlebar surface with rule
  fill and a divider row separating the title section from the search/content
  section. The native Alt-1 project sidebar uses this for a
  `Projects` titlebar.
- Native width/truncation uses terminal cell width for Korean/CJK text, emoji,
  and combining marks instead of raw rune count, so localized project names and
  decorated notify headers do not push the right frame border out of alignment.
- Searchable native pickers draw an explicit `Search` label and a muted
  separator under the prompt/count line so the query area reads as distinct
  chrome rather than the first row of the list. Footer, down-preview, and
  multi-line card gaps share the same `projmuxpicker` separator primitive for a
  more consistent native surface.
- Native interactive mode enables SGR mouse reporting while the alternate screen
  is active. Primary mouse down focuses the row under the cursor, primary mouse
  up applies it, mouse wheel moves selection up/down, and reporting is disabled
  again during screen restore.
- When terminal size detection is unavailable, native picker falls back to a
  conservative 80x24 terminal instead of assuming a wider surface. Interactive
  tmux popups still use the detected popup size when `stty size` is available.
- Simple and multi-line native rows share the same projmux current-row style and
  pointer marker rather than falling back to terminal inverse video for simple
  pickers.
- Selected multi-line rows use the same pointer-width `▌` continuation marker
  as the first selected project line, so switch/session/notify cards read as one
  focused block.
- Pointer and continuation markers render inside the current-row gutter style so
  selected cards do not visually break between the marker and row content.
- Native redraws use terminal synchronized-update wrappers and coalesced
  row-diff updates after the first frame. The frame/redraw renderer lives in
  `projmuxpicker` rather than the backend loop, skips unchanged frames, and
  avoids a trailing newline after the bottom border. This reduces visible
  keyboard-navigation flicker and prevents exact-height popups from scrolling
  the top border off screen.
- Native preview panes normalize tabs and control bytes before horizontal
  clipping, preventing long preview rows from wrapping and consuming extra
  vertical viewport rows in session popups.
- Native sidebar list scrollbars use the fixed list viewport as their track and
  measure multi-line cards in rendered rows, so the thumb does not shrink or
  jump when card heights vary.
- Switch picker git branch badges are capped more tightly for the native card
  surface, so inactive branch backgrounds do not dominate narrow Alt-1 sidebar
  rows.
- Native selection changes render their frame diff before running sidebar focus
  commands, so tmux focus/switch side effects do not delay the visible picker
  movement.
- In app TTY contexts, the native picker opens the controlling terminal
  (`/dev/tty`) before entering raw mode. This avoids stdin/stdout mismatch and
  line-mode escape leakage such as arrow keys appearing as `^[[`.
- Raw TTY reads keep polling briefly across empty reads while decoding
  escape-key sequences, so split arrow/Alt key bytes are consumed by the picker
  instead of leaking into the query or parent shell.

## Experimental Boundaries

- The `projmuxpicker` package is intended as a foundation that can be carried
  forward, along with the compatibility option/result mapping in
  `internal/ui/pickercompat`.
  Its frame, row, preview, theme, ANSI, and redraw modules are foundation code;
  Docker sandbox scripts and dependency-policy notes remain support
  scaffolding.
- Switch and sessions preview panes are native previews for the concrete
  projmux option shapes. Wide right-side preview windows render beside the
  list, and sidebar-style `down,25%,border-top` previews render below the list
  without a synthetic preview title row, using fzf-measured percent sizing.
  Preview rows are padded by `projmuxpicker` to the full preview surface width
  before the outer frame is applied, which keeps split/down preview columns
  visually stable during redraws.
  The full fzf preview-window grammar remains outside this POC surface.
- Preview cycle state is covered in Docker e2e against real tmux sessions: the
  switch and sessions popup flows type a filtered query, send `Right` and
  `Alt-Down`, and assert the stored preview window/pane cursor for the selected
  session.
- Public doctor dependency policy no longer includes an external picker binary.

## Interactive No-fzf Sandbox

Use this when you want to enter a Docker container and experience this build
directly without `fzf`. It builds the no-fzf dependency image, mounts this
worktree, builds `projmux` inside the container, creates sample projects under
`/workspace/projects`, stores the native picker backend in the sandbox config,
writes a tmux config with the same backend in the tmux server environment, and
launches `projmux shell`. It also forces UTF-8 locale inside the container. This
uses `wt path` instead of `wt run` because `docker run -it` needs the current
terminal TTY:

```sh
bash "$(wt path poc/native-picker-no-fzf)/scripts/poc-native-picker-no-fzf-sandbox.sh"
```

Inside the tmux shell, try:

```sh
projmux switch
projmux settings
projmux doctor --json
```

Manual UX checks for the Docker sandbox:

- Alt-1 opens with the top border/title visible, not clipped.
- Vertical borders stay continuous while moving Up/Down.
- Alt-1 closes the sidebar immediately when pressed again.
- Alt-2, Alt-4, and Alt-5 open their matching native popups and close
  on the same Alt key immediately.
- Alt-3 opens Recent Windows.
- Arrow keys move selection without leaking `^[[` text into the query.

`fzf` is intentionally not installed in the image.

## Automated No-fzf Docker E2E Command

Run this from the repository root. It builds a Go 1.24 Trixie no-fzf
dependency image from `test/docker/no-fzf-poc.Dockerfile`, including Go module
cache, then mounts the repository into an isolated `--network none` container,
builds `projmux`, asserts `fzf` is not on `PATH`, runs the focused native-picker
tests, stores the native backend through Settings > Labs, verifies the saved
backend works without an env override, exercises `projmux switch --ui=sidebar`
search/selection under a container PTY,
exercises `projmux switch --ui=popup` and `projmux sessions --ui=popup` against
existing tmux sessions under a wide 150x30 PTY, sends `Right` and `Alt-Down`
once to smoke the preview-cycle bindings, asserts those popup flows stay on the
right-side preview layout instead of falling back to inline preview, asserts the
explicit `Search` chrome across the searchable native picker surfaces, launches
`projmux shell` under a container PTY, verifies
that it creates a tmux session, verifies immediate launch-key close behavior for
Alt-1 through Alt-5 native popup surfaces, exercises `notify list --ui=sidebar`
with the printable `x` expect key, and exercises the settings picker under a PTY
using Enter and arrow-key navigation through the native backend. Settings flows
must keep stderr clean in the no-tmux-server container; tmux no-server noise is
treated as an e2e failure.

Short tmux-friendly form:

```sh
wt run poc/native-picker-no-fzf -- scripts/poc-native-picker-no-fzf-e2e.sh
```

The script contains the Docker image build and isolated `docker run` command:

```sh
scripts/poc-native-picker-no-fzf-e2e.sh
```

To compare another base image without editing the repo, override the build arg:

```sh
PROJMUX_POC_NO_FZF_BASE_IMAGE=golang:1.24-bookworm bash "$(wt path poc/native-picker-no-fzf)/scripts/poc-native-picker-no-fzf-sandbox.sh"
```
