example: Add IPC remote control example.
This commit is contained in:
27
README.md
27
README.md
@@ -131,7 +131,7 @@ pins the version and pulls in rust-analyzer + clippy.
|
||||
|
||||
## Current Status
|
||||
|
||||
Phases 1 through 3 are complete. pikl works as a TUI menu with:
|
||||
Phases 1 through 6 are complete. pikl works as a TUI menu with:
|
||||
- Fuzzy, exact, regex, and inverse filtering with `|` pipeline chaining
|
||||
- Vim navigation (j/k, gg/G, Ctrl+D/U, Ctrl+F/B, modes)
|
||||
- Structured JSON I/O with sublabel, icon, group, and arbitrary metadata
|
||||
@@ -140,6 +140,31 @@ Phases 1 through 3 are complete. pikl works as a TUI menu with:
|
||||
- Format templates (`--format '{label} - {sublabel}'`)
|
||||
- Field-scoped filtering (`--filter-fields`, `meta.res:3840` syntax)
|
||||
- Headless scripting via `--action-fd`
|
||||
- Multi-select with undo/redo and visual line mode
|
||||
- Table mode for CSV/TSV input with auto-generated or custom columns
|
||||
- IPC control via Unix socket (`--ipc`, `--session`)
|
||||
- Session filter history with Ctrl+P/Ctrl+N recall
|
||||
|
||||
### IPC example
|
||||
|
||||
Run pikl with IPC in one terminal, control it from another:
|
||||
|
||||
```sh
|
||||
# terminal 1: start pikl with IPC enabled
|
||||
echo -e "alpha\nbeta\ngamma\ndelta" | pikl --ipc --session demo
|
||||
|
||||
# terminal 2: query state over the socket
|
||||
echo '{"action":"get_state","id":"1"}' \
|
||||
| socat - UNIX-CONNECT:/run/user/$(id -u)/pikl-demo.sock
|
||||
|
||||
# terminal 2: add items dynamically
|
||||
echo '{"action":"add_items","items":["epsilon","zeta"]}' \
|
||||
| socat -t 0.1 - UNIX-CONNECT:/run/user/$(id -u)/pikl-demo.sock
|
||||
```
|
||||
|
||||
There's an interactive remote control demo at
|
||||
[examples/ipc-remote.sh](examples/ipc-remote.sh)
|
||||
(see [examples/ipc-remote.md](examples/ipc-remote.md) for the walkthrough).
|
||||
|
||||
## Platform Support
|
||||
|
||||
|
||||
@@ -379,6 +379,35 @@ Gentoo,rolling,openrc
|
||||
CSV
|
||||
}
|
||||
|
||||
# ── Phase 6 demos ────────────────────────────────────────
|
||||
|
||||
ipc_demo() {
|
||||
echo "IPC demo: pikl runs here with --ipc enabled." >&2
|
||||
echo "Open a second terminal and run:" >&2
|
||||
echo " ./examples/ipc-remote.sh demo" >&2
|
||||
echo "" >&2
|
||||
echo "Use the remote to navigate, filter, add items, and query state." >&2
|
||||
echo "Press Enter or q in the remote to exit." >&2
|
||||
echo "" >&2
|
||||
cat <<'ITEMS' | pikl --ipc --session demo --multi
|
||||
{"label": "Arch Linux", "category": "rolling", "init": "systemd"}
|
||||
{"label": "NixOS", "category": "rolling", "init": "systemd"}
|
||||
{"label": "Void Linux", "category": "rolling", "init": "runit"}
|
||||
{"label": "Debian", "category": "stable", "init": "systemd"}
|
||||
{"label": "Alpine", "category": "stable", "init": "openrc"}
|
||||
{"label": "Fedora", "category": "semi-rolling", "init": "systemd"}
|
||||
{"label": "Gentoo", "category": "rolling", "init": "openrc"}
|
||||
ITEMS
|
||||
}
|
||||
|
||||
session_demo() {
|
||||
echo "Session filter history: select with a filter, exit, relaunch." >&2
|
||||
echo "Press Ctrl+P to recall your previous filter." >&2
|
||||
echo "" >&2
|
||||
printf "apple\nbanana\ncherry\ndate\nelderberry\nfig\ngrape\nhoneydew\n" \
|
||||
| pikl --session fruit
|
||||
}
|
||||
|
||||
# ── Scenario menu ─────────────────────────────────────────
|
||||
|
||||
scenarios=(
|
||||
@@ -411,6 +440,9 @@ scenarios=(
|
||||
"CSV + multi-select"
|
||||
"CSV + field filter"
|
||||
"---"
|
||||
"IPC remote control"
|
||||
"Session filter history"
|
||||
"---"
|
||||
"on-select-exec hook (legacy)"
|
||||
)
|
||||
|
||||
@@ -441,6 +473,8 @@ run_scenario() {
|
||||
*"CSV with --columns"*) csv_columns_alias ;;
|
||||
*"CSV + multi"*) csv_multi_select ;;
|
||||
*"CSV + field"*) csv_filter ;;
|
||||
*"IPC remote"*) ipc_demo ;;
|
||||
*"Session filter"*) session_demo ;;
|
||||
*"on-select-exec"*) on_select_hook ;;
|
||||
"---")
|
||||
echo "that's a separator, not a scenario" >&2
|
||||
|
||||
78
examples/ipc-remote.md
Normal file
78
examples/ipc-remote.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# IPC Remote Control
|
||||
|
||||
Interactive demo of pikl's IPC socket. Run pikl in one terminal, control it
|
||||
from another. Watch the menu respond in real time.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `socat` for socket I/O (`pacman -S socat` / `brew install socat`)
|
||||
- `python3` for pretty-printing JSON responses (optional, falls back to raw output)
|
||||
|
||||
## Quick start
|
||||
|
||||
**Terminal 1:** start pikl with IPC enabled.
|
||||
|
||||
```sh
|
||||
echo -e "alpha\nbeta\ngamma\ndelta\nepsilon" | pikl --ipc --session demo
|
||||
```
|
||||
|
||||
**Terminal 2:** connect the remote control.
|
||||
|
||||
```sh
|
||||
./examples/ipc-remote.sh demo
|
||||
```
|
||||
|
||||
## What you can do
|
||||
|
||||
The remote gives you single-key commands:
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `j` / `k` | Move cursor down / up |
|
||||
| `g` / `G` | Jump to top / bottom |
|
||||
| `f` | Set filter text (then type the query) |
|
||||
| `F` | Clear filter |
|
||||
| `Space` | Toggle select on current item |
|
||||
| `a` | Select all |
|
||||
| `c` | Clear selections |
|
||||
| `s` | Print current menu state (JSON) |
|
||||
| `S` | Print current selection (JSON) |
|
||||
| `+` | Add new items interactively |
|
||||
| `r` | Replace all items |
|
||||
| `Enter` | Confirm selection (exits pikl) |
|
||||
| `q` | Cancel (exits pikl) |
|
||||
| `x` | Exit remote without affecting pikl |
|
||||
|
||||
## Things to try
|
||||
|
||||
1. Press `j` a few times and watch the cursor move in the pikl terminal.
|
||||
2. Press `s` to see the state, including cursor position and item count.
|
||||
3. Press `f`, type `al`, and watch the filter narrow the list.
|
||||
4. Press `F` to clear the filter.
|
||||
5. Press `+` and add a few items. They appear in the pikl terminal immediately.
|
||||
6. Press `Space` a couple times to toggle selections, then `S` to see what's selected.
|
||||
7. Press `Enter` to confirm. pikl exits and prints the selection.
|
||||
|
||||
## How it works
|
||||
|
||||
The remote sends newline-delimited JSON commands to pikl's Unix socket using
|
||||
`socat`. Write commands (move, filter, add items) are fire-and-forget. Read
|
||||
commands (get_state, get_selection) wait for a JSON response.
|
||||
|
||||
The socket path for a named session is `/run/user/$UID/pikl-{session}.sock`.
|
||||
Without `--session`, pikl uses the PID: `/run/user/$UID/pikl-{pid}.sock`.
|
||||
|
||||
## Using socat directly
|
||||
|
||||
You don't need the remote script. Any tool that speaks Unix sockets works:
|
||||
|
||||
```sh
|
||||
# Query state
|
||||
echo '{"action":"get_state","id":"1"}' | socat - UNIX-CONNECT:/run/user/$(id -u)/pikl-demo.sock
|
||||
|
||||
# Move cursor down
|
||||
echo '{"action":"move_down"}' | socat -t 0.1 - UNIX-CONNECT:/run/user/$(id -u)/pikl-demo.sock
|
||||
|
||||
# Add items
|
||||
echo '{"action":"add_items","items":["foo","bar"]}' | socat -t 0.1 - UNIX-CONNECT:/run/user/$(id -u)/pikl-demo.sock
|
||||
```
|
||||
178
examples/ipc-remote.sh
Executable file
178
examples/ipc-remote.sh
Executable file
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env bash
|
||||
# IPC remote control for pikl-menu.
|
||||
#
|
||||
# Connects to a running pikl instance over its Unix socket and
|
||||
# lets you send commands interactively. You see the effects in
|
||||
# the pikl terminal in real time.
|
||||
#
|
||||
# Usage:
|
||||
# Terminal 1: echo -e "alpha\nbeta\ngamma\ndelta" | pikl --ipc --session demo
|
||||
# Terminal 2: ./examples/ipc-remote.sh demo
|
||||
#
|
||||
# Requires: socat (for socket I/O)
|
||||
#
|
||||
# The session name is optional. Without it, you'll need to pass
|
||||
# the full socket path as the first argument.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Socket path ──────────────────────────────────────────
|
||||
|
||||
resolve_socket() {
|
||||
local arg="${1:-}"
|
||||
if [[ -z "$arg" ]]; then
|
||||
echo "usage: ipc-remote.sh <session-name | socket-path>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# If it looks like a path, use it directly
|
||||
if [[ "$arg" == */* || "$arg" == *.sock ]]; then
|
||||
echo "$arg"
|
||||
return
|
||||
fi
|
||||
|
||||
# Otherwise treat it as a session name
|
||||
local uid
|
||||
uid=$(id -u)
|
||||
echo "/run/user/${uid}/pikl-${arg}.sock"
|
||||
}
|
||||
|
||||
SOCK=$(resolve_socket "${1:-}")
|
||||
|
||||
if [[ ! -S "$SOCK" ]]; then
|
||||
echo "socket not found: $SOCK" >&2
|
||||
echo "" >&2
|
||||
echo "make sure pikl is running with --ipc:" >&2
|
||||
echo " echo -e 'alpha\nbeta\ngamma' | pikl --ipc --session demo" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v socat >/dev/null 2>&1; then
|
||||
echo "socat is required but not installed." >&2
|
||||
echo "install it with your package manager (pacman -S socat, brew install socat, etc.)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── IPC helpers ──────────────────────────────────────────
|
||||
|
||||
ipc_send() {
|
||||
echo "$1" | socat - "UNIX-CONNECT:${SOCK}" 2>/dev/null
|
||||
}
|
||||
|
||||
ipc_fire() {
|
||||
# Fire-and-forget: send command, don't wait for response
|
||||
echo "$1" | socat -t 0.1 - "UNIX-CONNECT:${SOCK}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ── Commands ─────────────────────────────────────────────
|
||||
|
||||
cmd_state() {
|
||||
local resp
|
||||
resp=$(ipc_send '{"action":"get_state","id":"q"}')
|
||||
echo "$resp" | python3 -m json.tool 2>/dev/null || echo "$resp"
|
||||
}
|
||||
|
||||
cmd_selection() {
|
||||
local resp
|
||||
resp=$(ipc_send '{"action":"get_selection","id":"q"}')
|
||||
echo "$resp" | python3 -m json.tool 2>/dev/null || echo "$resp"
|
||||
}
|
||||
|
||||
cmd_move_down() { ipc_fire '{"action":"move_down"}'; }
|
||||
cmd_move_up() { ipc_fire '{"action":"move_up"}'; }
|
||||
cmd_top() { ipc_fire '{"action":"move_to_top"}'; }
|
||||
cmd_bottom() { ipc_fire '{"action":"move_to_bottom"}'; }
|
||||
cmd_page_down() { ipc_fire '{"action":"page_down"}'; }
|
||||
cmd_page_up() { ipc_fire '{"action":"page_up"}'; }
|
||||
cmd_toggle() { ipc_fire '{"action":"toggle_select"}'; }
|
||||
cmd_select_all() { ipc_fire '{"action":"select_all"}'; }
|
||||
cmd_clear() { ipc_fire '{"action":"clear_selections"}'; }
|
||||
cmd_confirm() { ipc_fire '{"action":"confirm"}'; }
|
||||
cmd_cancel() { ipc_fire '{"action":"cancel"}'; }
|
||||
|
||||
cmd_filter() {
|
||||
local text="${1:-}"
|
||||
if [[ -z "$text" ]]; then
|
||||
read -rp "filter text: " text
|
||||
fi
|
||||
ipc_fire "{\"action\":\"set_filter\",\"text\":\"${text}\"}"
|
||||
}
|
||||
|
||||
cmd_add() {
|
||||
local items="${1:-}"
|
||||
if [[ -z "$items" ]]; then
|
||||
echo "enter items to add (one per line, empty line to finish):"
|
||||
local lines=()
|
||||
while IFS= read -rp "> " line; do
|
||||
[[ -z "$line" ]] && break
|
||||
lines+=("\"${line}\"")
|
||||
done
|
||||
items=$(IFS=,; echo "${lines[*]}")
|
||||
fi
|
||||
ipc_fire "{\"action\":\"add_items\",\"items\":[${items}]}"
|
||||
}
|
||||
|
||||
cmd_replace() {
|
||||
echo "enter new items (one per line, empty line to finish):"
|
||||
local lines=()
|
||||
while IFS= read -rp "> " line; do
|
||||
[[ -z "$line" ]] && break
|
||||
lines+=("\"${line}\"")
|
||||
done
|
||||
local items
|
||||
items=$(IFS=,; echo "${lines[*]}")
|
||||
ipc_fire "{\"action\":\"replace_items\",\"items\":[${items}]}"
|
||||
}
|
||||
|
||||
# ── Interactive loop ─────────────────────────────────────
|
||||
|
||||
show_help() {
|
||||
cat <<'HELP'
|
||||
|
||||
pikl IPC remote control
|
||||
───────────────────────
|
||||
j / down move cursor down k / up move cursor up
|
||||
g jump to top G jump to bottom
|
||||
f <text> set filter F clear filter
|
||||
space toggle select a select all
|
||||
c clear selections s get state
|
||||
S get selection
|
||||
+ add items r replace all items
|
||||
enter confirm selection q cancel menu
|
||||
? show this help x exit remote
|
||||
|
||||
HELP
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "connected to: $SOCK"
|
||||
show_help
|
||||
|
||||
while true; do
|
||||
read -rp "ipc> " -n1 key || break
|
||||
echo ""
|
||||
|
||||
case "$key" in
|
||||
j) cmd_move_down ;;
|
||||
k) cmd_move_up ;;
|
||||
g) cmd_top ;;
|
||||
G) cmd_bottom ;;
|
||||
f) read -rp "filter: " text; cmd_filter "$text" ;;
|
||||
F) cmd_filter "" ;;
|
||||
" ") cmd_toggle ;;
|
||||
a) cmd_select_all ;;
|
||||
c) cmd_clear ;;
|
||||
s) cmd_state ;;
|
||||
S) cmd_selection ;;
|
||||
+) cmd_add ;;
|
||||
r) cmd_replace ;;
|
||||
"") cmd_confirm; echo "confirmed."; break ;;
|
||||
q) cmd_cancel; echo "cancelled."; break ;;
|
||||
x) echo "bye."; break ;;
|
||||
"?") show_help ;;
|
||||
*) echo "unknown key '$key'. press ? for help." ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
main
|
||||
Reference in New Issue
Block a user