projmux 0.4.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,9 +8,10 @@ The recommended path when a key does not fire:
8
8
 
9
9
  1. Press the key inside `projmux shell` and see what works on your terminal
10
10
  out of the box ([Quick start](#quick-start-no-setup)).
11
- 2. If something is swallowed, run [`projmux setup`](#diagnose-projmux-setup)
12
- outside tmux — it tells you exactly which sequences reach the process and
13
- which the terminal is eating.
11
+ 2. If something is swallowed, open Settings > Labs > Diagnose keybindings, or
12
+ run [`projmux setup`](#diagnose-projmux-setup) outside tmux. Both paths tell
13
+ you exactly which sequences reach the process and which the terminal is
14
+ eating.
14
15
  3. For terminals projmux knows how to configure, run
15
16
  [`projmux init [terminal]`](#auto-config-projmux-init) as the fallback:
16
17
  preview first, then add `--apply` to merge the right bindings into your
@@ -19,6 +20,24 @@ The recommended path when a key does not fire:
19
20
  hand), use the [Manual fallback / advanced (CSI-u)](#manual-fallback--advanced-csi-u)
20
21
  section.
21
22
 
23
+ To change projmux's tmux-level chords, open Settings > Keybindings. The
24
+ in-app editor writes `~/.config/projmux/keymap.toml`, regenerates
25
+ `~/.config/projmux/tmux.conf`, and hot-reloads the live tmux config when
26
+ Settings is running inside tmux. Terminal fallback mappings are separate:
27
+ after changing fallback keys, rerun `projmux init` and restart the terminal
28
+ where applicable.
29
+
30
+ Settings > Labs > Diagnose keybindings reuses the same probe and terminal
31
+ fallback engines from `projmux setup` and `projmux init`. It lists the
32
+ keybinding catalog, lets you press one action key at a time from inside the
33
+ app, distinguishes plain / CSI-u / unexpected / timeout outcomes, and exposes
34
+ preview/apply rows for supported terminal fallbacks. When an unexpected
35
+ sequence can be safely read as a tmux plain chord, the Lab shows an explicit
36
+ `Save as plain override` row with the suggested chord; it never overwrites
37
+ `keymap.toml` from an unexpected sequence unless you select that confirmation
38
+ row. The Lab also shows whether the detected terminal can reload config after
39
+ fallback apply or needs a restart/manual reload.
40
+
22
41
  > 한국어 요약: 대부분의 터미널은 `projmux shell` 만으로 아래 키가 바로 동작합니다.
23
42
  > 동작하지 않으면 `projmux setup` 으로 어떤 키가 막혔는지 진단하고,
24
43
  > `projmux init [terminal]` 으로 자동 설정하세요. 자동 설정이 없는 터미널은
@@ -106,6 +125,10 @@ which projmux keys actually reach the process. The command auto-detects your
106
125
  terminal, then asks you to press each shortcut in turn and classifies the
107
126
  result:
108
127
 
128
+ The same diagnostic is available in-app at Settings > Labs > Diagnose
129
+ keybindings. The Lab flow reads the controlling TTY directly, so it can probe a
130
+ key while Settings itself is running inside tmux.
131
+
109
132
  | Status | Meaning |
110
133
  | --- | --- |
111
134
  | `OK plain` | The terminal forwarded the bare escape (e.g. `\x1b1` for `Alt-1`); tmux's plain bind handles it. |
@@ -118,6 +141,9 @@ tailored to the detected terminal (Ghostty, WezTerm, kitty, iTerm2,
118
141
  Alacritty, Windows Terminal, foot, VS Code, …). When projmux ships an init
119
142
  adapter for the terminal, the summary gives both the dry-run preview and the
120
143
  exact apply command, e.g. `projmux init ghostty --apply`.
144
+ Settings > Labs also includes a concise after-apply hint: Ghostty/WezTerm/kitty
145
+ can reload config, Windows Terminal and iTerm2 generally need a restarted tab
146
+ or session, and unknown terminals are marked manual.
121
147
 
122
148
  Useful flags:
123
149
 
@@ -160,6 +186,11 @@ already mapped to a *different* action are left untouched and reported as
160
186
  mappings. Re-running `projmux init --apply` after editing the file refreshes
161
187
  just the projmux-owned region.
162
188
 
189
+ `projmux init` always uses the built-in terminal fallback map. To override the
190
+ tmux chords rendered by `projmux tmux print-config`, `projmux tmux install`,
191
+ or `projmux shell`, use Settings > Keybindings or edit
192
+ `~/.config/projmux/keymap.toml`; see [Configuration](configuration.md#keymap-file).
193
+
163
194
  ### Supported terminals
164
195
 
165
196
  #### Ghostty
@@ -253,12 +284,16 @@ action fires.
253
284
  | `ESC [ 9006 u` | `User5` | AI split picker |
254
285
  | `ESC [ 9007 u` | `User6` | Settings |
255
286
  | `ESC [ 9008 u` | `User7` | New tmux window in the current pane directory |
256
- | `ESC [ 9009 u` | `User8` | Previous tmux window |
257
- | `ESC [ 9010 u` | `User9` | Next tmux window |
258
287
  | `ESC [ 9011 u` | `User10` | Rename the current tmux window |
259
288
  | `ESC [ 9012 u` | `User11` | Rename the current tmux pane label |
260
289
  | `ESC [ 9013 u` | `User12` | Project switcher popup |
261
290
 
291
+ > Previous/Next window (Alt-Shift-Left/Right) intentionally **do not** use a
292
+ > CSI-u detour — both projmux and modern terminals already agree on the
293
+ > xterm-standard modifier sequence (`\x1b[1;4D` / `\x1b[1;4C`), so tmux binds
294
+ > directly to `M-S-Left` / `M-S-Right`. This keeps the popup-side chord
295
+ > handler and the tmux root binding consuming the same sequence.
296
+
262
297
  ### Ghostty (manual)
263
298
 
264
299
  `projmux init ghostty` is the maintained path. The block below is what the
@@ -281,10 +316,14 @@ keybind = ctrl+shift+l=csi:9002u
281
316
  keybind = ctrl+shift+n=csi:9008u
282
317
  keybind = ctrl+m=csi:9011u
283
318
  keybind = ctrl+shift+m=csi:9012u
284
- keybind = alt+shift+left=csi:9009u
285
- keybind = alt+shift+right=csi:9010u
286
319
  ```
287
320
 
321
+ `Alt-Shift-Left` and `Alt-Shift-Right` are intentionally absent: Ghostty
322
+ already emits the xterm-standard `\x1b[1;4D` / `\x1b[1;4C` sequences for
323
+ those chords, which tmux now binds directly. Adding a `csi:9009u` /
324
+ `csi:9010u` override would re-introduce the detour that hid the chord from
325
+ projmux popup handlers.
326
+
288
327
  Reload Ghostty or restart the terminal after changing the config.
289
328
 
290
329
  ### Windows Terminal (manual)
@@ -1,7 +1,6 @@
1
1
  # Native Picker Engine
2
2
 
3
- This note tracks the native picker engine. Native is the default picker backend,
4
- while the `internal/ui/fzf` backend remains available as an explicit fallback.
3
+ This note tracks the native picker engine. Native is the only picker backend.
5
4
 
6
5
  The fzf compatibility surface for the native engine is tracked in
7
6
  [native-picker-parity.md](native-picker-parity.md).
@@ -16,21 +15,14 @@ The fzf compatibility surface for the native engine is tracked in
16
15
  responsible for backend routing, keyboard input, filtering, preview command
17
16
  execution, and result contracts, while moving visual composition into
18
17
  `projmuxpicker` so projmux can evolve a native picker design without coupling
19
- every visual tweak to the fzf adapter.
20
- - `internal/ui/fzf` now owns the adapter between fzf's legacy option/result
21
- shape and the backend-neutral `picker.Options` contract. App code should
22
- describe picker intent as rows, actions, preview commands, and initial focus;
23
- the fzf adapter is responsible for translating that into `--expect`,
24
- `--bind execute-silent(...)`, `+refresh-preview`, and `start:pos(N)`.
25
- - `intfzf.NewPickerRunner()` wraps fzf behind the same `picker.Runner`
26
- interface as the native runner. This is the current DI boundary for swapping
27
- picker engines while keeping fzf available.
28
- - Settings > Labs > Picker Engine stores the backend selection in
29
- `~/.config/projmux/picker-backend` and updates the live tmux environment so
30
- new picker popups can switch between `fzf` and `native` without restarting
31
- the app server.
32
- - Native is the default backend. `PROJMUX_PICKER_BACKEND=fzf` remains an
33
- explicit environment override and takes priority over the saved Labs setting.
18
+ every visual tweak to the compatibility option/result shape.
19
+ - `internal/ui/pickercompat` remains an internal compatibility mapper between
20
+ the old option/result shape and the backend-neutral `picker.Options`
21
+ contract. It is not a runtime backend. App code should
22
+ describe picker intent as rows, actions, preview commands, and initial focus,
23
+ then route through the native picker.
24
+ - Settings > Labs remains available for experimental settings, but picker
25
+ backend selection has been retired.
34
26
  - Picker flows covered by the native path include AI picker/settings, shell
35
27
  update prompt, settings hub sections, switch settings/add-pin, the main
36
28
  project switcher list, recent sessions, and notify sidebar.
@@ -41,9 +33,10 @@ The fzf compatibility surface for the native engine is tracked in
41
33
  expect keys such as notify `Ctrl-X`, `start:pos(N)` initial focus, preview
42
34
  command output, preview cycle command bindings, and sidebar focus command
43
35
  bindings.
44
- - FZF-style movement keys are supported for native selection: `Ctrl-N` moves
45
- down, while `Ctrl-P` and `Ctrl-K` move up unless the app claims the key as a
46
- custom action.
36
+ - FZF-style movement keys are supported for native selection: `Ctrl-N` and
37
+ `Ctrl-J` move down, while `Ctrl-P` and `Ctrl-K` move up unless the app claims
38
+ the key as a custom action. Up/down-family movement wraps at list boundaries;
39
+ PageUp/PageDown and Home/End remain clamped or explicit jumps.
47
40
  - Typed-query prompts support cursor-aware insertion/deletion with a visible
48
41
  prompt cursor, Left/Right, Ctrl-A/E, Delete, Backspace, Ctrl-U, and Ctrl-W
49
42
  for settings path entry.
@@ -135,7 +128,8 @@ The fzf compatibility surface for the native engine is tracked in
135
128
  ## Experimental Boundaries
136
129
 
137
130
  - The `projmuxpicker` package is intended as a foundation that can be carried
138
- forward, along with the fzf-to-picker adapter boundary in `internal/ui/fzf`.
131
+ forward, along with the compatibility option/result mapping in
132
+ `internal/ui/pickercompat`.
139
133
  Its frame, row, preview, theme, ANSI, and redraw modules are foundation code;
140
134
  Docker sandbox scripts and dependency-policy notes remain support
141
135
  scaffolding.
@@ -151,9 +145,7 @@ The fzf compatibility surface for the native engine is tracked in
151
145
  switch and sessions popup flows type a filtered query, send `Right` and
152
146
  `Alt-Down`, and assert the stored preview window/pane cursor for the selected
153
147
  session.
154
- - Public doctor dependency policy remains separate from picker backend
155
- selection. Native is the default picker backend, and fzf remains available as
156
- an explicit fallback.
148
+ - Public doctor dependency policy no longer includes an external picker binary.
157
149
 
158
150
  ## Interactive No-fzf Sandbox
159
151
 
@@ -14,8 +14,8 @@ native picker engine and is not a public dependency-policy change.
14
14
  | `--header` | AI, settings, shell update, notify | Covered | `renderNativeInteractive`, `renderNative`; settings native tests |
15
15
  | `--footer` / `--footer-border line` | AI, settings, shell update, switch, sessions, notify | Covered for interactive native screens | `renderNativeInteractive` reserves bottom footer space and renders a separator line; `TestNativeInteractiveRendersFooterAtBottom` |
16
16
  | `--ansi` | colored row labels from render package | Covered | native writes row labels directly, strips ANSI escapes from default search text, restores selected-row styling after embedded ANSI resets, and measures rendered cell width for Korean/CJK text, emoji, and combining marks; `TestFilterItemsIgnoresANSIEscapeSequences`; `TestNativeInteractiveUsesCurrentStyleForSimpleSelection`; `TestVisibleLenUsesTerminalCellWidth`; Docker e2e shows ANSI rows |
17
- | hidden value after tab delimiter | all picker selections and default fzf matching | Covered by `picker.Item.Value` and default search text | `fzf.PickerOptions`; `TestNativeRunnerFiltersAndSelectsByNumber`; `TestFilterItemsSearchesHiddenValueWhenNoSearchKey` |
18
- | plain fzf candidates without structured entries | legacy runner call shape | Covered | `fzf.PickerOptions`; `TestPickerOptionsFromFZFMapsCandidatesWhenEntriesAreEmpty` |
17
+ | hidden value after tab delimiter | all picker selections and default fzf matching | Covered by `picker.Item.Value` and default search text | `pickercompat.PickerOptions`; `TestNativeRunnerFiltersAndSelectsByNumber`; `TestFilterItemsSearchesHiddenValueWhenNoSearchKey` |
18
+ | plain fzf candidates without structured entries | compat option call shape | Covered | `pickercompat.PickerOptions`; `TestPickerOptionsFromCompatPickerMapsCandidatesWhenEntriesAreEmpty` |
19
19
  | search key filtering (`--nth`/reload filter file) | switch/sessions/notify entries | Covered by `Item.SearchText` with fzf reload order preservation | `FilterItems`; `TestFilterItemsUsesSearchTextNotMetadata`; `TestFilterItemsPreservesSearchKeyOrder` |
20
20
  | default `--smart-case` matching | all searchable picker rows | Covered | native filter keeps lower-case queries case-insensitive and uppercase queries case-sensitive; `TestFilterItemsUsesFZFSmartCase` |
21
21
  | fzf match highlighting | searchable simple picker rows | Covered for non-search-key simple rows | native highlights matched visible label runes while preserving embedded ANSI style; search-key reload lists intentionally keep fzf disabled-filter rendering without match highlights; `TestNativeInteractiveHighlightsSimpleQueryMatches`; `TestNativeInteractiveDoesNotHighlightSearchKeyReloadLists` |
@@ -26,14 +26,14 @@ native picker engine and is not a public dependency-policy change.
26
26
  | `--gap --gap-line ─` | switch, sessions, notify multi-line rows | Covered for app multiline rows | `nativeGapLine`, row-budgeted range; `TestNativeInteractiveRendersMultilineGapLine`, `TestNativeVisibleRangeCountsMultilineRenderedRows` |
27
27
  | selected multi-line marker | selected switch/session/notify cards | Covered for app multiline rows | native uses the same compact pointer-width red `▌` gutter as the first selected project line, and metadata lines align to the project-name column without the old deeper indent; `nativeContinuation`; `TestNativeInteractiveRendersSelectedMultilineContinuationMarker`; `TestInteractiveRowLinesUsesCompactSelectedMetaIndent`; `TestInteractiveRowLinesAlignsUnselectedMetaWithProjectName` |
28
28
  | fzf current row colors | simple and multi-line rows | Covered for app rows | `nativeCurrentStart`, `nativePointer`; pointer/continuation gutter tokens carry the current-row background; `TestNativeSelectedContentKeepsCurrentStyleAfterReset`; `TestNativeInteractiveUsesCurrentStyleForSimpleSelection` |
29
- | `--expect` keys | Enter/Ctrl-X/Alt-P/notify keys | Covered | `fzf.PickerOptions`; `TestNativeInteractiveSupportsCustomExpectKeys` |
29
+ | `--expect` keys | Enter/Ctrl-X/Alt-P/notify keys | Covered | `pickercompat.PickerOptions`; `TestNativeInteractiveSupportsCustomExpectKeys` |
30
30
  | printable expect keys | notify sidebar `x` ack | Covered | `TestNativeInteractiveSupportsPrintableExpectKeys`; Docker no-fzf e2e |
31
31
  | control expect keys | notify sidebar `Ctrl-X`, settings `Ctrl-Alt-S` close | Covered | `TestNativeInteractiveSupportsControlExpectKeys`; `TestNativeInteractiveSupportsControlAltCloseKeys` |
32
32
  | close `--bind key:abort` | Esc, Ctrl-C, Alt-N, Ctrl-Alt-S variants | Covered | `CloseActions`; `TestNativeRunnerUsesSharedCloseActions` |
33
33
  | terminal CSI-u key encoding | app keybind probe sequences, Ghostty/kitty-style modified keys | Covered | native handles app-specific Alt keys, generic modified letters/digits, and non-text keys such as Enter/Esc/Backspace/Tab; `TestNativeInteractiveSupportsCSIuAppKeyBindings` |
34
- | `execute-silent(...)+refresh-preview` | switch/session preview cycling | Covered for command execution and rerender loop | `fzf.PickerOptions`/`fzf.OptionsFromPicker`; `TestNativeInteractiveRunsCustomActionCommandAndRefreshes`; `TestPickerOptionsMapsFZFBindingsToContractActions`; Docker no-fzf e2e sends `Right` and `Alt-Down` before selection |
34
+ | `execute-silent(...)+refresh-preview` | switch/session preview cycling | Covered for command execution and rerender loop | `pickercompat.PickerOptions`/`pickercompat.OptionsFromPicker`; `TestNativeInteractiveRunsCustomActionCommandAndRefreshes`; `TestPickerOptionsMapsCompatBindingsToContractActions`; Docker no-fzf e2e sends `Right` and `Alt-Down` before selection |
35
35
  | `focus:execute-silent(...)` | switch sidebar focus | Covered | native renders the selection frame diff before running sidebar focus commands so movement stays visible before tmux focus side effects; `runNativeFocusAction`; `TestNativeInteractiveRunsFocusActionOnSelectionChange` |
36
- | `start:pos(N)` | switch sidebar initial row | Covered | `fzf.PickerOptions`/`fzf.OptionsFromPicker`; `TestPickerOptionsFromFZFMapsStartPosToInitialIndex`; `TestPickerOptionsMapsFZFBindingsToContractActions` |
36
+ | `start:pos(N)` | switch sidebar initial row | Covered | `pickercompat.PickerOptions`/`pickercompat.OptionsFromPicker`; `TestPickerOptionsFromCompatPickerMapsStartPosToInitialIndex`; `TestPickerOptionsMapsCompatBindingsToContractActions` |
37
37
  | `--preview` | switch, sessions | Covered by command output | `nativePreviewLines`; `TestNativeInteractiveRendersSelectedPreview` |
38
38
  | `--preview-window right,60%,border-left` | switch popup, sessions popup | Covered for projmux option shape | `renderNativeSplitPreview` renders a single-column left border without a synthetic title row, uses fzf-measured percent sizing, normalizes preview tabs/control bytes before clipping, and pads both panes to fixed row widths so long preview rows do not wrap into extra vertical rows; `TestNativeInteractiveRendersWidePreviewBesideList`; `TestNativePreviewWidthUsesPreviewWindowPercent`; `TestRenderSplitPreviewRowsPadsBothPanes`; `TestRenderSplitPreviewRowsNormalizesPreviewTabsBeforeTruncating` |
39
39
  | `--preview-window down,25%,border-top` | switch sidebar | Covered for projmux option shape | `renderNativeDownPreview` renders an immediate top border without a synthetic title row, uses fzf-measured percent sizing, and pads preview rows to the surface width; `TestNativeInteractiveRendersDownPreviewBelowList`; `TestNativePreviewHeightUsesPreviewWindowPercent`; `TestRenderDownPreviewPadsPreviewRows` |
@@ -41,9 +41,9 @@ native picker engine and is not a public dependency-policy change.
41
41
  | `--query` | typed settings path defaults | Covered | `Options.InitialQuery`; settings tests |
42
42
  | `--print-query` accept-query mode | typed settings path prompts | Covered | `Options.AcceptQuery`; `TestNativeRunnerAcceptsTypedQuery` |
43
43
  | query cursor editing | typed settings path prompts | Covered | Left/Right, Ctrl-A/E, Delete, Backspace, Ctrl-U/W query editing plus visible prompt cursor; `TestNativeInteractiveEditsTypedQueryAtCursor`; `TestNativeInteractiveSupportsQueryLineEditingKeys`; `TestNativeInteractiveCtrlUDeletesBeforeCursor`; `TestNativePromptLineRendersQueryCursor` |
44
- | terminal arrow key variants | interactive selection in tmux/docker | Covered | CSI, SS3/application cursor, modified CSI tests; app TTY `/dev/tty` fallback; raw TTY EOF polling keeps split ESC sequences from leaking into the query |
44
+ | terminal arrow key variants | interactive selection in tmux/docker | Covered | CSI, SS3/application cursor, modified CSI tests; up/down movement wraps at list boundaries while PageUp/PageDown and Home/End remain clamped or explicit jumps; app TTY `/dev/tty` fallback; raw TTY EOF polling keeps split ESC sequences from leaking into the query; `TestNativeInteractiveWrapsPreviousNavigationKeys`; `TestNativeInteractiveWrapsNextNavigationKeys`; `TestNativeInteractiveJumpNavigationRemainsClamped` |
45
45
  | mouse support | optional fzf mouse picker interaction | Partially covered | native enables SGR mouse reporting in interactive alternate-screen mode, primary mouse down focuses the clicked row, primary mouse up applies it, and wheel input moves selection; drag gestures remain outside this POC; `TestNativeInteractiveSelectsOnMouseRelease`; `TestNativeInteractiveMouseDownOnlyFocuses`; `TestNativeInteractiveIgnoresMouseReleaseBeforeDown`; `TestNativeInteractiveSupportsMouseWheelSelection`; Docker no-fzf e2e clicks the AI settings row under a PTY |
46
- | fzf navigation keys | interactive selection in searchable lists | Covered | native maps CSI-u `Ctrl-J` plus `Ctrl-N` to down and `Ctrl-P`/`Ctrl-K` to up when not claimed by a custom action; raw LF remains Enter for PTY compatibility; `TestNativeInteractiveSupportsFZFNavigationKeys` |
46
+ | fzf navigation keys | interactive selection in searchable lists | Covered | native maps CSI-u `Ctrl-J` plus `Ctrl-N` to down and `Ctrl-P`/`Ctrl-K` to up when not claimed by a custom action; up/down-family movement is safe on empty lists; raw LF remains Enter for PTY compatibility; `TestNativeInteractiveSupportsFZFNavigationKeys`; `TestNativeInteractiveNavigationKeysAreNoopForEmptyList` |
47
47
  | alternate-screen lifecycle | fzf fullscreen picker screen restore | Covered | native frame updates and screen exit return to column 0 before terminal control sequences, screen exit resets styles plus clears the alternate buffer from the home cursor before restore, and real TTY restores get a short settle window before caller handoff; `nativeScreenEnter`; `TestNativeInteractiveUsesAlternateScreen`; `TestRenderFullFrameUpdateAlwaysHomesAndWritesFrame` |
48
48
  | frame content width | fzf border inner width | Covered | `ContentLayout` uses the frame inner width so separators and rows reach the right border; `TestRendererContentLayoutUsesFrameInnerWidth` |
49
49
  | tmux popup frame interaction | native picker popups launched through `popup-toggle` | Covered for native backend popups | `popup-toggle` passes tmux `display-popup -B` when `PROJMUX_PICKER_BACKEND=native`, so the native picker owns the visible frame instead of double-drawing with the tmux popup border; native Alt-1 uses a compact native-only minimum while fzf keeps the previous project sidebar minimum; Alt-1/Alt-2 sidebar heights reserve two bottom statusbar rows; Alt-2 notify sidebar keeps the fzf-like `24%` / min `64` baseline; `TestAppRunTmuxPopupToggleUsesBorderlessPopupForNativeBackend`; `TestSessionizerSidebarWidthKeepsFZFMinimum`; `TestSidebarPopupHeightLeavesStatusbarRows`; `TestNotifySidebarWidthMatchesFZFBaseline`; `TestAppRunTmuxPopupToggleKeepsNotifySidebarFZFSizingForNative` |
@@ -52,36 +52,39 @@ native picker engine and is not a public dependency-policy change.
52
52
 
53
53
  ## Native Surface Architecture
54
54
 
55
- - `internal/ui/picker` remains the backend-neutral contract and owns fzf/native
55
+ - `internal/ui/picker` remains the backend-neutral contract and owns native
56
56
  routing, keyboard input, fuzzy filtering, action dispatch, preview command
57
57
  execution, and result handling.
58
58
  - `internal/ui/projmuxpicker` owns projmux-native visual composition: frame,
59
59
  redraw updates, ANSI width/truncation, theme tokens, prompt/footer/list
60
60
  rendering, selected row styling, scrollbars/gap rows, and preview pane
61
61
  geometry/rendering.
62
- - `internal/ui/fzf` owns the compatibility adapter in both directions:
63
- `picker.Options` becomes fzf flags/bindings for fallback, and legacy
64
- `fzf.Options` becomes `picker.Options` for the native backend. This keeps app
65
- code closer to a DI-style picker contract instead of embedding fzf binding
66
- strings at each call site.
67
- - `intfzf.NewPickerRunner()` adapts fzf to the same `picker.Runner` interface
68
- as `picker.NativeRunner`, so follow-up branches can inject either backend at
69
- a narrower boundary without deleting the existing fzf runner.
70
- - Settings > Labs > Picker Engine persists the selected backend in
71
- `~/.config/projmux/picker-backend` and updates the live tmux server
72
- environment through `PROJMUX_PICKER_BACKEND`, while direct environment
73
- overrides still win over saved config. Native is the default when no saved
74
- backend or override exists; `fzf` remains available as an explicit fallback.
62
+ - `internal/ui/pickercompat` remains as the internal compatibility option/result
63
+ mapper from older app option shapes to `picker.Options` for the native
64
+ backend. It is not a runtime backend. This keeps app code closer to a
65
+ DI-style picker contract instead of embedding binding strings at each call
66
+ site.
67
+ - Settings > Labs remains available, but picker backend selection has been
68
+ retired. Deprecated saved/env backend values normalize to native.
75
69
  - The split lets projmux grow a first-party picker design independently from
76
- the fzf compatibility adapter.
70
+ the compatibility option/result mapper.
71
+
72
+ ## Frame Chrome ANSI
73
+
74
+ Native picker frame chrome is normalized in `internal/ui/projmuxpicker`.
75
+ Titlebars restore the titlebar style after embedded ANSI resets, search prompt
76
+ and footer separators fill the available frame width, and header, row, footer,
77
+ and preview lines close any active SGR style before padding or frame borders can
78
+ inherit it. This phase does not add popup modes or change the `popup-toggle`
79
+ contract; native popups still rely on the existing borderless tmux popup path.
77
80
 
78
81
  ## Verified Flows
79
82
 
80
83
  - `ai` picker/settings: native backend routing covered by app tests. Docker
81
84
  no-fzf e2e also types `Codex` into `projmux ai settings` and verifies the
82
85
  native simple picker writes the `codex` mode without fzf.
83
- - `shell` update prompt: native backend routing covered by shared fzf-to-native
84
- adapter and settings-style typed prompt tests.
86
+ - `shell` update prompt: native backend routing covered by shared compat-to-native
87
+ bridge and settings-style typed prompt tests.
85
88
  - `settings`: native backend exercised in unit tests and Docker no-fzf e2e
86
89
  using Enter plus arrow-key navigation under a PTY. The Docker e2e also fails
87
90
  if the Settings flows write tmux no-server noise to stderr while running
@@ -121,9 +124,8 @@ native picker engine and is not a public dependency-policy change.
121
124
  - Mouse support is intentionally narrow in this POC: primary mouse down focuses
122
125
  the clicked row, primary mouse up applies it, and wheel input moves selection.
123
126
  Drag gestures and the full fzf mouse grammar are follow-up work.
124
- - The public doctor/docs dependency policy is tracked separately from picker
125
- backend selection. Native is the default picker backend, while fzf remains an
126
- explicit fallback.
127
+ - The public doctor/docs dependency policy no longer includes an external
128
+ picker binary.
127
129
  - Draft PR: https://github.com/crevissepartners/projmux/pull/98.
128
130
 
129
131
  ## Commands
@@ -2,42 +2,27 @@
2
2
 
3
3
  ## Goal
4
4
 
5
- The project switcher needs a richer picker surface than a single-line fzf row.
6
- The target interaction is a card-like list where each item can show a title plus
7
- small contextual lines such as session state, window/pane summary, branch, or
8
- path. Search should stay focused on stable identity text, especially the project
9
- or session title, instead of matching every contextual preview line.
5
+ The project switcher needs a rich native picker surface. The target interaction
6
+ is a card-like list where each item can show a title plus small contextual lines
7
+ such as session state, window/pane summary, branch, or path. Search should stay
8
+ focused on stable identity text, especially the project or session title,
9
+ instead of matching every contextual preview line.
10
10
 
11
11
  ## Current Contract
12
12
 
13
13
  The picker contract is split in two layers:
14
14
 
15
15
  - `internal/ui/picker` owns backend-neutral items, actions, preview metadata,
16
- backend selection, title-focused filtering, and the default native runner.
17
- - `internal/ui/fzf` adapts that model into the historical fzf command line.
18
-
19
- The fzf fallback backend sends one logical row per item:
20
-
21
- ```text
22
- <visible label>\t<selection value>
23
- ```
24
-
25
- fzf is configured with:
26
-
27
- - `--delimiter "\t"`
28
- - `--with-nth 1`
29
- - `--exit-0`
30
- - optional `--preview` and `--preview-window`
31
-
32
- The app depends on fzf returning the selected row and then extracts the hidden
33
- value after the first tab. This contract is simple and stable, but it limits each
34
- row to one visible line.
16
+ title-focused filtering, and the native runner.
17
+ - `internal/ui/pickercompat` is an internal compatibility option/result shape
18
+ for older app call sites. It is not a runtime backend, and product flows do
19
+ not execute the external fzf binary.
35
20
 
36
21
  ## fzf Capability Check
37
22
 
38
- The installed fzf version supports multi-line items with `--read0`. That means a
39
- single item can contain newline characters when input records are NUL-delimited.
40
- This can render card-like rows.
23
+ This section is historical context. Earlier design work evaluated fzf because it
24
+ supported multi-line items with `--read0`, where a single item can contain
25
+ newline characters when input records are NUL-delimited.
41
26
 
42
27
  The simple fzf option path is not enough for the desired search behavior:
43
28
 
@@ -59,7 +44,7 @@ Use `--read0` and NUL-delimited multi-line entries. This is the smallest change,
59
44
  but contextual card text will participate in search unless the visible card is
60
45
  kept title-only. This does not meet the intended search model.
61
46
 
62
- This path is acceptable only as a temporary visual experiment.
47
+ This path is retired and is not supported.
63
48
 
64
49
  ### 2. fzf custom filtering
65
50
 
@@ -73,13 +58,12 @@ Tradeoffs:
73
58
  - More edge cases around selection identity and tracking.
74
59
  - Still constrained by fzf's list layout and event model.
75
60
 
76
- This is viable, but it is a bridge rather than a clean long-term model.
61
+ This bridge path is retired and is not supported.
77
62
 
78
63
  ### 3. Native picker TUI
79
64
 
80
65
  Introduce a picker abstraction and implement a native terminal UI for card rows,
81
- title-focused search, stable selection identity, and app-owned key handling. fzf
82
- remains the default backend until parity is reached.
66
+ title-focused search, stable selection identity, and app-owned key handling.
83
67
 
84
68
  This best matches the desired product direction:
85
69
 
@@ -90,9 +74,9 @@ This best matches the desired product direction:
90
74
 
91
75
  ## Implemented Direction
92
76
 
93
- Do not extend the current fzf row format again as the main implementation. The
94
- previous hidden-field attempt showed that small fzf encoding changes can break
95
- selection and navigation in subtle ways.
77
+ Do not extend the retired fzf row format again. The previous hidden-field attempt
78
+ showed that small fzf encoding changes can break selection and navigation in
79
+ subtle ways.
96
80
 
97
81
  Current implementation:
98
82
 
@@ -100,12 +84,8 @@ Current implementation:
100
84
  `SearchText`, `MetaLines`, `Badges`, and `PreviewTarget`.
101
85
  - `picker.Options` carries backend-neutral actions, preview metadata, prompt,
102
86
  footer, initial query, and multiline intent.
103
- - Native is the default backend and renders the popup/sidebar surfaces.
104
- - `PROJMUX_PICKER_BACKEND=fzf` opts into the external fzf runner. Native supports
105
- multiline item rendering, title-focused search via `SearchText`, numeric
106
- selection, and shared close actions.
107
- - Switcher popup/sidebar use the selected backend for preview and key action
108
- parity, including native preview panes, raw-key navigation, and sidebar focus
109
- tracking.
110
-
111
- fzf can stay as the stable fallback while the native picker continues to mature.
87
+ - Native is the picker backend and renders the popup/sidebar surfaces.
88
+ - Native supports multiline item rendering, title-focused search via
89
+ `SearchText`, numeric selection, and shared close actions.
90
+ - Switcher popup/sidebar use native preview panes, raw-key navigation, and
91
+ sidebar focus tracking.
@@ -21,7 +21,9 @@ projmux/
21
21
  tmux/
22
22
  state/
23
23
  ui/
24
- fzf/
24
+ picker/
25
+ pickercompat/
26
+ projmuxpicker/
25
27
  render/
26
28
  version/
27
29
  docs/
@@ -36,7 +38,8 @@ projmux/
36
38
  - `cmd/projmux` contains only CLI wiring.
37
39
  - `internal/core` contains product behavior that should be testable without tmux.
38
40
  - `internal/integrations/tmux` should be the only place that knows tmux command strings and output formats.
39
- - `internal/ui/fzf` may depend on shelling out to `fzf`, but should call typed core services.
41
+ - `internal/ui/picker` and `internal/ui/projmuxpicker` own native picker behavior.
42
+ - `internal/ui/pickercompat` is an internal compatibility option/result shape for older app call sites. It is not a runtime backend; product code should route through the native picker.
40
43
  - `scripts/` is for development tooling only, not product logic.
41
44
 
42
45
  ## Early implementation order
@@ -46,5 +49,5 @@ projmux/
46
49
  3. `internal/core/pins`
47
50
  4. `internal/state`
48
51
  5. `internal/integrations/tmux`
49
- 6. `internal/ui/fzf`
52
+ 6. `internal/ui/picker`
50
53
  7. CLI command wiring
package/docs/roadmap.md CHANGED
@@ -19,7 +19,7 @@ core that 0.3 had landed.
19
19
  ### Diagnostics
20
20
 
21
21
  - `projmux doctor` — runtime dependency report. Enforces minimum tmux
22
- 3.4 and fzf 0.65.0 (`stale` status when present but below the floor).
22
+ 3.4 and checks workflow dependencies such as `git` and `stty`.
23
23
 
24
24
  ### Focus
25
25
 
@@ -52,7 +52,7 @@ core that 0.3 had landed.
52
52
 
53
53
  - Two-line clickable status bar: row 0 is the existing
54
54
  session/window/path/git/kube row, row 1 splits notify (left) and
55
- usage (right).
55
+ usage (right), with a row-0 settings click fallback.
56
56
  - `projmux statusbar click` — single dispatcher for both mouse clicks
57
57
  and the `prefix s {u,n,g,k,p,s}` keyboard chord. Window-list clicks
58
58
  on tabs short-circuit to native `select-window`.
@@ -71,20 +71,34 @@ v0.4 shipped.
71
71
 
72
72
  ### Picker UI
73
73
 
74
- - Picker-domain model separate from fzf row encoding (kept fzf as the
75
- stable fallback backend). Done in the 0.5 picker contract slice.
74
+ - Picker-domain model separate from row rendering. Done in the 0.5 picker
75
+ contract slice.
76
76
  - Native picker backend for multi-line card rows and title-focused search.
77
- Done in the 0.5 picker contract slice, later promoted to the default picker
78
- backend with `PROJMUX_PICKER_BACKEND=fzf` kept as the explicit fallback.
77
+ Done in the 0.5 picker contract slice, later promoted to the only picker
78
+ backend.
79
79
  - Port switcher popup/sidebar surfaces after parity tests cover
80
80
  selection, preview, and key actions.
81
81
 
82
82
  ### Picker dismissal
83
83
 
84
84
  - Picker-agnostic popup close/toggle handling so AI picker dismissal
85
- does not depend on fzf-specific key bindings. Done in the 0.5 picker
86
- contract slice; fzf maps close actions to `abort`, and the native runner
87
- consumes the same close action keys.
85
+ does not depend on backend-specific key bindings. Done in the 0.5 picker
86
+ contract slice; the native runner consumes shared close action keys.
87
+
88
+ ### Keybindings
89
+
90
+ - In-app Settings editor for tmux keymap entries. Done in the 0.5 keybinding
91
+ M4 slice: Settings > Keybindings lists actions, opens action detail rows,
92
+ accepts typed `plain`/`prefix` tmux chord strings, supports disable/reset,
93
+ writes `~/.config/projmux/keymap.toml`, regenerates the app tmux config, and
94
+ hot-reloads the live tmux config when Settings is running inside tmux.
95
+ - M6 progress: Settings > Labs now exposes a Keybinding Lab that reuses the
96
+ setup probe and init merge engines for per-action diagnostics, single-key
97
+ plain/CSI-u/unknown/timeout outcomes, and supported terminal fallback
98
+ preview/apply actions.
99
+ - Still outstanding for later milestones: broader terminal-specific fallback
100
+ remediation for changed/custom fallback mappings beyond the supported init
101
+ adapters.
88
102
 
89
103
  ### Docker install and E2E harness
90
104
 
package/docs/statusbar.md CHANGED
@@ -27,7 +27,10 @@ row 1 #[range=user|notify] <notify HUD pill> #[norange]
27
27
  `isWindowListRangeToken` fallback is now defense-in-depth only.
28
28
  The session, pwd, kube, and git segments on this row are wrapped
29
29
  in `#[range=user|<id>]` ranges and dispatched through the projmux
30
- handler. The git segment shows the current branch or detached commit,
30
+ handler. The standalone config also wraps the right-side `projmux`
31
+ badge as the `settings` range; the app config renders a compact
32
+ `⚙` settings chip after the clock. The git segment shows the current
33
+ branch or detached commit,
31
34
  then compact state indicators when available: `*` for local changes,
32
35
  `+N` for staged entries, and `↑N`/`↓N` for ahead/behind counts. Each
33
36
  state token gets its own compact foreground color while preserving the
@@ -46,7 +49,7 @@ A single tmux bind handles both lines:
46
49
  ```tmux
47
50
  bind-key -n MouseDown1Status if-shell -F "#{==:#{mouse_status_range},window}" \
48
51
  { select-window -t = } \
49
- { run-shell "'<projmux>' statusbar click \"#{mouse_status_range}\" --mouse-window \"#{mouse_window}\"" }
52
+ { run-shell "'<projmux>' statusbar click \"#{mouse_status_range}\" --client \"#{client_tty}\" --mouse-window \"#{mouse_window}\"" }
50
53
  ```
51
54
 
52
55
  `MouseDown1Status` fires from any line of a multi-line status bar with
@@ -57,10 +60,11 @@ bind-key -n MouseDown1Status if-shell -F "#{==:#{mouse_status_range},window}" \
57
60
  | Range id | Row | Click action | Keyboard |
58
61
  | -------- | --- | ----------------------------------------- | ------------- |
59
62
  | `session` | 0 | `projmux tmux popup-toggle sessionizer-sidebar` | `prefix s s` |
60
- | `pwd` | 0 | copy `#{pane_current_path}` to tmux buffer and show a compact `Path copied` popup | `prefix s p` |
63
+ | `pwd` | 0 | show a native-framed current-path popup; no clipboard or tmux buffer copy | `prefix s p` |
61
64
  | `kube` | 0 | `projmux tmux popup-toggle sessionizer` | `prefix s k` |
62
65
  | `git` | 0 | `projmux tmux popup-toggle sessionizer` | `prefix s g` |
63
- | `usage` | 1 | `display-popup -E -h 60% -w 80% -- projmux usage`, then wait for Enter | `prefix s u` |
66
+ | `settings` | 0 | `projmux tmux popup-toggle --client <tty> ai-split-settings` | mouse only; `prefix s s` remains `session` |
67
+ | `usage` | 1 | show a native-framed usage HUD popup from cached usage state | `prefix s u` |
64
68
  | `notify` | 1 | `projmux focus --target <newest> --source status-bar --kind segment-click`, then ack on focus success | `prefix s n` |
65
69
 
66
70
  `notify` reads the pending queue only. For a live pane-state view that is
@@ -70,13 +74,23 @@ The notify segment renders the newest queued item as a single notification
70
74
  block: project, state (`NEED`/`INFO`/`WARN`/`CRIT`), optional agent, text,
71
75
  age, and `+N` for older pending entries. Window/pane ids are not shown in the
72
76
  compact status segment.
73
- `usage` deliberately opens the detailed `projmux usage` table popup; it is the
74
- clear action surface for the compact HUD bar.
75
-
76
- The path popup uses a short title, one-line copy status, the current path, and
77
- an `Enter closes this popup` prompt. If the tmux buffer write fails, it keeps
78
- the same compact surface with `Current path` as the title and a copy-unavailable
79
- message. The notification HUD detail surface (`Alt-2` / `User2`) opens the
77
+ `usage` opens a native-framed detail HUD for the compact usage bar. It reads
78
+ the cached usage state in-process, keeps the existing `projmux usage` CLI
79
+ output shape unchanged for external consumers, aligns model/window rows with
80
+ right-aligned numeric values, dims unavailable values, and colors rows at the
81
+ same alert thresholds as the popup: amber at 80% and red at 95%.
82
+
83
+ The path popup uses the native picker frame chrome, a one-line title,
84
+ the full wrapped current path, cheap project/git metadata when available, and
85
+ an `Enter closes this popup` prompt. The click is display-only: it does not
86
+ invoke system clipboard tools and does not write a tmux paste buffer. The
87
+ popup command prints one quoted payload and waits for a plain Enter read so it
88
+ does not leave terminal key state behind. The usage popup uses the same
89
+ single-payload print and plain Enter-close pattern. It shows the authoritative
90
+ last collect timestamp when present, falls back to the cache file mtime when
91
+ needed, and colors that sync line amber once it is more than 60 seconds old.
92
+ The notification HUD detail surface
93
+ (`Alt-2` / `User2`) opens the
80
94
  right-side notification popup with newest-first rows. The popup itself is
81
95
  untitled; when decoration mode is `symbol` or `emoji`, the bell appears before
82
96
  the fzf header text instead. Selecting a row still focuses and acknowledges
@@ -100,7 +114,9 @@ bind-key -T projmux-status s run-shell '#{q:projmux} statusbar click session'
100
114
  ```
101
115
 
102
116
  The chord routes through the same dispatcher as the mouse click, so
103
- keyboard and mouse paths are functionally identical.
117
+ keyboard and mouse paths are functionally identical for keyed ranges.
118
+ There is intentionally no `prefix s s` settings fallback because that chord
119
+ already opens the session/sidebar range.
104
120
 
105
121
  ## Click failure handling
106
122
 
@@ -115,10 +131,11 @@ them as `display-message` toasts:
115
131
  <reason>`.
116
132
  - `session`, `kube`, or `git` popup launch failure: toast
117
133
  `statusbar <range>: popup failed`.
118
- - `pwd` path popup failure: keep the copied path in the tmux paste
119
- buffer when possible and fall back to a short `display-message`.
120
- - `usage` popup failure: fall back to inlining the rendered table
121
- into a single `display-message`.
134
+ - `settings` popup launch failure: toast `statusbar settings: popup failed`.
135
+ - `pwd` path popup failure: fall back to a short `display-message`
136
+ containing the current path.
137
+ - `usage` popup failure: fall back to a compact usage summary
138
+ `display-message`.
122
139
 
123
140
  `MouseX` / `MouseY` are accepted but not consumed today; the fields
124
141
  are wired through so click telemetry can land without changing the
package/docs/testing.md CHANGED
@@ -7,9 +7,8 @@ and humans run the same entrypoints.
7
7
 
8
8
  - `make test` runs the fast Go unit suite. These tests avoid tmux, TTY, GUI,
9
9
  and host shell dependencies.
10
- - Picker unit coverage includes the backend-neutral item/action contract, fzf
11
- adapter mapping, native title-focused filtering, numeric selection, and
12
- shared close actions.
10
+ - Picker unit coverage includes the backend-neutral item/action contract,
11
+ native title-focused filtering, numeric selection, and shared close actions.
13
12
  - `make test-integration` builds `test/docker/Dockerfile` and runs
14
13
  `test/integration/linux-smoke.sh` in Docker. It validates Linux dependency
15
14
  discovery, tmux config generation/install, app config reload against a real
@@ -29,7 +28,7 @@ The Docker suites are intended to cover portable Linux behavior that can be
29
28
  made deterministic in a container:
30
29
 
31
30
  - binary build and source install into an isolated prefix
32
- - `doctor` dependency checks for `tmux`, `fzf`, `git`, and `stty`
31
+ - `doctor` dependency checks for `tmux`, `git`, and `stty`
33
32
  - tmux config print/install/apply paths
34
33
  - notify queue push/list/ack/reconcile state transitions
35
34
  - focus fallback behavior when a tmux server has sessions but no attached
@@ -37,9 +36,8 @@ made deterministic in a container:
37
36
  - status rendering that only depends on tmux state and local files
38
37
 
39
38
  The test container disables networking during `docker run`. The image build may
40
- use the network to fetch the pinned base image, apt packages, and pinned fzf
41
- version, but suite execution should not need network access after the image is
42
- built.
39
+ use the network to fetch the pinned base image and apt packages, but suite
40
+ execution should not need network access after the image is built.
43
41
 
44
42
  ## Host-Only Checks
45
43