doc: Update plans around hover actions and bidirectional hook communication.
Some checks failed
CI / Check (macos-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Clippy (strict) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
Some checks failed
CI / Check (macos-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Clippy (strict) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
183
docs/DESIGN.md
183
docs/DESIGN.md
@@ -66,58 +66,164 @@ naturally to table/column display mode.
|
|||||||
|
|
||||||
## Event Hooks
|
## Event Hooks
|
||||||
|
|
||||||
Hooks are shell commands that run in response to lifecycle
|
Lifecycle events fire as the user interacts with the menu.
|
||||||
events. They receive the relevant item(s) as JSON on stdin.
|
There are two ways to respond to them: **exec hooks** and
|
||||||
|
**handler hooks**.
|
||||||
|
|
||||||
### Available Hooks
|
### Lifecycle Events
|
||||||
|
|
||||||
| Hook | Fires When | Use Case |
|
| Event | Fires When | Use Case |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `on-open` | Menu opens | Save current state for revert |
|
| `on-open` | Menu opens | Save current state for revert |
|
||||||
| `on-close` | Menu closes (any reason) | Cleanup |
|
| `on-close` | Menu closes (any reason) | Cleanup |
|
||||||
| `on-hover` | Selection cursor moves to a new item | Live wallpaper preview |
|
| `on-hover` | Cursor moves to a new item | Live preview, prefetch |
|
||||||
| `on-select` | User confirms selection (Enter) | Apply the choice |
|
| `on-select` | User confirms (Enter) | Apply the choice |
|
||||||
| `on-cancel` | User cancels (Escape) | Revert preview |
|
| `on-cancel` | User cancels (Escape) | Revert preview |
|
||||||
| `on-filter` | Filter text changes | Dynamic item reloading |
|
| `on-filter` | Filter text changes | Dynamic item reloading |
|
||||||
| `on-mark` | User marks/unmarks an item | Visual feedback, register management |
|
| `on-mark` | User marks/unmarks an item | Visual feedback |
|
||||||
|
|
||||||
### Configuration
|
### Exec Hooks (fire-and-forget)
|
||||||
|
|
||||||
Hooks are set via CLI flags:
|
`--on-<event>-exec` spawns a subprocess for each event.
|
||||||
|
The item is piped as JSON on stdin. Stdout is discarded.
|
||||||
|
One subprocess per event, no state fed back.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pikl --on-hover 'hyprctl hyprpaper wallpaper "DP-4, {path}"' \
|
pikl --on-hover-exec 'notify-send "$(jq -r .label)"' \
|
||||||
--on-cancel 'hyprctl hyprpaper wallpaper "DP-4, {original}"' \
|
--on-select-exec 'apply-wallpaper.sh'
|
||||||
--on-hover-debounce 100
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Or via a manifest file for reusable configurations:
|
Good for simple side effects: notifications, applying a
|
||||||
|
setting, logging.
|
||||||
|
|
||||||
|
### Handler Hooks (bidirectional)
|
||||||
|
|
||||||
|
`--on-<event>` launches a **persistent process** that
|
||||||
|
receives events as JSON lines on stdin over the menu's
|
||||||
|
lifetime. The process can emit commands as JSON lines on
|
||||||
|
stdout to modify menu state.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pikl --on-hover './wallpaper-handler.sh' \
|
||||||
|
--on-filter './search-provider.sh'
|
||||||
|
```
|
||||||
|
|
||||||
|
The handler process stays alive. Each time the event fires,
|
||||||
|
a new JSON line is written to its stdin. The process reads
|
||||||
|
them in a loop:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
#!/bin/bash
|
||||||
|
while IFS= read -r event; do
|
||||||
|
label=$(echo "$event" | jq -r '.label')
|
||||||
|
set-wallpaper "$label"
|
||||||
|
# optionally emit commands back to pikl on stdout
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Stdout from the handler is parsed line-by-line as JSON
|
||||||
|
commands (see Handler Protocol below). Stderr passes
|
||||||
|
through to the terminal for debug output.
|
||||||
|
|
||||||
|
If the handler process exits unexpectedly, pikl logs a
|
||||||
|
warning via tracing and stops sending events. When the
|
||||||
|
menu closes, pikl closes the handler's stdin (breaking
|
||||||
|
the read loop naturally) and gives it a moment to exit
|
||||||
|
before killing it.
|
||||||
|
|
||||||
|
### Handler Protocol
|
||||||
|
|
||||||
|
Handler stdout commands, one JSON line per command:
|
||||||
|
|
||||||
|
| Action | Payload | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `add_items` | `{"items": [...]}` | Append items to the list |
|
||||||
|
| `replace_items` | `{"items": [...]}` | Replace all items, preserve cursor position if possible |
|
||||||
|
| `remove_items` | `{"indices": [0, 3]}` | Remove items by index |
|
||||||
|
| `set_filter` | `{"text": "query"}` | Change the filter text |
|
||||||
|
| `close` | (none) | Close the menu |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```jsonl
|
||||||
|
{"action": "add_items", "items": [{"label": "new result"}]}
|
||||||
|
{"action": "set_filter", "text": "updated query"}
|
||||||
|
{"action": "close"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Lines that don't parse as valid JSON or contain an unknown
|
||||||
|
action are logged as warnings (via tracing) and skipped.
|
||||||
|
Never fatal. A handler bug doesn't crash the menu.
|
||||||
|
|
||||||
|
For atomic multi-step mutations, use `replace_items` instead
|
||||||
|
of a `remove_items` + `add_items` pair. If a handler is
|
||||||
|
cancelled mid-stream (due to debounce), commands already
|
||||||
|
applied are not rolled back.
|
||||||
|
|
||||||
|
### Configuration via Manifest
|
||||||
|
|
||||||
|
Hooks can also be configured in a manifest file:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# ~/.config/pikl/wallpaper.toml
|
# ~/.config/pikl/wallpaper.toml
|
||||||
[hooks]
|
[hooks]
|
||||||
on-hover = 'hyprctl hyprpaper wallpaper "DP-4, {label}"'
|
on-hover = './wallpaper-handler.sh'
|
||||||
on-cancel = 'restore-wallpaper.sh'
|
on-select-exec = 'apply-wallpaper.sh'
|
||||||
on-hover-debounce = 100
|
on-cancel-exec = 'restore-wallpaper.sh'
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
columns = ["label", "meta.res"]
|
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
|
### Debouncing
|
||||||
|
|
||||||
All hooks support a debounce option
|
All hooks (exec and handler) support debouncing. Three
|
||||||
(`--on-hover-debounce 100`). When the user is scrolling
|
modes:
|
||||||
fast, only the last event in the debounce window fires.
|
|
||||||
Built-in, no external tooling needed.
|
| Mode | Behaviour | Default for |
|
||||||
|
|---|---|---|
|
||||||
|
| None | Fire immediately, every time | on-select, on-cancel, on-open, on-close |
|
||||||
|
| Debounce(ms) | Wait for quiet period, fire last event | on-filter (200ms) |
|
||||||
|
| Cancel-stale | New event cancels any in-flight invocation | (opt-in) |
|
||||||
|
|
||||||
|
Debounce and cancel-stale can combine: wait for quiet,
|
||||||
|
then fire, and if the previous invocation is still running,
|
||||||
|
cancel it first. This is the default for on-hover (200ms
|
||||||
|
debounce + cancel-stale).
|
||||||
|
|
||||||
|
CLI flags:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Set debounce duration
|
||||||
|
pikl --on-hover './preview.sh' --on-hover-debounce 200
|
||||||
|
|
||||||
|
# Disable debounce (fire every event)
|
||||||
|
pikl --on-hover './preview.sh' --on-hover-debounce 0
|
||||||
|
|
||||||
|
# Enable cancel-stale (for exec hooks, kills subprocess;
|
||||||
|
# for handler hooks, a cancelled event is not sent)
|
||||||
|
pikl --on-hover-exec 'slow-command' --on-hover-cancel-stale
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hook Architecture (Core vs CLI)
|
||||||
|
|
||||||
|
pikl-core defines the `HookHandler` trait and emits
|
||||||
|
lifecycle events. It does not know what handlers do with
|
||||||
|
them. The core manages debouncing and cancel-stale logic
|
||||||
|
since that interacts with the event loop timing.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[async_trait]
|
||||||
|
pub trait HookHandler: Send + Sync {
|
||||||
|
async fn handle(&self, event: HookEvent)
|
||||||
|
-> Result<Vec<HookResponse>, PiklError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The CLI binary provides `ShellHookHandler`, which maps
|
||||||
|
CLI flags to shell commands. Library consumers implement
|
||||||
|
their own handlers with whatever behaviour they want:
|
||||||
|
in-process closures, network calls, anything.
|
||||||
|
|
||||||
## Filtering
|
## Filtering
|
||||||
|
|
||||||
@@ -651,18 +757,26 @@ complexity:
|
|||||||
branching, no custom DSL. For shell one-liners,
|
branching, no custom DSL. For shell one-liners,
|
||||||
integration tests, and simple automation. If you need
|
integration tests, and simple automation. If you need
|
||||||
conditionals, you've outgrown this.
|
conditionals, you've outgrown this.
|
||||||
2. **IPC** (phase 6): bidirectional JSON over Unix socket.
|
2. **Exec hooks:** fire-and-forget shell commands triggered
|
||||||
|
by lifecycle events. Subprocess per event, stdout
|
||||||
|
discarded. For simple side effects.
|
||||||
|
3. **Handler hooks:** persistent bidirectional processes.
|
||||||
|
Receive events as JSON lines on stdin, emit commands on
|
||||||
|
stdout to modify menu state. The shell scripter's
|
||||||
|
extension point: anyone who can write a bash script can
|
||||||
|
extend pikl without touching Rust.
|
||||||
|
4. **IPC** (phase 6): bidirectional JSON over Unix socket.
|
||||||
External tools can read state and send actions while pikl
|
External tools can read state and send actions while pikl
|
||||||
runs interactively. Good for tool integration.
|
runs interactively. Good for tool integration.
|
||||||
3. **Lua** (post phase 6): embedded LuaJIT via mlua. Full
|
5. **Lua** (post phase 6): embedded LuaJIT via mlua. Full
|
||||||
stateful scripting: subscribe to events, branch on state,
|
stateful scripting: subscribe to events, branch on state,
|
||||||
loops, the works. The Lua runtime is just another
|
loops, the works. The Lua runtime is just another
|
||||||
frontend pushing Actions and subscribing to MenuEvents.
|
frontend pushing Actions and subscribing to MenuEvents.
|
||||||
For anything complex enough to need a real language.
|
For anything complex enough to need a real language.
|
||||||
|
|
||||||
No custom DSL. Action-fd stays simple forever. The jump from
|
No custom DSL. Action-fd stays simple forever. The jump
|
||||||
"I need conditionals" to "use Lua" is intentional: there's
|
from "I need conditionals" to "use Lua" is intentional:
|
||||||
no value in a half-language.
|
there's no value in a half-language.
|
||||||
|
|
||||||
## Use Cases
|
## Use Cases
|
||||||
|
|
||||||
@@ -688,5 +802,8 @@ with the full writeup.
|
|||||||
- Maximum practical item count before we need virtual
|
- Maximum practical item count before we need virtual
|
||||||
scrolling? (Probably around 100k)
|
scrolling? (Probably around 100k)
|
||||||
- Should hooks run in a pool or strictly sequential?
|
- Should hooks run in a pool or strictly sequential?
|
||||||
|
Resolved: exec hooks are one subprocess per event.
|
||||||
|
Handler hooks are persistent processes. Debounce and
|
||||||
|
cancel-stale manage concurrency.
|
||||||
- Plugin system via WASM for custom filter strategies?
|
- Plugin system via WASM for custom filter strategies?
|
||||||
(Probably way later if ever)
|
(Probably way later if ever)
|
||||||
|
|||||||
@@ -114,22 +114,61 @@ with vim muscle memory.
|
|||||||
|
|
||||||
## Phase 3: Structured I/O & Hooks
|
## Phase 3: Structured I/O & Hooks
|
||||||
|
|
||||||
The structured data pipeline.
|
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:**
|
**Deliverables:**
|
||||||
- JSON line input parsing (label, sublabel, meta, icon,
|
- Item model: sublabel, meta, icon, group as first-class
|
||||||
group)
|
optional fields
|
||||||
- JSON output with action context
|
- Output: separate struct with action context (action,
|
||||||
- Full hook lifecycle: on-open, on-close, on-hover,
|
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
|
on-select, on-cancel, on-filter
|
||||||
- Hook debouncing
|
- Debounce: three modes (none, debounce, cancel-stale),
|
||||||
- Bidirectional hooks (hook stdout modifies menu state)
|
per-hook CLI flags
|
||||||
- `--format` template strings for display
|
- Default debounce: on-hover 200ms + cancel-stale,
|
||||||
- Field filters (`meta.res:3840`)
|
on-filter 200ms, others none
|
||||||
- Filter scoping (`--filter-fields`)
|
- 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
|
**Done when:** The wallpaper picker use case works entirely
|
||||||
through hooks and structured I/O.
|
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
|
## Phase 4: Multi-Select & Registers
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user