doc: Add project design, dev plan, coding standards, and use cases.
This commit is contained in:
121
docs/CODING_STANDARDS.md
Normal file
121
docs/CODING_STANDARDS.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Coding Standards
|
||||||
|
|
||||||
|
Rules of the road for pikl-menu. If something's not covered
|
||||||
|
here, use good judgement and keep it consistent with the rest
|
||||||
|
of the codebase.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- **No `anyhow`.** Every error type is a proper enum. No
|
||||||
|
stringly-typed errors, no `Box<dyn Error>` as a crutch.
|
||||||
|
- Use `thiserror` where it helps (derive `Error` +
|
||||||
|
`Display`), but it's not mandatory. Hand-written impls
|
||||||
|
are fine too.
|
||||||
|
- Use `Result` and `?` everywhere. Convert between error
|
||||||
|
types with `.map_err(YourVariant)`. Keep it simple, no
|
||||||
|
closures when a variant constructor will do.
|
||||||
|
- It's fine to stringify an error when crossing thread/async
|
||||||
|
boundaries (e.g. `JoinError` to `Io`), but always wrap
|
||||||
|
it in a typed variant with context.
|
||||||
|
|
||||||
|
## No Panics
|
||||||
|
|
||||||
|
- **No `unwrap()`, `expect()`, or `panic!()` in library or
|
||||||
|
binary code.** CI lints enforce this.
|
||||||
|
- We know IO and FFI can't be made perfectly safe. The goal
|
||||||
|
is to not *add* unsafe situations. Handle failures, don't
|
||||||
|
crash through them.
|
||||||
|
|
||||||
|
## Clippy & Lints
|
||||||
|
|
||||||
|
- Deny `clippy::unwrap_used`, `clippy::expect_used`,
|
||||||
|
`clippy::panic` workspace-wide. These are the guardrails
|
||||||
|
for the no-panic rule.
|
||||||
|
- Deny `clippy::dbg_macro`, `clippy::print_stdout`,
|
||||||
|
`clippy::print_stderr`. Use `tracing` instead.
|
||||||
|
- Enable `clippy::must_use_candidate`. If a function returns
|
||||||
|
a `Result`, `Option`, or any value the caller shouldn't
|
||||||
|
silently ignore, it gets `#[must_use]`.
|
||||||
|
- Run `cargo clippy -- -D warnings` in CI. Warnings are
|
||||||
|
errors.
|
||||||
|
|
||||||
|
## Unsafe Code
|
||||||
|
|
||||||
|
- `unsafe` blocks need a `// SAFETY:` comment explaining
|
||||||
|
why it's sound.
|
||||||
|
- Keep unsafe surface area minimal. If there's a safe
|
||||||
|
alternative that's not meaningfully worse, use it.
|
||||||
|
|
||||||
|
## Logging & Diagnostics
|
||||||
|
|
||||||
|
- **All logging goes through `tracing` and
|
||||||
|
`tracing-subscriber`.** No `println!`, `eprintln!`,
|
||||||
|
`dbg!()`, or manual log files.
|
||||||
|
- No temp files for logs. No `/tmp/pikl-debug.log`. None
|
||||||
|
of that.
|
||||||
|
- We'll eventually build a debug subscriber tool (think
|
||||||
|
tokio-console style). Design logging with structured
|
||||||
|
events in mind.
|
||||||
|
|
||||||
|
## Async Conventions
|
||||||
|
|
||||||
|
- Tokio is the async runtime. No mixing in other runtimes.
|
||||||
|
- Prefer structured concurrency with `JoinSet` over
|
||||||
|
fire-and-forget `tokio::spawn`. If you spawn a task, you
|
||||||
|
should be able to cancel it and know when it's done.
|
||||||
|
- Be explicit about cancellation safety. If a future holds
|
||||||
|
state that would be lost on drop, document it. This
|
||||||
|
matters for hooks and IPC especially.
|
||||||
|
- Use `tokio::select!` carefully. Every branch should be
|
||||||
|
cancellation-safe or documented as not.
|
||||||
|
|
||||||
|
## Public API Surface
|
||||||
|
|
||||||
|
- `pikl-core` is an embeddable library. Treat its public
|
||||||
|
API like a contract.
|
||||||
|
- Don't make things `pub` unless they need to be. Default
|
||||||
|
to `pub(crate)` and open up intentionally.
|
||||||
|
- Re-export the public interface from `lib.rs`. Consumers
|
||||||
|
shouldn't need to reach into submodules.
|
||||||
|
- Breaking changes to the public API should be deliberate,
|
||||||
|
not accidental side effects of refactoring.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Avoid cloning** where possible. Cheap copies (small
|
||||||
|
`Copy` types, `Arc::clone`) are fine. Cloning strings
|
||||||
|
and vecs as a convenience? Try harder.
|
||||||
|
- **Use zero-copy** patterns: borrowed slices, `Cow`, views
|
||||||
|
into existing data.
|
||||||
|
- **Prefer iterators** over collecting into intermediate
|
||||||
|
`Vec`s. Chain, filter, map. Only collect when you
|
||||||
|
actually need the collected result.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Every module in `pikl-core` gets unit tests. Every filter,
|
||||||
|
hook lifecycle event, I/O format.
|
||||||
|
- Integration tests exercise the real binary with real
|
||||||
|
pipes. No mocked IO at that level.
|
||||||
|
- Cross-platform: tests must pass on Linux and macOS. Gate
|
||||||
|
platform-specific tests with `#[cfg]`.
|
||||||
|
- When the GUI frontend lands, add snapshot/visual
|
||||||
|
regression tests.
|
||||||
|
- Build toward an e2e framework that can drive the full
|
||||||
|
tool.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `pikl-core` stays free of rendering, terminal, and GUI
|
||||||
|
deps. It's an embeddable library.
|
||||||
|
- Prefer pure-Rust deps. C deps behind feature flags only
|
||||||
|
(e.g. `pcre2`).
|
||||||
|
- Be intentional about what you pull in. If the standard
|
||||||
|
library does it, use the standard library.
|
||||||
|
|
||||||
|
## Import Ordering
|
||||||
|
|
||||||
|
- Group imports in this order: `std`, external crates,
|
||||||
|
workspace crates (`pikl_*`), then `self`/`super`/`crate`.
|
||||||
|
- Blank line between each group.
|
||||||
|
- `rustfmt` handles the rest. Don't fight it.
|
||||||
692
docs/DESIGN.md
Normal file
692
docs/DESIGN.md
Normal file
@@ -0,0 +1,692 @@
|
|||||||
|
# pikl-menu: Design Document
|
||||||
|
|
||||||
|
A keyboard-driven, hookable, streaming menu for Wayland and
|
||||||
|
X11. Built as a Rust library with TUI and GUI frontends.
|
||||||
|
|
||||||
|
Think rofi/wofi but with structured I/O, lifecycle hooks,
|
||||||
|
vim keybindings, streaming input/output, and multi-select.
|
||||||
|
The kind of tool you build a wallpaper picker on top of, or
|
||||||
|
a clipboard manager, or a process killer, or anything else
|
||||||
|
that's fundamentally "pick from a filterable list and do
|
||||||
|
something with it."
|
||||||
|
|
||||||
|
## Core Concept
|
||||||
|
|
||||||
|
pikl-menu is a **filterable, hookable, streaming item list
|
||||||
|
with structured I/O**. Everything else is a feature on top
|
||||||
|
of that core.
|
||||||
|
|
||||||
|
Items go in (stdin, file, directory watch, IPC). The user
|
||||||
|
filters, navigates, selects. Events fire (hover, select,
|
||||||
|
cancel, filter change). Selected items come out (stdout,
|
||||||
|
hooks, IPC). That's it.
|
||||||
|
|
||||||
|
## Structured I/O
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
Items are JSON lines on stdin. Each item has fields:
|
||||||
|
|
||||||
|
```jsonl
|
||||||
|
{"label": "beach_sunset.jpg", "sublabel": "/home/maple/walls/nature/", "meta": {"size": "2.4MB", "res": "3840x2160"}, "icon": "/path/to/thumb.png"}
|
||||||
|
{"label": "city_night.png", "sublabel": "/home/maple/walls/urban/", "meta": {"size": "1.8MB", "res": "2560x1440"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Minimum viable item is just `{"label": "something"}`.
|
||||||
|
Everything else is optional.
|
||||||
|
|
||||||
|
Plain text mode also works: one line per item, each line
|
||||||
|
becomes a label. Keeps it pipeable for simple use cases.
|
||||||
|
|
||||||
|
### Output
|
||||||
|
|
||||||
|
Selected items are emitted as JSON lines on stdout.
|
||||||
|
Multi-select emits multiple lines. The output includes any
|
||||||
|
metadata that came in, plus selection context:
|
||||||
|
|
||||||
|
```jsonl
|
||||||
|
{"label": "beach_sunset.jpg", "sublabel": "/home/maple/walls/nature/", "meta": {"size": "2.4MB"}, "action": "select", "index": 3}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streaming
|
||||||
|
|
||||||
|
Input can arrive over time. The list populates progressively
|
||||||
|
as items stream in. Useful for slow sources (network
|
||||||
|
queries, recursive directory walks, long-running commands).
|
||||||
|
|
||||||
|
Output can also stream. `on-hover` events can be configured
|
||||||
|
to emit to stdout as they happen, not just on final
|
||||||
|
selection. Useful for live preview pipelines.
|
||||||
|
|
||||||
|
### CSV / TSV
|
||||||
|
|
||||||
|
`--input-format csv` or `--input-format tsv` parses columnar
|
||||||
|
input. First row is headers, which become field names. Maps
|
||||||
|
naturally to table/column display mode.
|
||||||
|
|
||||||
|
## Event Hooks
|
||||||
|
|
||||||
|
Hooks are shell commands that run in response to lifecycle
|
||||||
|
events. They receive the relevant item(s) as JSON on stdin.
|
||||||
|
|
||||||
|
### Available Hooks
|
||||||
|
|
||||||
|
| Hook | Fires When | Use Case |
|
||||||
|
|---|---|---|
|
||||||
|
| `on-open` | Menu opens | Save current state for revert |
|
||||||
|
| `on-close` | Menu closes (any reason) | Cleanup |
|
||||||
|
| `on-hover` | Selection cursor moves to a new item | Live wallpaper preview |
|
||||||
|
| `on-select` | User confirms selection (Enter) | Apply the choice |
|
||||||
|
| `on-cancel` | User cancels (Escape) | Revert preview |
|
||||||
|
| `on-filter` | Filter text changes | Dynamic item reloading |
|
||||||
|
| `on-mark` | User marks/unmarks an item | Visual feedback, register management |
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Hooks are set via CLI flags:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pikl --on-hover 'hyprctl hyprpaper wallpaper "DP-4, {path}"' \
|
||||||
|
--on-cancel 'hyprctl hyprpaper wallpaper "DP-4, {original}"' \
|
||||||
|
--on-hover-debounce 100
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via a manifest file for reusable configurations:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# ~/.config/pikl/wallpaper.toml
|
||||||
|
[hooks]
|
||||||
|
on-hover = 'hyprctl hyprpaper wallpaper "DP-4, {label}"'
|
||||||
|
on-cancel = 'restore-wallpaper.sh'
|
||||||
|
on-hover-debounce = 100
|
||||||
|
|
||||||
|
[display]
|
||||||
|
columns = ["label", "meta.res"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bidirectional Hooks
|
||||||
|
|
||||||
|
Hooks can return JSON on stdout to modify menu state:
|
||||||
|
- Update an item's display
|
||||||
|
- Add/remove items from the list
|
||||||
|
- Set the filter text
|
||||||
|
- Close the menu
|
||||||
|
|
||||||
|
### Debouncing
|
||||||
|
|
||||||
|
All hooks support a debounce option
|
||||||
|
(`--on-hover-debounce 100`). When the user is scrolling
|
||||||
|
fast, only the last event in the debounce window fires.
|
||||||
|
Built-in, no external tooling needed.
|
||||||
|
|
||||||
|
## Filtering
|
||||||
|
|
||||||
|
### Strategies
|
||||||
|
|
||||||
|
The filter strategy is determined by the prefix/syntax of
|
||||||
|
the query text. No separate mode selector. The filter engine
|
||||||
|
parses the query on every keystroke and picks the right
|
||||||
|
strategy.
|
||||||
|
|
||||||
|
| Prefix | Strategy | Keeps/Removes | Case | Example |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| (none) | Fuzzy (nucleo) | keeps matches | smart case | `bch snst` matches "beach_sunset" |
|
||||||
|
| `'` | Exact substring | keeps matches | case-insensitive | `'sunset` matches "Sunset_beach" |
|
||||||
|
| `!` | Fuzzy inverse | removes matches | smart case | `!urban` excludes items matching "urban" |
|
||||||
|
| `!'` | Exact inverse | removes matches | case-insensitive | `!'temp` excludes items containing "temp" |
|
||||||
|
| `/pattern/` | Regex | keeps matches | case-sensitive | `/\d{4}x\d{4}/` matches resolution strings |
|
||||||
|
| `!/pattern/` | Regex inverse | removes matches | case-sensitive | `!/\.bak$/` excludes backup files |
|
||||||
|
|
||||||
|
**Case sensitivity rules:**
|
||||||
|
|
||||||
|
- **Fuzzy (nucleo):** Smart case. Case-insensitive until
|
||||||
|
you type an uppercase character.
|
||||||
|
- **Exact substring:** Case-insensitive always.
|
||||||
|
- **Regex:** Case-sensitive by default. Use `(?i)` flag
|
||||||
|
inside the pattern for case-insensitive matching. This
|
||||||
|
follows standard regex convention.
|
||||||
|
|
||||||
|
The `!` prefix is a modifier that inverts any strategy. It
|
||||||
|
stacks with `'` and `/pattern/`.
|
||||||
|
|
||||||
|
### Chaining (Pipeline)
|
||||||
|
|
||||||
|
Filters chain with `|` (pipe). Each stage takes the previous
|
||||||
|
stage's output as its input and narrows further:
|
||||||
|
|
||||||
|
```
|
||||||
|
'log | !temp | /[0-9]+/
|
||||||
|
```
|
||||||
|
|
||||||
|
Means: exact match "log", then exclude items containing
|
||||||
|
"temp", then keep only items with at least one digit. Each
|
||||||
|
`|` is a pipeline step, not an OR.
|
||||||
|
|
||||||
|
OR semantics live inside regex: `/foo|bar/` matches either.
|
||||||
|
Outside regex, `|` always means "then filter again."
|
||||||
|
|
||||||
|
**Escaping:** `\|` produces a literal pipe character in
|
||||||
|
the query.
|
||||||
|
|
||||||
|
**Whitespace:** Spaces around `|` are trimmed. `log|!temp`
|
||||||
|
and `log | !temp` are identical.
|
||||||
|
|
||||||
|
**Empty segments:** Skipped silently. `log | | foo` is the
|
||||||
|
same as `log | foo`.
|
||||||
|
|
||||||
|
### Incremental Evaluation
|
||||||
|
|
||||||
|
The filter pipeline uses an incremental caching strategy to
|
||||||
|
avoid redundant work:
|
||||||
|
|
||||||
|
- Each pipeline stage caches its result as a set of item
|
||||||
|
indices (not copies of the items).
|
||||||
|
- When the user edits a segment, only that stage and
|
||||||
|
everything downstream is recomputed. Upstream stages keep
|
||||||
|
their cached results.
|
||||||
|
- Backspacing over a `|` pops the last stage. Instant, no
|
||||||
|
recomputation.
|
||||||
|
- Typing a new `|` pushes a new empty stage.
|
||||||
|
- Within a fuzzy stage, nucleo's own incremental narrowing
|
||||||
|
applies when appending characters.
|
||||||
|
- Regex and exact stages recompute fully on each keystroke
|
||||||
|
within their segment, but they run against the previous
|
||||||
|
stage's (already filtered) output, so the input set is
|
||||||
|
small.
|
||||||
|
|
||||||
|
The cursor position determines which segment is active.
|
||||||
|
Arrow keys move through the query text normally. Editing an
|
||||||
|
earlier segment invalidates from that point forward and
|
||||||
|
cascades recomputation.
|
||||||
|
|
||||||
|
### Regex Engine
|
||||||
|
|
||||||
|
Built on `fancy-regex` (pure Rust, no C deps). Full
|
||||||
|
PCRE2-style features:
|
||||||
|
- Unlimited capture groups (no artificial cap)
|
||||||
|
- Lookahead / lookbehind
|
||||||
|
- Non-greedy quantifiers
|
||||||
|
- Unicode support
|
||||||
|
- Named groups
|
||||||
|
|
||||||
|
Optional `pcre2` feature flag for JIT-compiled patterns
|
||||||
|
when performance matters on huge datasets.
|
||||||
|
|
||||||
|
### Filter Scoping
|
||||||
|
|
||||||
|
By default, filters match against the label.
|
||||||
|
`--filter-fields label,sublabel,meta.tags` expands the
|
||||||
|
search scope.
|
||||||
|
|
||||||
|
Field filters (`meta.res:3840`) are a Phase 3 feature that
|
||||||
|
builds on top of this pipeline infrastructure.
|
||||||
|
|
||||||
|
### Future: Syntax Highlighting
|
||||||
|
|
||||||
|
The filter input is a single text field. A future
|
||||||
|
enhancement is to syntax-highlight the query: color-code
|
||||||
|
prefixes (`'`, `!`, `/`), pipe delimiters, and regex
|
||||||
|
patterns so the user gets visual feedback on how their query
|
||||||
|
is being parsed. Low-cost, high-value, but deferred until
|
||||||
|
we get into proper theming work.
|
||||||
|
|
||||||
|
## Navigation & Interaction
|
||||||
|
|
||||||
|
### Modes
|
||||||
|
|
||||||
|
pikl-menu has two input modes, inspired by vim:
|
||||||
|
|
||||||
|
**Insert mode** (default on open):
|
||||||
|
- Typing goes to the filter input
|
||||||
|
- Arrow keys navigate the list
|
||||||
|
- Ctrl+N switches to normal mode
|
||||||
|
- Escape always cancels and exits (no exceptions, in any
|
||||||
|
mode)
|
||||||
|
|
||||||
|
**Normal mode**:
|
||||||
|
- Full vim keybindings for navigation
|
||||||
|
- Ctrl+E switches to insert (edit) mode
|
||||||
|
- `/` enters insert mode with filter focused (like vim
|
||||||
|
search)
|
||||||
|
- Escape cancels and exits
|
||||||
|
|
||||||
|
`--start-mode normal` to open in normal mode instead.
|
||||||
|
|
||||||
|
**Escape is sacred.** It always means cancel and exit,
|
||||||
|
regardless of mode. It never means "go back to previous
|
||||||
|
mode." This is a deliberate departure from vim to keep the
|
||||||
|
exit path predictable.
|
||||||
|
|
||||||
|
**Command mode** is planned for the future but not yet
|
||||||
|
designed.
|
||||||
|
|
||||||
|
### Vim Keybindings (Normal Mode)
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| `j` / `k` | Down / up |
|
||||||
|
| `g g` | Jump to top |
|
||||||
|
| `G` | Jump to bottom |
|
||||||
|
| `Ctrl+D` / `Ctrl+U` | Half-page down / up |
|
||||||
|
| `Ctrl+F` / `Ctrl+B` | Full page down / up |
|
||||||
|
| `/` | Enter filter (insert) mode |
|
||||||
|
| `n` / `N` | Next / previous filter match |
|
||||||
|
| `m{a-z}` | Set mark at current position |
|
||||||
|
| `'{a-z}` | Jump to mark |
|
||||||
|
| `"a` | Select into register `a` |
|
||||||
|
| `v` | Toggle multi-select on current item |
|
||||||
|
| `V` | Visual line select mode |
|
||||||
|
| `Enter` | Confirm selection |
|
||||||
|
| `Escape` | Cancel and exit |
|
||||||
|
| `q` | Cancel and exit |
|
||||||
|
| `Space` | Toggle select current item and move down |
|
||||||
|
|
||||||
|
`H`/`M`/`L` (viewport-relative jumps) are deferred. They're
|
||||||
|
nice-to-have but not essential for the first pass of vim
|
||||||
|
navigation.
|
||||||
|
|
||||||
|
### Multi-Select
|
||||||
|
|
||||||
|
- `Space` or `v` toggles selection on current item
|
||||||
|
- `V` enters visual line mode, moving up/down selects ranges
|
||||||
|
- All selected items are emitted on confirm
|
||||||
|
- `"a` through `"z`: select into named registers (like vim
|
||||||
|
yanking)
|
||||||
|
- Registers can be recalled or included in output
|
||||||
|
|
||||||
|
### Drill-Down
|
||||||
|
|
||||||
|
An `on-select` hook can return
|
||||||
|
`{"action": "replace", "items": [...]}` to replace the
|
||||||
|
current list in-place. No new process spawn. Combined with
|
||||||
|
a back keybind (`Backspace` or `h` in normal mode), this
|
||||||
|
gives you hierarchical navigation.
|
||||||
|
|
||||||
|
## Display
|
||||||
|
|
||||||
|
### List Mode (default)
|
||||||
|
|
||||||
|
Vertical list of items. Each entry shows available fields
|
||||||
|
based on a format template:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pikl --format '{icon} {label} <dim>{sublabel}</dim>'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Table / Column Mode
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pikl --columns label,meta.res,meta.size --column-separator ' │ '
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-aligns columns. Sortable per-column with keybinds.
|
||||||
|
CSV/TSV input maps naturally here.
|
||||||
|
|
||||||
|
### Preview Pane
|
||||||
|
|
||||||
|
Optional side panel that runs a command on the hovered item
|
||||||
|
and displays its stdout:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pikl --preview 'cat {meta.path}' --preview-position right --preview-width 50%
|
||||||
|
```
|
||||||
|
|
||||||
|
For GUI mode, this can render images (thumbnails, etc). For
|
||||||
|
TUI mode, text or sixel/kitty graphics protocol.
|
||||||
|
|
||||||
|
### Sections / Groups
|
||||||
|
|
||||||
|
Items can include a `group` field. pikl-menu renders group
|
||||||
|
headers and allows Tab to cycle between groups. Groups are
|
||||||
|
collapsible in normal mode with `za` (vim fold toggle).
|
||||||
|
|
||||||
|
### Theming
|
||||||
|
|
||||||
|
Minimal but configurable. TOML-based:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# ~/.config/pikl/theme.toml
|
||||||
|
[colors]
|
||||||
|
background = "#1a1a2e"
|
||||||
|
foreground = "#e0e0e0"
|
||||||
|
selection = "#e94560"
|
||||||
|
filter = "#0f3460"
|
||||||
|
dim = "#666666"
|
||||||
|
border = "#333333"
|
||||||
|
|
||||||
|
[layout]
|
||||||
|
width = "40%"
|
||||||
|
position = "right" # left, right, center
|
||||||
|
padding = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
A few built-in themes ship with pikl. `--theme monokai` or
|
||||||
|
`--theme nord` etc.
|
||||||
|
|
||||||
|
## Sessions
|
||||||
|
|
||||||
|
`--session name` persists the menu's state (filter text,
|
||||||
|
scroll position, selected items, marks, registers) across
|
||||||
|
invocations. State lives in
|
||||||
|
`~/.local/state/pikl/sessions/`.
|
||||||
|
|
||||||
|
No `--session` = ephemeral, nothing saved.
|
||||||
|
|
||||||
|
Session names are just strings, use shell expansion for
|
||||||
|
dynamic names:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pikl --session "walls-$(hostname)"
|
||||||
|
pikl --session "logs-$USER"
|
||||||
|
```
|
||||||
|
|
||||||
|
Session history is a log file alongside the state. Other
|
||||||
|
tools can tail it for observability.
|
||||||
|
|
||||||
|
## Watched Sources
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pikl --watch ~/Pictures/walls/ --watch-extensions jpg,png,webp
|
||||||
|
```
|
||||||
|
|
||||||
|
The list updates live as files are added, removed, or
|
||||||
|
renamed. Combines naturally with hooks: a new file appears,
|
||||||
|
the list updates, the user can immediately select it.
|
||||||
|
|
||||||
|
Also supports watching a file (one item per line) or a
|
||||||
|
named pipe.
|
||||||
|
|
||||||
|
## IPC
|
||||||
|
|
||||||
|
While running, pikl-menu listens on a Unix socket
|
||||||
|
(`/run/user/$UID/pikl-$PID.sock`). External tools can:
|
||||||
|
|
||||||
|
- Push new items
|
||||||
|
- Remove items
|
||||||
|
- Update item fields
|
||||||
|
- Set the filter text
|
||||||
|
- Read current selection
|
||||||
|
- Close the menu
|
||||||
|
|
||||||
|
Protocol is newline-delimited JSON. Simple enough to use
|
||||||
|
with `socat` or any language's Unix socket support.
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| 0 | User selected item(s) |
|
||||||
|
| 1 | User cancelled (Escape / q) |
|
||||||
|
| 2 | Error (bad input, hook failure, etc) |
|
||||||
|
| 10-19 | Custom actions (configurable keybinds, future) |
|
||||||
|
|
||||||
|
## Platform Support
|
||||||
|
|
||||||
|
### Primary Targets
|
||||||
|
|
||||||
|
- **Wayland** (layer-shell overlay via `iced` +
|
||||||
|
`iced_layershell`)
|
||||||
|
- **X11** (standard window with override-redirect or EWMH
|
||||||
|
hints)
|
||||||
|
- **TUI** (ratatui, runs in any terminal)
|
||||||
|
|
||||||
|
### Rendering Selection
|
||||||
|
|
||||||
|
pikl auto-detects the environment:
|
||||||
|
1. If `$WAYLAND_DISPLAY` is set: GUI (Wayland)
|
||||||
|
2. If `$DISPLAY` is set: GUI (X11)
|
||||||
|
3. Otherwise: TUI
|
||||||
|
|
||||||
|
Override with `--mode gui` or `--mode tui`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
pikl-core (library crate)
|
||||||
|
├── Item store - structured items, streaming input, indexing
|
||||||
|
├── Filter engine - fuzzy, regex, fancy-regex, field filters, chaining
|
||||||
|
├── Selection - single, multi, registers, marks
|
||||||
|
├── Hook system - lifecycle events, debouncing, bidirectional
|
||||||
|
├── Session - persistence, history log
|
||||||
|
├── IPC server - Unix socket, JSON protocol
|
||||||
|
└── Event bus - internal pub/sub connecting all the above
|
||||||
|
|
||||||
|
pikl-tui (binary crate)
|
||||||
|
├── ratatui renderer
|
||||||
|
├── Terminal input handling
|
||||||
|
└── Sixel / kitty image protocol (optional)
|
||||||
|
|
||||||
|
pikl-gui (binary crate)
|
||||||
|
├── iced + layer-shell (Wayland)
|
||||||
|
├── iced + X11 windowing
|
||||||
|
└── Native image rendering
|
||||||
|
|
||||||
|
pikl (binary crate, unified CLI)
|
||||||
|
├── CLI argument parsing (clap)
|
||||||
|
├── Auto-detection of GUI vs TUI
|
||||||
|
├── Stdin/stdout JSON streaming
|
||||||
|
└── Manifest file loading
|
||||||
|
```
|
||||||
|
|
||||||
|
`pikl-core` has zero rendering dependencies. It's the
|
||||||
|
engine. The frontends are thin layers that subscribe to the
|
||||||
|
event bus and render state.
|
||||||
|
|
||||||
|
Third-party Rust tools can depend on `pikl-core` directly
|
||||||
|
and skip the CLI entirely. Your wallpaper picker becomes a
|
||||||
|
Rust binary that calls `pikl_core::Menu::new()` with items
|
||||||
|
and hook closures.
|
||||||
|
|
||||||
|
### Frontends
|
||||||
|
|
||||||
|
Every way of driving pikl is a frontend. Frontends send
|
||||||
|
`Action` variants into the core's `mpsc` channel and
|
||||||
|
optionally subscribe to `MenuEvent` via the `broadcast`
|
||||||
|
channel. The core is frontend-agnostic: it processes actions
|
||||||
|
sequentially regardless of where they came from.
|
||||||
|
|
||||||
|
| Frontend | Input Source | Interactive | Phase |
|
||||||
|
|---|---|---|---|
|
||||||
|
| TUI | Terminal keypresses (crossterm) | Yes | 1 |
|
||||||
|
| GUI | GUI events (iced) | Yes | 8 |
|
||||||
|
| Action-fd | Pre-validated script from a file descriptor | No (unless `show-ui`) | 1.5 |
|
||||||
|
| IPC | Unix socket, JSON protocol | Yes (bidirectional) | 6 |
|
||||||
|
| Lua | LuaJIT script via mlua | Yes (stateful, conditional) | Post-6 |
|
||||||
|
|
||||||
|
This framing means new interaction modes don't require core
|
||||||
|
changes: they're just new frontends that push actions.
|
||||||
|
|
||||||
|
## Action-fd (Headless Mode)
|
||||||
|
|
||||||
|
`--action-fd <N>` reads a pre-built action script from file
|
||||||
|
descriptor N. Items come from stdin, selected output goes to
|
||||||
|
stdout. No TUI or GUI is launched unless the script requests
|
||||||
|
it.
|
||||||
|
|
||||||
|
### Protocol
|
||||||
|
|
||||||
|
One action per line, plain text:
|
||||||
|
|
||||||
|
```
|
||||||
|
filter foo
|
||||||
|
move-down
|
||||||
|
move-down
|
||||||
|
confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
Available actions:
|
||||||
|
|
||||||
|
| Action | Argument | Maps to |
|
||||||
|
|---|---|---|
|
||||||
|
| `filter <text>` | Filter string | `Action::UpdateFilter` |
|
||||||
|
| `move-up` | (none) | `Action::MoveUp` |
|
||||||
|
| `move-down` | (none) | `Action::MoveDown` |
|
||||||
|
| `move-to-top` | (none) | `Action::MoveToTop` |
|
||||||
|
| `move-to-bottom` | (none) | `Action::MoveToBottom` |
|
||||||
|
| `page-up` | (none) | `Action::PageUp` |
|
||||||
|
| `page-down` | (none) | `Action::PageDown` |
|
||||||
|
| `resize <n>` | Height (items) | `Action::Resize` |
|
||||||
|
| `confirm` | (none) | `Action::Confirm` |
|
||||||
|
| `cancel` | (none) | `Action::Cancel` |
|
||||||
|
| `show-ui` | (none) | Hand off to interactive frontend |
|
||||||
|
| `show-tui` | (none) | Hand off to TUI specifically |
|
||||||
|
| `show-gui` | (none) | Hand off to GUI specifically |
|
||||||
|
|
||||||
|
`show-ui` auto-detects the appropriate interactive frontend
|
||||||
|
(Wayland: GUI, X11: GUI, otherwise: TUI). `show-tui` and
|
||||||
|
`show-gui` are explicit overrides. All three must be the
|
||||||
|
last action in the script: anything after them is a
|
||||||
|
validation error.
|
||||||
|
|
||||||
|
### Execution Model
|
||||||
|
|
||||||
|
1. **Read + validate action-fd:** the entire script is
|
||||||
|
loaded and validated before anything runs. Unknown
|
||||||
|
actions, malformed arguments, and actions after
|
||||||
|
`show-ui`/`show-tui`/`show-gui` are rejected with
|
||||||
|
diagnostic errors.
|
||||||
|
2. **Read all stdin until EOF:** items are fully loaded. If
|
||||||
|
stdin doesn't close within the timeout (default 30s),
|
||||||
|
pikl exits with an error.
|
||||||
|
3. **Create Menu** with items, default viewport height
|
||||||
|
of 50.
|
||||||
|
4. **Execute actions** sequentially.
|
||||||
|
5. **Exit with result.** If the script ended with
|
||||||
|
`show-ui`/`show-tui`/`show-gui`, launch the interactive
|
||||||
|
frontend and let the user finish.
|
||||||
|
|
||||||
|
### Stdin Timeout
|
||||||
|
|
||||||
|
`--stdin-timeout <seconds>` controls how long pikl waits
|
||||||
|
for stdin to close.
|
||||||
|
|
||||||
|
- **Action-fd mode:** defaults to 30 seconds. Catches
|
||||||
|
mistakes like piping from a streaming source (`tail -f`).
|
||||||
|
- **Interactive mode:** defaults to 0 (no timeout). The
|
||||||
|
user can Ctrl+C.
|
||||||
|
- `--stdin-timeout 0` disables the timeout in any mode.
|
||||||
|
|
||||||
|
### Constraints
|
||||||
|
|
||||||
|
- **Action-fd is intentionally stateless.** The caller sends
|
||||||
|
actions but cannot read state back or branch on it. For
|
||||||
|
conditional/stateful automation, use Lua (when available)
|
||||||
|
or IPC.
|
||||||
|
- **Action-fd and streaming stdin are mutually exclusive.**
|
||||||
|
Action-fd requires stdin to be finite and fully consumed
|
||||||
|
before actions execute. Streaming input (phase 7) is for
|
||||||
|
interactive frontends.
|
||||||
|
- **Unknown actions are validation errors**, not silently
|
||||||
|
ignored. The script is pre-validated: there's no
|
||||||
|
forward-compatibility concern since the caller controls
|
||||||
|
both the script and the pikl version.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Simple: filter and confirm
|
||||||
|
exec 3<<'EOF'
|
||||||
|
filter projects
|
||||||
|
confirm
|
||||||
|
EOF
|
||||||
|
ls ~/dev | pikl --action-fd 3
|
||||||
|
|
||||||
|
# Pre-navigate, then hand off to user
|
||||||
|
exec 3<<'EOF'
|
||||||
|
filter .rs
|
||||||
|
move-down
|
||||||
|
move-down
|
||||||
|
show-ui
|
||||||
|
EOF
|
||||||
|
find . -type f | pikl --action-fd 3
|
||||||
|
|
||||||
|
# Integration test (Rust)
|
||||||
|
# Spawn pikl with items on stdin, actions on fd 3, assert stdout
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Crates
|
||||||
|
|
||||||
|
| Crate | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `fancy-regex` | Regex engine with lookaround support |
|
||||||
|
| `serde` + `serde_json` | Structured I/O |
|
||||||
|
| `clap` | CLI argument parsing |
|
||||||
|
| `ratatui` + `crossterm` | TUI rendering |
|
||||||
|
| `iced` + `iced_layershell` | GUI rendering (Wayland) |
|
||||||
|
| `nucleo` | Fuzzy matching with smart case and incremental narrowing |
|
||||||
|
| `tokio` | Async runtime (streaming, IPC, hooks) |
|
||||||
|
| `notify` | File/directory watching |
|
||||||
|
| `csv` | CSV/TSV input parsing |
|
||||||
|
| `toml` | Config and manifest parsing |
|
||||||
|
| `dirs` | XDG directory paths |
|
||||||
|
|
||||||
|
## Example Use Cases
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Simple picker: pipe in lines, get selection back
|
||||||
|
ls ~/walls/*.jpg | pikl
|
||||||
|
|
||||||
|
# Structured items with live preview hooks
|
||||||
|
find-walls --json | pikl \
|
||||||
|
--on-hover 'set-wallpaper {label}' \
|
||||||
|
--on-cancel 'restore-wallpaper' \
|
||||||
|
--on-hover-debounce 100
|
||||||
|
|
||||||
|
# Process killer with table view
|
||||||
|
ps aux --no-headers | pikl --input-format tsv --columns 1,10,2 --multi
|
||||||
|
|
||||||
|
# Drill-down file browser
|
||||||
|
pikl --manifest ~/.config/pikl/filebrowser.toml --session filebrowser
|
||||||
|
|
||||||
|
# Clipboard history with preview
|
||||||
|
clipman list --json | pikl --preview 'echo {meta.content}' --on-select 'clipman paste {id}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripting Ladder
|
||||||
|
|
||||||
|
pikl has a deliberate progression for automation
|
||||||
|
complexity:
|
||||||
|
|
||||||
|
1. **Action-fd:** fire-and-forget scripts. No state, no
|
||||||
|
branching, no custom DSL. For shell one-liners,
|
||||||
|
integration tests, and simple automation. If you need
|
||||||
|
conditionals, you've outgrown this.
|
||||||
|
2. **IPC** (phase 6): bidirectional JSON over Unix socket.
|
||||||
|
External tools can read state and send actions while pikl
|
||||||
|
runs interactively. Good for tool integration.
|
||||||
|
3. **Lua** (post phase 6): embedded LuaJIT via mlua. Full
|
||||||
|
stateful scripting: subscribe to events, branch on state,
|
||||||
|
loops, the works. The Lua runtime is just another
|
||||||
|
frontend pushing Actions and subscribing to MenuEvents.
|
||||||
|
For anything complex enough to need a real language.
|
||||||
|
|
||||||
|
No custom DSL. Action-fd stays simple forever. The jump from
|
||||||
|
"I need conditionals" to "use Lua" is intentional: there's
|
||||||
|
no value in a half-language.
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
Concrete workflows pikl-menu is designed to support.
|
||||||
|
These are first-class targets that inform feature
|
||||||
|
priorities, not hypotheticals. Each has its own doc
|
||||||
|
with the full writeup.
|
||||||
|
|
||||||
|
- [Wallpaper Picker](use-cases/wallpaper-picker.md):
|
||||||
|
keyboard-driven wallpaper browser with live preview
|
||||||
|
via hyprpaper hooks.
|
||||||
|
- [App Launcher](use-cases/app-launcher.md): global
|
||||||
|
hotkey launcher that fuzzy-filters PATH binaries
|
||||||
|
or .desktop entries.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Should marks/registers persist across sessions or only
|
||||||
|
within a session?
|
||||||
|
- Accessibility: screen reader support for TUI mode?
|
||||||
|
- Should `--watch` support inotify on Linux and FSEvents on
|
||||||
|
macOS, or use `notify` crate to abstract?
|
||||||
|
- Maximum practical item count before we need virtual
|
||||||
|
scrolling? (Probably around 100k)
|
||||||
|
- Should hooks run in a pool or strictly sequential?
|
||||||
|
- Plugin system via WASM for custom filter strategies?
|
||||||
|
(Probably way later if ever)
|
||||||
269
docs/DEVPLAN.md
Normal file
269
docs/DEVPLAN.md
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- JSON line input parsing (label, sublabel, meta, icon,
|
||||||
|
group)
|
||||||
|
- JSON output with action context
|
||||||
|
- Full hook lifecycle: on-open, on-close, on-hover,
|
||||||
|
on-select, on-cancel, on-filter
|
||||||
|
- Hook debouncing
|
||||||
|
- Bidirectional hooks (hook stdout modifies menu state)
|
||||||
|
- `--format` template strings for display
|
||||||
|
- Field filters (`meta.res:3840`)
|
||||||
|
- Filter scoping (`--filter-fields`)
|
||||||
|
|
||||||
|
**Done when:** The wallpaper picker use case works entirely
|
||||||
|
through hooks and structured I/O.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
137
docs/use-cases/app-launcher.md
Normal file
137
docs/use-cases/app-launcher.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Use Case: App Launcher
|
||||||
|
|
||||||
|
A global-hotkey application launcher that replaces
|
||||||
|
Spotlight, Alfred, or rofi's drun mode. Bind a key
|
||||||
|
combo to open pikl as a GUI overlay, fuzzy-filter
|
||||||
|
binaries from PATH, hit Enter to launch.
|
||||||
|
|
||||||
|
## What It Looks Like
|
||||||
|
|
||||||
|
1. User presses a global hotkey (Super on Linux,
|
||||||
|
Cmd+Space on macOS).
|
||||||
|
2. pikl opens as a centered overlay.
|
||||||
|
3. The list is pre-populated with binaries from PATH
|
||||||
|
(or parsed .desktop entries).
|
||||||
|
4. User types to fuzzy-filter. Frecency sorting puts
|
||||||
|
frequently launched apps at the top.
|
||||||
|
5. Enter launches the selection. Escape dismisses.
|
||||||
|
|
||||||
|
This should feel instant. Sub-100ms to first paint.
|
||||||
|
|
||||||
|
## How It Works With pikl
|
||||||
|
|
||||||
|
### Basic Version
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Collect PATH binaries, open pikl, run selection
|
||||||
|
compgen -c | sort -u \
|
||||||
|
| pikl --mode gui \
|
||||||
|
| xargs -I{} sh -c '{} &'
|
||||||
|
```
|
||||||
|
|
||||||
|
### With tmux Integration
|
||||||
|
|
||||||
|
On select, create a new tmux session running the
|
||||||
|
chosen binary, then switch to it. Keeps everything
|
||||||
|
inside tmux for window management.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
compgen -c | sort -u \
|
||||||
|
| pikl --mode gui \
|
||||||
|
--on-select 'cmd=$(cat);
|
||||||
|
tmux new-session -d -s "$cmd" "$cmd" &&
|
||||||
|
tmux switch-client -t "$cmd"'
|
||||||
|
```
|
||||||
|
|
||||||
|
### With .desktop Files
|
||||||
|
|
||||||
|
Parse XDG .desktop entries for proper app names and
|
||||||
|
icons instead of raw binary names:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Hypothetical helper that emits JSON items
|
||||||
|
desktop-entries --json \
|
||||||
|
| pikl --mode gui \
|
||||||
|
--format '{icon} {label} <dim>{sublabel}</dim>'
|
||||||
|
```
|
||||||
|
|
||||||
|
Input would look like:
|
||||||
|
|
||||||
|
```jsonl
|
||||||
|
{"label": "Firefox", "sublabel": "Web Browser",
|
||||||
|
"meta": {"exec": "firefox", "desktop": "firefox.desktop"},
|
||||||
|
"icon": "/usr/share/icons/.../firefox.png"}
|
||||||
|
{"label": "Alacritty", "sublabel": "Terminal",
|
||||||
|
"meta": {"exec": "alacritty"},
|
||||||
|
"icon": "/usr/share/icons/.../alacritty.png"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hyprland / Sway Keybinding
|
||||||
|
|
||||||
|
```
|
||||||
|
bind = SUPER, SPACE, exec, app-launcher.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
On macOS, use skhd or a similar hotkey daemon.
|
||||||
|
|
||||||
|
### Fallback: TUI Mode
|
||||||
|
|
||||||
|
Before the GUI frontend exists, this works today in
|
||||||
|
a drop-down terminal:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Bind to a hotkey that opens a terminal running:
|
||||||
|
compgen -c | sort -u | pikl | xargs -I{} sh -c '{} &'
|
||||||
|
```
|
||||||
|
|
||||||
|
## What pikl Features This Exercises
|
||||||
|
|
||||||
|
| Feature | Phase | How It's Used |
|
||||||
|
|---|---|---|
|
||||||
|
| GUI overlay | 8 | Centered layer-shell popup |
|
||||||
|
| Fuzzy filtering | 2 | Filter thousands of binaries |
|
||||||
|
| Frecency sorting | future | Boost frequently launched apps |
|
||||||
|
| on-select hook | 3 | Spawn or exec the selection |
|
||||||
|
| Structured I/O | 3 | .desktop metadata, icons |
|
||||||
|
| Fast startup | 1 | Must feel instant |
|
||||||
|
| Icon rendering | 8 | App icons in the list |
|
||||||
|
| Groups | 9 | Categories (dev, media, etc) |
|
||||||
|
|
||||||
|
## Stretch Goals
|
||||||
|
|
||||||
|
- **Frecency:** track launch counts per binary, sort
|
||||||
|
by frequency so your top apps float to the top.
|
||||||
|
This is the single biggest UX improvement over a
|
||||||
|
plain sorted list.
|
||||||
|
- **Categories:** group items by type. Dev tools,
|
||||||
|
browsers, media, system. Parsed from .desktop
|
||||||
|
Categories field or manually tagged.
|
||||||
|
- **Recent files:** a second section below apps
|
||||||
|
showing recently opened files. Needs a separate
|
||||||
|
data source (zeitgeist, custom tracker, etc).
|
||||||
|
- **Calculator / snippets:** if the query doesn't
|
||||||
|
match any app, treat it as a math expression or
|
||||||
|
snippet expansion. Scope creep, but it is what
|
||||||
|
makes launchers sticky.
|
||||||
|
|
||||||
|
## Platform Notes
|
||||||
|
|
||||||
|
- **Linux (Wayland):** layer-shell overlay. Global
|
||||||
|
hotkey via the compositor (Hyprland bind, Sway
|
||||||
|
bindsym). This is the primary target.
|
||||||
|
- **Linux (X11):** override-redirect window. Global
|
||||||
|
hotkey via xbindkeys or similar.
|
||||||
|
- **macOS:** no layer-shell. Needs a borderless
|
||||||
|
window with proper focus handling. Global hotkey
|
||||||
|
via skhd or a native Swift shim. Secondary target.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- How to handle apps that need a terminal (e.g.
|
||||||
|
htop, vim). Detect from .desktop Terminal=true and
|
||||||
|
wrap in the user's preferred terminal emulator?
|
||||||
|
- Should the launcher persist as a background process
|
||||||
|
for faster re-open, or cold-start each time?
|
||||||
|
Background process is snappier but uses memory.
|
||||||
|
- PATH scanning: rescan on every open, or cache with
|
||||||
|
inotify/FSEvents on PATH directories?
|
||||||
145
docs/use-cases/wallpaper-picker.md
Normal file
145
docs/use-cases/wallpaper-picker.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# Use Case: Wallpaper Picker
|
||||||
|
|
||||||
|
A keyboard-driven wallpaper picker for Hyprland that
|
||||||
|
previews wallpapers live on the desktop as you browse.
|
||||||
|
This is a first-class use case for pikl-menu: it
|
||||||
|
exercises structured I/O, lifecycle hooks, debouncing,
|
||||||
|
and the GUI overlay.
|
||||||
|
|
||||||
|
## What It Looks Like
|
||||||
|
|
||||||
|
1. User presses a keybind. pikl opens as a layer-shell
|
||||||
|
overlay on the focused monitor.
|
||||||
|
2. A list of wallpapers appears, filterable by typing.
|
||||||
|
3. Navigating the list applies each wallpaper live to
|
||||||
|
the desktop behind the overlay (via hyprpaper IPC).
|
||||||
|
4. Enter confirms. The wallpaper stays, overlay closes.
|
||||||
|
5. Escape cancels. The wallpaper reverts to whatever
|
||||||
|
was set before the picker opened.
|
||||||
|
|
||||||
|
The overlay is semi-transparent so the user sees the
|
||||||
|
real wallpaper behind it. No preview pane needed.
|
||||||
|
|
||||||
|
## How It Works With pikl
|
||||||
|
|
||||||
|
The wallpaper picker is not a standalone app. It is a
|
||||||
|
shell script (or a small wrapper) that pipes items into
|
||||||
|
pikl and uses hooks for the live preview behaviour.
|
||||||
|
|
||||||
|
### Basic Version
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# List wallpapers as JSON, pipe to pikl with hooks
|
||||||
|
find ~/Pictures/walls -type f \
|
||||||
|
-name '*.jpg' -o -name '*.png' -o -name '*.webp' \
|
||||||
|
| jq -Rc '{label: (. | split("/") | last),
|
||||||
|
sublabel: (. | split("/") | .[:-1]
|
||||||
|
| join("/")), meta: {path: .}}' \
|
||||||
|
| pikl --mode gui \
|
||||||
|
--on-open 'hyprctl hyprpaper listactive
|
||||||
|
| head -1 > /tmp/pikl-wall-orig' \
|
||||||
|
--on-hover 'jq -r .meta.path
|
||||||
|
| xargs -I{} hyprctl hyprpaper
|
||||||
|
wallpaper "DP-4, {}"' \
|
||||||
|
--on-cancel 'cat /tmp/pikl-wall-orig
|
||||||
|
| cut -d: -f2 | xargs -I{}
|
||||||
|
hyprctl hyprpaper wallpaper "DP-4, {}"' \
|
||||||
|
--on-hover-debounce 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### With a Manifest
|
||||||
|
|
||||||
|
For a reusable setup, use a pikl manifest file:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# ~/.config/pikl/wallpaper.toml
|
||||||
|
|
||||||
|
[hooks]
|
||||||
|
on-open = "save-current-wallpaper.sh"
|
||||||
|
on-hover = '''
|
||||||
|
jq -r .meta.path \
|
||||||
|
| xargs -I{} hyprctl hyprpaper \
|
||||||
|
wallpaper "DP-4, {}"
|
||||||
|
'''
|
||||||
|
on-cancel = "restore-wallpaper.sh"
|
||||||
|
on-hover-debounce = 100
|
||||||
|
|
||||||
|
[display]
|
||||||
|
format = "{label} <dim>{sublabel}</dim>"
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
find ~/Pictures/walls -type f \( \
|
||||||
|
-name '*.jpg' -o -name '*.png' \) \
|
||||||
|
| to-json-items \
|
||||||
|
| pikl --manifest ~/.config/pikl/wallpaper.toml \
|
||||||
|
--mode gui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modes
|
||||||
|
|
||||||
|
Different invocations cover different workflows:
|
||||||
|
|
||||||
|
**Browse:** open the picker, filter and select.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
find ~/Pictures/walls -type f ... | pikl ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**History:** pipe wallpaper history instead of a
|
||||||
|
directory scan. Same hooks, different input source.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cat ~/.local/state/wallpaper-history.jsonl \
|
||||||
|
| pikl --manifest ~/.config/pikl/wallpaper.toml \
|
||||||
|
--mode gui
|
||||||
|
```
|
||||||
|
|
||||||
|
**Random:** no pikl needed, just a shell one-liner.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
find ~/Pictures/walls -type f ... \
|
||||||
|
| shuf -n1 \
|
||||||
|
| xargs -I{} hyprctl hyprpaper wallpaper "DP-4, {}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restore:** read the last state file, apply it.
|
||||||
|
Also just a shell script, no pikl.
|
||||||
|
|
||||||
|
### Hyprland Keybindings
|
||||||
|
|
||||||
|
```
|
||||||
|
bind = $mod CTRL, B, exec, wallpicker browse
|
||||||
|
bind = $mod SHIFT, B, exec, wallpicker random
|
||||||
|
bind = $mod, B, exec, wallpicker copy
|
||||||
|
```
|
||||||
|
|
||||||
|
## What pikl Features This Exercises
|
||||||
|
|
||||||
|
| Feature | Phase | How It's Used |
|
||||||
|
|---|---|---|
|
||||||
|
| GUI overlay | 8 | Layer-shell on Wayland |
|
||||||
|
| Structured I/O | 3 | JSON items with metadata |
|
||||||
|
| on-hover hook | 3 | Live wallpaper preview |
|
||||||
|
| on-open hook | 3 | Save current wallpaper |
|
||||||
|
| on-cancel hook | 3 | Revert wallpaper |
|
||||||
|
| Hook debouncing | 3 | Don't flood hyprctl |
|
||||||
|
| Fuzzy filtering | 2 | Filter by filename |
|
||||||
|
| Manifest files | 3 | Reusable config |
|
||||||
|
| Watched sources | 7 | Live update on new files |
|
||||||
|
| Sessions | 6 | Remember last position |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Should pikl support image thumbnails in the item
|
||||||
|
list? Useful here but adds complexity. Could be a
|
||||||
|
GUI-only feature using the icon field.
|
||||||
|
- Debounce timing: 100ms feels right for hyprctl IPC.
|
||||||
|
Might need tuning for slower wallpaper daemons.
|
||||||
|
- Multi-monitor: the on-hover hook needs to know which
|
||||||
|
monitor to target. Could come from a CLI flag or
|
||||||
|
from hyprctl's focused monitor.
|
||||||
|
- History management: pikl sessions handle state, but
|
||||||
|
wallpaper history (which wallpapers were set when)
|
||||||
|
is the wrapper script's responsibility. Keep it
|
||||||
|
simple: append to a JSONL file.
|
||||||
Reference in New Issue
Block a user