Item Model Expansion - Item now caches sublabel, icon, group with accessors. Added resolve_field_path() for dotted path traversal and field_value() on Item.
Output Struct - New OutputItem with OutputAction (select/cancel) and index. Object values flatten, strings get a value field. MenuResult::Selected now carries { value, index }.
Hook Types - Replaced the old Hook trait with HookEvent (serializable, 6 variants), HookResponse (deserializable, 5 commands), HookHandler trait (sync for dyn-compatibility), and parse_hook_response() with tracing warnings.
New Actions & Menu Methods - Added ReplaceItems, RemoveItems, ProcessHookResponse, CloseMenu actions. Menu trait gained original_index(), replace_all(), remove_by_indices(), formatted_label(). Pipeline got rebuild() and rebuild_with_values(). Smart cursor preservation on replace.
Lifecycle Events - MenuRunner emits Open, Close, Hover, Select, Cancel, Filter events through the dispatcher. Cursor tracking for Hover detection.
Debounce - DebouncedDispatcher with 4 modes: None, Debounce, CancelStale, DebounceAndCancelStale. Defaults: hover=DebounceAndCancelStale(200ms), filter=Debounce(200ms).
Exec Hooks - ShellExecHandler maps --on-{open,close,hover,select,cancel,filter}-exec flags to fire-and-forget subprocesses. Event JSON piped to stdin.
Handler Hooks - ShellHandlerHook launches persistent processes per --on-{event} flag. Bidirectional JSON lines: events on stdin, responses on stdout flowing back through Action::ProcessHookResponse. CompositeHookHandler dispatches to both.
--filter-fields - --filter-fields label,sublabel,meta.tags searches multiple fields. Combined text for fuzzy, individual for exact/regex.
--format - FormatTemplate parses {field.path} placeholders. --format '{label} - {sublabel}' controls display. TUI renders formatted_text when available.
Field Filters - meta.res:3840 in query syntax matches specific fields. !meta.res:3840 for inverse. Pipeline stores item Values for field resolution. Requires dotted path (single word colons stay fuzzy).
361 lines
12 KiB
Bash
Executable File
361 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Interactive demo launcher for pikl-menu.
|
|
# Uses pikl to pick a scenario, then runs that scenario in pikl.
|
|
#
|
|
# Usage: ./examples/demo.sh
|
|
|
|
set -euo pipefail
|
|
|
|
# Resolve the pikl binary once up front.
|
|
if [[ -n "${PIKL:-}" ]]; then
|
|
PIKL_BIN="$PIKL"
|
|
elif command -v pikl >/dev/null 2>&1; then
|
|
PIKL_BIN="pikl"
|
|
else
|
|
# Build quietly, use the debug binary directly.
|
|
cargo build --quiet 2>&1
|
|
PIKL_BIN="cargo run --quiet --"
|
|
fi
|
|
|
|
# Wrapper so scenarios can just call `pikl` with args.
|
|
pikl() {
|
|
$PIKL_BIN "$@"
|
|
}
|
|
|
|
# ── Scenario runners ──────────────────────────────────────
|
|
|
|
plain_list() {
|
|
printf "apple\nbanana\ncherry\ndate\nelderberry\nfig\ngrape\nhoneydew\n" \
|
|
| pikl
|
|
}
|
|
|
|
big_list() {
|
|
# seq output is wrapped as JSON strings so they get
|
|
# proper labels (bare numbers parse as JSON numbers
|
|
# with empty display text).
|
|
seq 1 500 | sed 's/.*/"&"/' | pikl
|
|
}
|
|
|
|
json_objects() {
|
|
cat <<'ITEMS' | pikl
|
|
{"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
|
|
}
|
|
|
|
custom_label_key() {
|
|
cat <<'ITEMS' | pikl --label-key name
|
|
{"name": "Neovim", "type": "editor", "lang": "C/Lua"}
|
|
{"name": "Helix", "type": "editor", "lang": "Rust"}
|
|
{"name": "Kakoune", "type": "editor", "lang": "C++"}
|
|
{"name": "Emacs", "type": "editor", "lang": "Lisp"}
|
|
{"name": "Vim", "type": "editor", "lang": "C"}
|
|
ITEMS
|
|
}
|
|
|
|
git_branches() {
|
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
echo "not in a git repo" >&2
|
|
return 1
|
|
fi
|
|
git branch --format='%(refname:short)' | pikl
|
|
}
|
|
|
|
git_log_picker() {
|
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
echo "not in a git repo" >&2
|
|
return 1
|
|
fi
|
|
git log --oneline -30 | pikl
|
|
}
|
|
|
|
file_picker() {
|
|
find . -maxdepth 3 -type f \
|
|
-not -path './.git/*' \
|
|
-not -path './target/*' \
|
|
-not -name '*.lock' \
|
|
| sort \
|
|
| pikl
|
|
}
|
|
|
|
on_select_hook() {
|
|
printf "one\ntwo\nthree\nfour\nfive\n" \
|
|
| pikl --on-select-exec 'echo "you picked: $(cat)" >&2'
|
|
}
|
|
|
|
mixed_input() {
|
|
cat <<'ITEMS' | pikl
|
|
just a plain string
|
|
{"label": "a json object", "extra": 42}
|
|
another plain string
|
|
{"label": "second object", "extra": 99}
|
|
ITEMS
|
|
}
|
|
|
|
# ── Phase 3 demos ────────────────────────────────────────
|
|
|
|
structured_items() {
|
|
echo "Items have sublabel, icon, and metadata fields." >&2
|
|
echo "The output JSON includes action and index context." >&2
|
|
echo "" >&2
|
|
cat <<'ITEMS' | pikl
|
|
{"label": "Firefox", "sublabel": "Web Browser", "icon": "firefox", "meta": {"version": "125", "type": "browser"}}
|
|
{"label": "Neovim", "sublabel": "Text Editor", "icon": "nvim", "meta": {"version": "0.10", "type": "editor"}}
|
|
{"label": "Alacritty", "sublabel": "Terminal Emulator", "icon": "alacritty", "meta": {"version": "0.13", "type": "terminal"}}
|
|
{"label": "Thunar", "sublabel": "File Manager", "icon": "thunar", "meta": {"version": "4.18", "type": "filemanager"}}
|
|
{"label": "mpv", "sublabel": "Media Player", "icon": "mpv", "meta": {"version": "0.37", "type": "media"}}
|
|
ITEMS
|
|
}
|
|
|
|
format_template() {
|
|
echo "Using --format to control display." >&2
|
|
echo "Template: '{label} ({sublabel}) v{meta.version}'" >&2
|
|
echo "" >&2
|
|
cat <<'ITEMS' | pikl --format '{label} ({sublabel}) v{meta.version}'
|
|
{"label": "Firefox", "sublabel": "Web Browser", "meta": {"version": "125"}}
|
|
{"label": "Neovim", "sublabel": "Text Editor", "meta": {"version": "0.10"}}
|
|
{"label": "Alacritty", "sublabel": "Terminal Emulator", "meta": {"version": "0.13"}}
|
|
{"label": "Thunar", "sublabel": "File Manager", "meta": {"version": "4.18"}}
|
|
{"label": "mpv", "sublabel": "Media Player", "meta": {"version": "0.37"}}
|
|
ITEMS
|
|
}
|
|
|
|
filter_fields_demo() {
|
|
echo "Using --filter-fields to search sublabel and meta fields." >&2
|
|
echo "Try typing 'browser' or 'editor' to filter by sublabel." >&2
|
|
echo "" >&2
|
|
cat <<'ITEMS' | pikl --filter-fields label,sublabel --format '{label} - {sublabel}'
|
|
{"label": "Firefox", "sublabel": "Web Browser"}
|
|
{"label": "Neovim", "sublabel": "Text Editor"}
|
|
{"label": "Alacritty", "sublabel": "Terminal Emulator"}
|
|
{"label": "Thunar", "sublabel": "File Manager"}
|
|
{"label": "mpv", "sublabel": "Media Player"}
|
|
{"label": "GIMP", "sublabel": "Image Editor"}
|
|
{"label": "Inkscape", "sublabel": "Vector Graphics Editor"}
|
|
ITEMS
|
|
}
|
|
|
|
field_filter_demo() {
|
|
echo "Field filters in the query: type meta.type:browser to match a field." >&2
|
|
echo "Try: meta.type:browser or meta.type:editor or !meta.type:browser" >&2
|
|
echo "" >&2
|
|
cat <<'ITEMS' | pikl --format '{label} [{meta.type}] {meta.res}'
|
|
{"label": "Firefox", "meta": {"type": "browser", "res": "n/a"}}
|
|
{"label": "Chrome", "meta": {"type": "browser", "res": "n/a"}}
|
|
{"label": "Neovim", "meta": {"type": "editor", "res": "n/a"}}
|
|
{"label": "Helix", "meta": {"type": "editor", "res": "n/a"}}
|
|
{"label": "Alacritty", "meta": {"type": "terminal", "res": "n/a"}}
|
|
{"label": "kitty", "meta": {"type": "terminal", "res": "n/a"}}
|
|
{"label": "mpv", "meta": {"type": "media", "res": "3840x2160"}}
|
|
{"label": "vlc", "meta": {"type": "media", "res": "1920x1080"}}
|
|
ITEMS
|
|
}
|
|
|
|
exec_hooks_demo() {
|
|
echo "Exec hooks: --on-hover-exec fires a command on each cursor move." >&2
|
|
echo "Watch stderr for hover notifications as you navigate." >&2
|
|
echo "" >&2
|
|
cat <<'ITEMS' | pikl \
|
|
--on-hover-exec 'jq -r ".item.label // empty" | xargs -I{} echo " hovering: {}" >&2' \
|
|
--on-select-exec 'jq -r ".item.label // .value // empty" | xargs -I{} echo " selected: {}" >&2'
|
|
{"label": "Arch Linux", "sublabel": "rolling release"}
|
|
{"label": "NixOS", "sublabel": "declarative"}
|
|
{"label": "Void Linux", "sublabel": "runit-based"}
|
|
{"label": "Debian", "sublabel": "rock solid"}
|
|
{"label": "Alpine", "sublabel": "musl + busybox"}
|
|
ITEMS
|
|
}
|
|
|
|
handler_hook_demo() {
|
|
echo "Handler hooks: a persistent process receives events on stdin" >&2
|
|
echo "and can emit commands on stdout to modify the menu." >&2
|
|
echo "" >&2
|
|
echo "This demo logs hover events to stderr via a handler script." >&2
|
|
echo "The handler stays alive for the menu's lifetime." >&2
|
|
echo "" >&2
|
|
|
|
# Create a temporary handler script
|
|
local handler
|
|
handler=$(mktemp /tmp/pikl-handler-XXXXXX.sh)
|
|
cat > "$handler" <<'HANDLER'
|
|
#!/bin/bash
|
|
# Simple handler: logs events to stderr, demonstrates the protocol
|
|
while IFS= read -r event; do
|
|
event_type=$(echo "$event" | jq -r '.event // "unknown"')
|
|
case "$event_type" in
|
|
hover)
|
|
label=$(echo "$event" | jq -r '.item.label // "?"')
|
|
index=$(echo "$event" | jq -r '.index // "?"')
|
|
echo " handler got hover: $label (index $index)" >&2
|
|
;;
|
|
filter)
|
|
text=$(echo "$event" | jq -r '.text // ""')
|
|
echo " handler got filter: '$text'" >&2
|
|
;;
|
|
open)
|
|
echo " handler got open event" >&2
|
|
;;
|
|
close)
|
|
echo " handler got close event" >&2
|
|
;;
|
|
*)
|
|
echo " handler got: $event_type" >&2
|
|
;;
|
|
esac
|
|
done
|
|
HANDLER
|
|
chmod +x "$handler"
|
|
|
|
cat <<'ITEMS' | pikl --on-hover "$handler" --on-hover-debounce 100
|
|
{"label": "Maple", "sublabel": "Studio founder"}
|
|
{"label": "Cedar", "sublabel": "Backend dev"}
|
|
{"label": "Birch", "sublabel": "Frontend dev"}
|
|
{"label": "Pine", "sublabel": "DevOps"}
|
|
{"label": "Spruce", "sublabel": "QA"}
|
|
ITEMS
|
|
|
|
rm -f "$handler"
|
|
}
|
|
|
|
handler_add_items_demo() {
|
|
echo "Handler hooks can modify the menu by emitting commands." >&2
|
|
echo "This demo adds items when you hover over specific entries." >&2
|
|
echo "" >&2
|
|
|
|
local handler
|
|
handler=$(mktemp /tmp/pikl-handler-XXXXXX.sh)
|
|
cat > "$handler" <<'HANDLER'
|
|
#!/bin/bash
|
|
# Handler that adds related items on hover
|
|
while IFS= read -r event; do
|
|
event_type=$(echo "$event" | jq -r '.event // "unknown"')
|
|
if [ "$event_type" = "hover" ]; then
|
|
label=$(echo "$event" | jq -r '.item.label // ""')
|
|
case "$label" in
|
|
"Languages")
|
|
echo '{"action": "add_items", "items": [{"label": " Rust"}, {"label": " Go"}, {"label": " Python"}]}'
|
|
;;
|
|
"Editors")
|
|
echo '{"action": "add_items", "items": [{"label": " Neovim"}, {"label": " Helix"}, {"label": " Emacs"}]}'
|
|
;;
|
|
esac
|
|
fi
|
|
done
|
|
HANDLER
|
|
chmod +x "$handler"
|
|
|
|
cat <<'ITEMS' | pikl --on-hover "$handler" --on-hover-debounce 300
|
|
{"label": "Languages"}
|
|
{"label": "Editors"}
|
|
{"label": "Shells"}
|
|
ITEMS
|
|
|
|
rm -f "$handler"
|
|
}
|
|
|
|
pipeline_filter_demo() {
|
|
echo "Filter pipeline demo: chain filters with |" >&2
|
|
echo "Try: 'rolling | !void (exact 'rolling', then exclude void)" >&2
|
|
echo "Try: /sys/ (regex: items containing 'sys')" >&2
|
|
echo "Try: meta.init:systemd (field filter on init system)" >&2
|
|
echo "" >&2
|
|
cat <<'ITEMS' | pikl --format '{label} ({meta.category}, {meta.init})'
|
|
{"label": "Arch Linux", "meta": {"category": "rolling", "init": "systemd"}}
|
|
{"label": "NixOS", "meta": {"category": "rolling", "init": "systemd"}}
|
|
{"label": "Void Linux", "meta": {"category": "rolling", "init": "runit"}}
|
|
{"label": "Debian", "meta": {"category": "stable", "init": "systemd"}}
|
|
{"label": "Alpine", "meta": {"category": "stable", "init": "openrc"}}
|
|
{"label": "Fedora", "meta": {"category": "semi-rolling", "init": "systemd"}}
|
|
{"label": "Gentoo", "meta": {"category": "rolling", "init": "openrc"}}
|
|
ITEMS
|
|
}
|
|
|
|
# ── Scenario menu ─────────────────────────────────────────
|
|
|
|
scenarios=(
|
|
"Plain text list"
|
|
"Big list (500 items)"
|
|
"JSON objects (distros)"
|
|
"Custom --label-key (editors)"
|
|
"Git branches"
|
|
"Git log (last 30)"
|
|
"File picker"
|
|
"Mixed input (plain + JSON)"
|
|
"---"
|
|
"Structured items (sublabel, meta)"
|
|
"Format template (--format)"
|
|
"Filter fields (--filter-fields)"
|
|
"Field filters (meta.type:browser)"
|
|
"Pipeline + field filters"
|
|
"---"
|
|
"Exec hooks (on-hover/select)"
|
|
"Handler hook (event logging)"
|
|
"Handler hook (add items on hover)"
|
|
"---"
|
|
"on-select-exec hook (legacy)"
|
|
)
|
|
|
|
# Map display names to functions
|
|
run_scenario() {
|
|
case "$1" in
|
|
*"Plain text"*) plain_list ;;
|
|
*"Big list"*) big_list ;;
|
|
*"JSON objects"*) json_objects ;;
|
|
*"label-key"*) custom_label_key ;;
|
|
*"Git branches"*) git_branches ;;
|
|
*"Git log"*) git_log_picker ;;
|
|
*"File picker"*) file_picker ;;
|
|
*"Mixed input"*) mixed_input ;;
|
|
*"Structured items"*) structured_items ;;
|
|
*"Format template"*) format_template ;;
|
|
*"Filter fields"*) filter_fields_demo ;;
|
|
*"Field filters"*) field_filter_demo ;;
|
|
*"Pipeline + field"*) pipeline_filter_demo ;;
|
|
*"Exec hooks"*) exec_hooks_demo ;;
|
|
*"Handler hook (event"*) handler_hook_demo ;;
|
|
*"Handler hook (add"*) handler_add_items_demo ;;
|
|
*"on-select-exec"*) on_select_hook ;;
|
|
"---")
|
|
echo "that's a separator, not a scenario" >&2
|
|
return 1
|
|
;;
|
|
*)
|
|
echo "unknown scenario" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ── Main ──────────────────────────────────────────────────
|
|
|
|
main() {
|
|
echo "pikl demo launcher" >&2
|
|
echo "pick a scenario, then interact with it" >&2
|
|
echo "" >&2
|
|
|
|
choice=$(printf '%s\n' "${scenarios[@]}" | pikl) || {
|
|
echo "cancelled" >&2
|
|
exit 1
|
|
}
|
|
|
|
# pikl outputs JSON. Strip quotes and extract value for matching.
|
|
choice=$(echo "$choice" | jq -r '.value // .' 2>/dev/null || echo "$choice" | tr -d '"')
|
|
|
|
echo "" >&2
|
|
echo "── running: $choice ──" >&2
|
|
echo "" >&2
|
|
|
|
result=$(run_scenario "$choice") || exit $?
|
|
|
|
echo "" >&2
|
|
echo "── result ──" >&2
|
|
echo "$result"
|
|
}
|
|
|
|
main "$@"
|