From 3b574e89ab986033d164a2f3bc0be71c4b6cd2c4 Mon Sep 17 00:00:00 2001 From: "J. Champagne" Date: Sat, 14 Mar 2026 17:05:18 -0400 Subject: [PATCH] example: Add IPC remote control example. --- README.md | 27 ++++++- examples/demo.sh | 34 ++++++++ examples/ipc-remote.md | 78 ++++++++++++++++++ examples/ipc-remote.sh | 178 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 examples/ipc-remote.md create mode 100755 examples/ipc-remote.sh diff --git a/README.md b/README.md index 3a6cfb3..0df9dea 100644 --- a/README.md +++ b/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 diff --git a/examples/demo.sh b/examples/demo.sh index 643befb..409a079 100755 --- a/examples/demo.sh +++ b/examples/demo.sh @@ -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 diff --git a/examples/ipc-remote.md b/examples/ipc-remote.md new file mode 100644 index 0000000..ddafd40 --- /dev/null +++ b/examples/ipc-remote.md @@ -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 +``` diff --git a/examples/ipc-remote.sh b/examples/ipc-remote.sh new file mode 100755 index 0000000..82d04b8 --- /dev/null +++ b/examples/ipc-remote.sh @@ -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 " >&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 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