feat: Add support IPC socket control over menu.

This commit is contained in:
2026-03-14 17:02:16 -04:00
parent cd1927fa9f
commit aad4c16d6e
15 changed files with 1501 additions and 185 deletions

View File

@@ -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)