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
|
## 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
|
- Fuzzy, exact, regex, and inverse filtering with `|` pipeline chaining
|
||||||
- Vim navigation (j/k, gg/G, Ctrl+D/U, Ctrl+F/B, modes)
|
- Vim navigation (j/k, gg/G, Ctrl+D/U, Ctrl+F/B, modes)
|
||||||
- Structured JSON I/O with sublabel, icon, group, and arbitrary metadata
|
- 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}'`)
|
- Format templates (`--format '{label} - {sublabel}'`)
|
||||||
- Field-scoped filtering (`--filter-fields`, `meta.res:3840` syntax)
|
- Field-scoped filtering (`--filter-fields`, `meta.res:3840` syntax)
|
||||||
- Headless scripting via `--action-fd`
|
- 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
|
## Platform Support
|
||||||
|
|
||||||
|
|||||||
@@ -379,6 +379,35 @@ Gentoo,rolling,openrc
|
|||||||
CSV
|
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 ─────────────────────────────────────────
|
# ── Scenario menu ─────────────────────────────────────────
|
||||||
|
|
||||||
scenarios=(
|
scenarios=(
|
||||||
@@ -411,6 +440,9 @@ scenarios=(
|
|||||||
"CSV + multi-select"
|
"CSV + multi-select"
|
||||||
"CSV + field filter"
|
"CSV + field filter"
|
||||||
"---"
|
"---"
|
||||||
|
"IPC remote control"
|
||||||
|
"Session filter history"
|
||||||
|
"---"
|
||||||
"on-select-exec hook (legacy)"
|
"on-select-exec hook (legacy)"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -441,6 +473,8 @@ run_scenario() {
|
|||||||
*"CSV with --columns"*) csv_columns_alias ;;
|
*"CSV with --columns"*) csv_columns_alias ;;
|
||||||
*"CSV + multi"*) csv_multi_select ;;
|
*"CSV + multi"*) csv_multi_select ;;
|
||||||
*"CSV + field"*) csv_filter ;;
|
*"CSV + field"*) csv_filter ;;
|
||||||
|
*"IPC remote"*) ipc_demo ;;
|
||||||
|
*"Session filter"*) session_demo ;;
|
||||||
*"on-select-exec"*) on_select_hook ;;
|
*"on-select-exec"*) on_select_hook ;;
|
||||||
"---")
|
"---")
|
||||||
echo "that's a separator, not a scenario" >&2
|
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