12 KiB
pikl-menu: Development Plan
Implementation order, phase definitions, and the running list of ideas. The DESIGN.md is the source of truth for what pikl-menu is. This doc is the plan for how and when we build it.
Principles
- Get a working vertical slice before going wide on features
- pikl-core stays rendering-agnostic. No GUI or TUI deps in the library.
- Every feature gets tests in pikl-core before frontend work
- Ship something usable early, iterate from real usage
- Don't optimize until there's a reason to
Phase 1: Core Loop (TUI) ✓
The minimum thing that works end-to-end.
Deliverables:
- pikl-core: Item store (accepts JSON lines on stdin, plain text fallback)
- pikl-core: Basic fuzzy filter on label
- pikl-core: Single selection, Enter to confirm, Escape to cancel
- pikl-core: Event bus with on-select and on-cancel hooks (shell commands)
- pikl-tui: ratatui list renderer
- pikl (CLI): Reads stdin, opens TUI, emits selected item as JSON on stdout
- Exit codes: 0 = selected, 1 = cancelled, 2 = error
- CI: Strict clippy, fmt, tests on Linux + macOS
Done when: ls | pikl works and prints the selected
item.
Phase 1.5: Action-fd (Headless Mode) ✓
Scriptable, non-interactive mode for integration tests and
automation. Small enough to slot in before phase 2. It's
about 100 lines of new code plus the binary orchestration
for show-ui.
Deliverables:
--action-fd <N>: read action script from file descriptor N- Action protocol: plain text, one action per line (filter, move-up/down, confirm, cancel, etc.)
show-ui/show-tui/show-guiactions for headless-to-interactive handoff- Upfront validation pass: reject unknown actions, malformed args, actions after show-ui
--stdin-timeout <seconds>: default 30s in action-fd mode, 0 in interactive mode- Default viewport height of 50 in headless mode
- Binary integration tests: spawn pikl with piped stdin + action-fd, assert stdout
Done when: echo -e "hello\nworld" | pikl --action-fd 3
with confirm on fd 3 prints "hello" to stdout.
Phase 2: Navigation & Filtering ✓
Make it feel like home for a vim user. The filter system is the real star here: strategy prefixes, pipeline chaining, incremental caching.
Implementation order:
- Mode system in core (Insert/Normal, Ctrl+N/Ctrl+E to switch)
- Normal mode vim nav (j/k/gg/G/Ctrl+D/U/Ctrl+F/B)
/in normal mode enters insert mode with filter focused- Filter strategy engine (prefix parsing:
',!,!',/pattern/,!/pattern/) fancy-regexintegration for the regex strategy- Filter pipeline (
|chaining between stages, incremental caching)
Deliverables:
- Insert mode / normal mode (Ctrl+N to normal, Ctrl+E to insert)
- Escape always cancels and exits, in any mode
- Normal mode: j/k, gg, G, Ctrl+D/U, Ctrl+F/B
/in normal mode enters filter (insert) mode--start-mode normalflag- Filter strategies via inline prefixes: fuzzy (default),
exact (
'term), inverse (!term,!'term), regex (/pattern/,!/pattern/) - fancy-regex integration (unlimited capture groups, lookaround)
- Filter pipeline with
|: each stage narrows the previous stage's output - Incremental filter caching: each stage caches item indices, only recomputes from the edited stage forward
- Arrow keys to navigate within the filter text and edit earlier pipeline segments
\|escapes a literal pipe in the query- Case rules: fuzzy = smart case, exact =
case-insensitive, regex = case-sensitive (use
(?i)for insensitive)
Deferred to later:
- H/M/L (viewport-relative jumps): nice-to-have, not essential
- Filter syntax highlighting in the input field: deferred to theming work
Done when: You can navigate and filter a large list
with vim muscle memory.
'log | !temp | /[0-9]+/ works as a pipeline.
Phase 3: Structured I/O & Hooks ✓
The structured data pipeline and the full hook system.
Implementation order:
- Item model expansion (sublabel, meta, icon, group as explicit optional fields on Item, alongside the raw Value)
- Output struct with action context (separate from the original item, no mutation)
- HookHandler trait in pikl-core, HookEvent enum, HookResponse enum
- Exec hooks in CLI:
--on-<event>-execflags, subprocess per event, stdout discarded - Debounce system: none / debounce(ms) / cancel-stale, configurable per hook via CLI flags
- Handler hooks in CLI:
--on-<event>flags, persistent process, stdin/stdout JSON line protocol - Handler protocol commands: add_items, replace_items, remove_items, set_filter, close
--filter-fieldsscoping (which fields the filter searches against)--formattemplate strings for display ({label} - {sublabel})- Field filters in query syntax (
meta.res:3840), integrated into the filter pipeline
Deliverables:
- Item model: sublabel, meta, icon, group as first-class optional fields
- Output: separate struct with action context (action, index) wrapping the original item
- Exec hooks (
--on-<event>-exec): fire-and-forget, subprocess per event, item JSON on stdin - Handler hooks (
--on-<event>): persistent bidirectional process, JSON lines on stdin/stdout - Handler protocol: add_items, replace_items, remove_items, set_filter, close
- Full lifecycle events: on-open, on-close, on-hover, on-select, on-cancel, on-filter
- Debounce: three modes (none, debounce, cancel-stale), per-hook CLI flags
- Default debounce: on-hover 200ms + cancel-stale, on-filter 200ms, others none
- HookHandler trait in pikl-core (core emits events, does not know what handlers do)
--filter-fields label,sublabel,meta.tags--format '{label} - {sublabel}'template rendering- Field filters:
meta.res:3840in query text - tracing for hook warnings (bad JSON, unknown actions, process exit)
Done when: The wallpaper picker use case works entirely through hooks and structured I/O. A handler hook can receive hover events and emit commands to modify menu state.
Phase 4: Multi-Select & Registers
Power selection features.
Deliverables:
- Space to toggle select, V for visual line mode
- Multi-select output (multiple JSON lines)
- Named registers (
"athrough"z) - Marks (
m{a-z},'{a-z}) --multiflag to enable multi-select mode
Done when: You can select multiple items, store them in registers, and get them all in the output.
Phase 5: Table Mode & CSV
Columnar data display.
Deliverables:
--columnsflag for table layout- Auto-alignment
- Column sorting keybinds
--input-format csvand--input-format tsv- Column-specific filtering
Done when: ps aux | pikl --input-format tsv --columns 1,10,2
renders a clean table.
Phase 6: Sessions & IPC
Persistence and external control.
Deliverables:
--session namefor state persistence- Session state: filter, scroll position, selections, marks, registers
- Session history log file
- Unix socket IPC while running
- IPC commands: push/remove/update items, set filter, read selection, close
- Protocol: newline-delimited JSON
Done when: You can close and reopen a session and find your state intact. External scripts can push items into a running pikl instance.
Phase 7: Streaming & Watched Sources
Live, dynamic menus.
Deliverables:
- Async/streaming stdin (items arrive over time, list updates progressively)
- Streaming output (on-hover events emitted to stdout)
--watch pathfor file/directory watching--watch-extensionsfilter- notify crate integration (inotify on Linux, FSEvents on macOS)
Done when: find / -name '*.log' | pikl populates
progressively. A watched directory updates the list live.
Phase 8: GUI Frontend (Wayland + X11)
The graphical overlay.
Deliverables:
- pikl-gui crate with iced
- Wayland: layer-shell overlay via iced_layershell
- X11: override-redirect window or EWMH hints
- Auto-detection of Wayland vs X11 vs fallback to TUI
--mode gui/--mode tuioverride- Theming (TOML-based, a few built-in themes)
- Image/icon rendering in item list
- Preview pane (text + images)
Done when: pikl looks and feels like a native Wayland overlay with keyboard-first interaction.
Phase 9: Drill-Down & Groups
Hierarchical navigation.
Deliverables:
on-selecthook returning{"action": "replace", "items": [...]}for drill-down- Backspace /
hin normal mode to go back - Navigation history stack
- Item groups with headers
- Tab to cycle groups
zato collapse/expand groups
Done when: A file browser built on pikl-menu can navigate directories without spawning new processes.
Open Design Notes
-
Viewport cursor preservation on filter change. When the filter narrows and the highlighted item is still in the result set, keep it highlighted (like fzf/rofi). When it's gone, fall back to the top. Needs the filter to report whether a specific original index survived the query, or the viewport to do a lookup after each filter pass.
-
Confirm-with-arguments (Shift+Enter). Select an item and also pass free-text arguments alongside it. Primary use case: app launcher where you select
lsand want to pass-lato it. The output would include both the selected item and the user-supplied arguments.Open questions:
- UX flow: does the filter text become the args on Shift+Enter? Or does Shift+Enter open a second input field for args after selection? The filter-as-args approach is simpler but conflates filtering and argument input. A two-step flow (select, then type args) is cleaner but adds a mode.
- Output format: separate field in the JSON output
(
"args": "-la")? Second line on stdout? Appended to the label? Needs to be unambiguous for scripts. - Should regular Enter with a non-empty filter that matches exactly one item just confirm that item (current behaviour), or should it also treat any "extra" text as args? Probably not, too implicit.
- Keybind: Shift+Enter is natural, but some terminals don't distinguish it from Enter. May need a fallback like Ctrl+Enter or a normal-mode keybind.
This is a core feature (new keybind, new output field), not just a launcher script concern. Fits naturally after phase 4 (multi-select) since it's another selection mode variant. The launcher script would assemble
{selected} {args}for execution.
Future Ideas (Unscheduled)
These are things we've talked about or thought of. No commitment, no order.
- Lua scripting frontend (mlua + LuaJIT): stateful/ conditional automation, natural follow-on to action-fd and IPC. Lua runtime is just another frontend pushing Actions and subscribing to MenuEvents. Deliberately deferred until after IPC (phase 6) so the event/action API is battle-tested before exposing it to a scripting language. See "Scripting Ladder" in DESIGN.md.
- WASM plugin system for custom filter strategies
pcre2feature flag for JIT-compiled regex- Frecency sorting (track selection frequency, boost common picks)
- Manifest file format for reusable pikl configurations
- Sixel / kitty graphics protocol support in TUI mode
- Custom action keybinds (Ctrl+1 through Ctrl+9) with distinct exit codes
- Accessibility / screen reader support
- Sections as a first-class concept separate from groups
- Network input sources (HTTP, WebSocket)
- Shell completion generation (bash, zsh, fish)
- Man page generation
- Homebrew formula + AUR PKGBUILD
- App launcher use case: global hotkey opens pikl as GUI
overlay, fuzzy-filters PATH binaries, launches selection
(optionally into a tmux session). Needs GUI frontend
(phase 8) and frecency sorting.
See
docs/use-cases/app-launcher.md. Setup guides:docs/guides/app-launcher.md. - App description indexing: a tool or subcommand that
builds a local cache of binary descriptions from man
pages (
whatis), .desktop file Comment fields, and macOS Info.plist data. Solves the "whatis is too slow to run per-keystroke" problem for the app launcher. Could be apikl indexsubcommand or a standalone helper script.