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).
309 lines
10 KiB
Markdown
309 lines
10 KiB
Markdown
# 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`.
|