feat: Add support IPC socket control over menu.
This commit is contained in:
143
docs/DESIGN.md
143
docs/DESIGN.md
@@ -519,13 +519,19 @@ A few built-in themes ship with pikl. `--theme monokai` or
|
||||
|
||||
## Sessions
|
||||
|
||||
`--session name` persists the menu's state (filter text,
|
||||
scroll position, selections) across
|
||||
invocations. State lives in
|
||||
`~/.local/state/pikl/sessions/`.
|
||||
|
||||
`--session name` enables filter history across invocations.
|
||||
No `--session` = ephemeral, nothing saved.
|
||||
|
||||
When a session is active, every filter that leads to a
|
||||
selection (single, multi, or quicklist) is appended to
|
||||
`~/.local/state/pikl/sessions/{name}.history`. Empty
|
||||
filters and consecutive duplicates are skipped.
|
||||
|
||||
On startup with a session name, the history file is loaded.
|
||||
Ctrl+P / Ctrl+N in insert mode cycles through previous
|
||||
filters. Selecting a history entry replaces the current
|
||||
filter text.
|
||||
|
||||
Session names are just strings, use shell expansion for
|
||||
dynamic names:
|
||||
|
||||
@@ -534,8 +540,8 @@ pikl --session "walls-$(hostname)"
|
||||
pikl --session "logs-$USER"
|
||||
```
|
||||
|
||||
Session history is a log file alongside the state. Other
|
||||
tools can tail it for observability.
|
||||
Sessions are lightweight by design. No item persistence,
|
||||
no scroll position, no selections. Just filter recall.
|
||||
|
||||
## Watched Sources
|
||||
|
||||
@@ -552,18 +558,96 @@ named pipe.
|
||||
|
||||
## IPC
|
||||
|
||||
While running, pikl-menu listens on a Unix socket
|
||||
(`/run/user/$UID/pikl-$PID.sock`). External tools can:
|
||||
Opt-in external control via Unix socket. Enabled with
|
||||
`--ipc`. Off by default: pikl is ephemeral and shouldn't
|
||||
have side effects unless asked.
|
||||
|
||||
- Push new items
|
||||
- Remove items
|
||||
- Update item fields
|
||||
- Set the filter text
|
||||
- Read current selection
|
||||
- Close the menu
|
||||
Socket path:
|
||||
- Named session: `/run/user/$UID/pikl-{session}.sock`
|
||||
- No session: `/run/user/$UID/pikl-{pid}.sock`
|
||||
|
||||
Protocol is newline-delimited JSON. Simple enough to use
|
||||
with `socat` or any language's Unix socket support.
|
||||
The socket path is logged via tracing on startup and
|
||||
cleaned up on exit.
|
||||
|
||||
### Protocol
|
||||
|
||||
Newline-delimited JSON, one message per line. Simple
|
||||
enough for `socat` or any language with Unix sockets.
|
||||
|
||||
### Client-to-pikl Commands
|
||||
|
||||
**Write commands** (fire-and-forget, no response):
|
||||
|
||||
| Action | Payload | Effect |
|
||||
|---|---|---|
|
||||
| `add_items` | `{"items": [...]}` | Append items |
|
||||
| `replace_items` | `{"items": [...]}` | Replace all items |
|
||||
| `remove_items` | `{"indices": [...]}` | Remove by index |
|
||||
| `set_filter` | `{"text": "..."}` | Set filter text |
|
||||
| `move_up` | (none) | Navigate up |
|
||||
| `move_down` | (none) | Navigate down |
|
||||
| `move_to_top` | (none) | Jump to top |
|
||||
| `move_to_bottom` | (none) | Jump to bottom |
|
||||
| `page_up` | (none) | Page up |
|
||||
| `page_down` | (none) | Page down |
|
||||
| `toggle_select` | (none) | Toggle current item |
|
||||
| `select_all` | (none) | Select all items |
|
||||
| `clear_selections` | (none) | Clear selections |
|
||||
| `confirm` | (none) | Confirm selection |
|
||||
| `cancel` | (none) | Cancel menu |
|
||||
| `close` | (none) | Close menu |
|
||||
|
||||
**Read commands** (require `id` field, response echoes it):
|
||||
|
||||
| Action | Response |
|
||||
|---|---|
|
||||
| `get_state` | `{"id": "...", "state": {"filter": "...", "cursor": 3, "total_items": 150, "total_filtered": 12, "selection_count": 0, "mode": "insert"}}` |
|
||||
| `get_selection` | `{"id": "...", "selection": [{"label": "...", "index": 3}, ...]}` |
|
||||
|
||||
**Event subscription:**
|
||||
|
||||
| Action | Payload | Effect |
|
||||
|---|---|---|
|
||||
| `subscribe` | `{"events": ["hover", "filter", ...]}` | Start receiving events |
|
||||
| `unsubscribe` | (none) | Stop receiving events |
|
||||
|
||||
Subscribed events are pushed as
|
||||
`{"event": "hover", "item": {...}, "index": 3}` lines.
|
||||
|
||||
### Multiple Connections
|
||||
|
||||
Multiple clients can connect simultaneously. Each gets
|
||||
independent subscription state. All writes are serialized
|
||||
through the same action channel.
|
||||
|
||||
### Auth
|
||||
|
||||
Unix socket permissions (user-only access). No additional
|
||||
authentication.
|
||||
|
||||
### Architecture
|
||||
|
||||
IPC is just another frontend. It deserializes JSON
|
||||
messages into `Action` variants and sends them through the
|
||||
same `mpsc` channel as the TUI or action-fd. Event
|
||||
subscriptions tap into the `broadcast` channel. No special
|
||||
core changes needed.
|
||||
|
||||
### Examples
|
||||
|
||||
```sh
|
||||
# Push items into a running pikl
|
||||
echo '{"action": "add_items", "items": [{"label": "new"}]}' \
|
||||
| socat - UNIX-CONNECT:/run/user/1000/pikl-mypicker.sock
|
||||
|
||||
# Read current state
|
||||
echo '{"action": "get_state", "id": "1"}' \
|
||||
| socat - UNIX-CONNECT:/run/user/1000/pikl-mypicker.sock
|
||||
|
||||
# Subscribe to hover events
|
||||
echo '{"action": "subscribe", "events": ["hover"]}' \
|
||||
| socat - UNIX-CONNECT:/run/user/1000/pikl-mypicker.sock
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
@@ -644,8 +728,8 @@ sequentially regardless of where they came from.
|
||||
| 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 |
|
||||
| IPC | Unix socket, newline-delimited JSON | Yes (bidirectional) | 6 |
|
||||
| Lua | Embedded LuaJIT via mlua (in-process) | Yes (stateful, conditional) | Post-6 |
|
||||
|
||||
This framing means new interaction modes don't require core
|
||||
changes: they're just new frontends that push actions.
|
||||
@@ -821,12 +905,15 @@ complexity:
|
||||
extend pikl without touching Rust.
|
||||
4. **IPC** (phase 6): bidirectional JSON over Unix socket.
|
||||
External tools can read state and send actions while pikl
|
||||
runs interactively. Good for tool integration.
|
||||
5. **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.
|
||||
runs interactively. Language-agnostic: anything that can
|
||||
write JSON to a socket works.
|
||||
5. **Lua** (post phase 6): embedded LuaJIT via mlua.
|
||||
In-process scripting for complex hooks and behaviour.
|
||||
Direct access to the action/event system, no socket
|
||||
overhead. Think Neovim's Lua layer: you're extending the
|
||||
tool, not talking to it from outside. IPC and Lua are
|
||||
separate integration points. IPC is for external
|
||||
processes. Lua is for embedded logic.
|
||||
|
||||
No custom DSL. Action-fd stays simple forever. The jump
|
||||
from "I need conditionals" to "use Lua" is intentional:
|
||||
@@ -848,16 +935,10 @@ with the full writeup.
|
||||
|
||||
## 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?
|
||||
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?
|
||||
(Probably way later if ever)
|
||||
|
||||
@@ -212,22 +212,68 @@ CSV/TSV input parsing and columnar table rendering.
|
||||
**Done when:** `echo "name,age\nalice,30\nbob,25" | pikl --input-format csv`
|
||||
renders a navigable table with headers.
|
||||
|
||||
## Phase 6: Sessions & IPC
|
||||
## Phase 6: IPC & Session History
|
||||
|
||||
Persistence and external control.
|
||||
Two independent features bundled together. IPC adds live
|
||||
external control over a Unix socket. Session history gives
|
||||
lightweight filter recall for repeated workflows.
|
||||
|
||||
### IPC (External Control)
|
||||
|
||||
Opt-in Unix socket server for live control of a running
|
||||
pikl instance by external processes.
|
||||
|
||||
**Deliverables:**
|
||||
- `--session name` for state persistence
|
||||
- Session state: filter, scroll position, selections
|
||||
- Session history log file
|
||||
- Unix socket IPC while running
|
||||
- IPC commands: push/remove/update items, set filter,
|
||||
read selection, close
|
||||
- Protocol: newline-delimited JSON
|
||||
- `--ipc` flag to enable the socket listener (off by
|
||||
default, pikl is ephemeral and shouldn't have side
|
||||
effects unless asked)
|
||||
- Socket path: `/run/user/$UID/pikl-{session}.sock`
|
||||
(named session) or `/run/user/$UID/pikl-{pid}.sock`
|
||||
(no session name)
|
||||
- Socket path logged via tracing on startup
|
||||
- Cleanup on exit (normal, cancel, SIGTERM)
|
||||
- Protocol: newline-delimited JSON, one message per line
|
||||
- Write commands: `add_items`, `replace_items`,
|
||||
`remove_items`, `set_filter`, `confirm`, `cancel`,
|
||||
`close`, plus navigation actions (`move_up`,
|
||||
`move_down`, `move_to_top`, `move_to_bottom`,
|
||||
`page_up`, `page_down`, `toggle_select`, `select_all`,
|
||||
`clear_selections`)
|
||||
- Read commands: `get_state` (filter, cursor, counts,
|
||||
mode), `get_selection` (selected items). Both require
|
||||
an `id` field, response echoes it back.
|
||||
- Event subscription: `subscribe` with event type list,
|
||||
`unsubscribe` to stop. Subscribed events pushed as
|
||||
`{"event": "...", ...}` lines.
|
||||
- Multiple simultaneous client connections
|
||||
- Auth: Unix socket permissions (user-only)
|
||||
- IPC is just another frontend: deserializes JSON into
|
||||
Actions, optionally subscribes to MenuEvent broadcast
|
||||
|
||||
**Done when:** You can close and reopen a session and find
|
||||
your state intact. External scripts can push items into a
|
||||
running pikl instance.
|
||||
**Not in scope:**
|
||||
- `get_items` (full item list read, potentially large)
|
||||
- Auto-enable with `--session`
|
||||
- Auth beyond Unix socket permissions
|
||||
|
||||
### Session Filter History
|
||||
|
||||
**Deliverables:**
|
||||
- `--session name` flag
|
||||
- On any confirm (single, multi, quicklist), append
|
||||
current filter text to
|
||||
`~/.local/state/pikl/sessions/{name}.history`
|
||||
- Skip empty filters and consecutive duplicates
|
||||
- Load history on startup if session file exists
|
||||
- Ctrl+P / Ctrl+N in insert mode to cycle through
|
||||
filter history
|
||||
- Selecting a history entry replaces the current filter
|
||||
text
|
||||
- Per-session only, no global history
|
||||
|
||||
**Done when:** An external script can connect to a running
|
||||
pikl instance over the socket, push items, read state, and
|
||||
subscribe to events. A named session remembers filter
|
||||
history across invocations.
|
||||
|
||||
## Phase 7: Streaming & Watched Sources
|
||||
|
||||
|
||||
Reference in New Issue
Block a user