Files
pikl/docs/DEVPLAN.md
J. Champagne 8bf3366740 feat: Expand hook system to handle simple exec and plugin extensibility.
Item Model Expansion - Item now caches sublabel, icon, group with accessors. Added resolve_field_path() for dotted path traversal and field_value() on Item.
Output Struct - New OutputItem with OutputAction (select/cancel) and index. Object values flatten, strings get a value field. MenuResult::Selected now carries { value, index }.
Hook Types - Replaced the old Hook trait with HookEvent (serializable, 6 variants), HookResponse (deserializable, 5 commands), HookHandler trait (sync for dyn-compatibility), and parse_hook_response() with tracing warnings.
New Actions & Menu Methods - Added ReplaceItems, RemoveItems, ProcessHookResponse, CloseMenu actions. Menu trait gained original_index(), replace_all(), remove_by_indices(), formatted_label(). Pipeline got rebuild() and rebuild_with_values(). Smart cursor preservation on replace.
Lifecycle Events - MenuRunner emits Open, Close, Hover, Select, Cancel, Filter events through the dispatcher. Cursor tracking for Hover detection.
Debounce - DebouncedDispatcher with 4 modes: None, Debounce, CancelStale, DebounceAndCancelStale. Defaults: hover=DebounceAndCancelStale(200ms), filter=Debounce(200ms).
Exec Hooks - ShellExecHandler maps --on-{open,close,hover,select,cancel,filter}-exec flags to fire-and-forget subprocesses. Event JSON piped to stdin.
Handler Hooks - ShellHandlerHook launches persistent processes per --on-{event} flag. Bidirectional JSON lines: events on stdin, responses on stdout flowing back through Action::ProcessHookResponse. CompositeHookHandler dispatches to both.
--filter-fields - --filter-fields label,sublabel,meta.tags searches multiple fields. Combined text for fuzzy, individual for exact/regex.
--format - FormatTemplate parses {field.path} placeholders. --format '{label} - {sublabel}' controls display. TUI renders formatted_text when available.
Field Filters - meta.res:3840 in query syntax matches specific fields. !meta.res:3840 for inverse. Pipeline stores item Values for field resolution. Requires dotted path (single word colons stay fuzzy).
2026-03-14 01:42:11 -04:00

10 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-gui actions 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:

  1. Mode system in core (Insert/Normal, Ctrl+N/Ctrl+E to switch)
  2. Normal mode vim nav (j/k/gg/G/Ctrl+D/U/Ctrl+F/B)
  3. / in normal mode enters insert mode with filter focused
  4. Filter strategy engine (prefix parsing: ', !, !', /pattern/, !/pattern/)
  5. fancy-regex integration for the regex strategy
  6. 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 normal flag
  • 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:

  1. Item model expansion (sublabel, meta, icon, group as explicit optional fields on Item, alongside the raw Value)
  2. Output struct with action context (separate from the original item, no mutation)
  3. HookHandler trait in pikl-core, HookEvent enum, HookResponse enum
  4. Exec hooks in CLI: --on-<event>-exec flags, subprocess per event, stdout discarded
  5. Debounce system: none / debounce(ms) / cancel-stale, configurable per hook via CLI flags
  6. Handler hooks in CLI: --on-<event> flags, persistent process, stdin/stdout JSON line protocol
  7. Handler protocol commands: add_items, replace_items, remove_items, set_filter, close
  8. --filter-fields scoping (which fields the filter searches against)
  9. --format template strings for display ({label} - {sublabel})
  10. 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:3840 in 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 ("a through "z)
  • Marks (m{a-z}, '{a-z})
  • --multi flag 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:

  • --columns flag for table layout
  • Auto-alignment
  • Column sorting keybinds
  • --input-format csv and --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 name for 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 path for file/directory watching
  • --watch-extensions filter
  • 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 tui override
  • 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-select hook returning {"action": "replace", "items": [...]} for drill-down
  • Backspace / h in normal mode to go back
  • Navigation history stack
  • Item groups with headers
  • Tab to cycle groups
  • za to 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.

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
  • pcre2 feature 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.