Automation recipes
These recipes are complete scripts that automate rozi through its CLI, pickers, hooks, services, and published activity. Copy one, adapt it, and see Control CLI for every command and flag they use.
Call rozi from a script
A script that rozi launches — from a pane, a key binding, a hook, or a service — should call the same rozi binary and UI that launched it. rozi provides both in ROZI_BIN and ROZI_SOCKET:
ROZI=${ROZI_BIN:-rozi}
if [ -n "${ROZI_SOCKET:-}" ]; then
set -- "$ROZI" --socket "$ROZI_SOCKET"
else
set -- "$ROZI"
fiAfter this setup, "$@" <COMMAND> runs a rozi command. Each shell recipe below repeats the block so it can be copied on its own.
Pick and switch a Git branch
Choose a branch from a picker and switch to it:
#!/bin/sh
set -eu
ROZI=${ROZI_BIN:-rozi}
if [ -n "${ROZI_SOCKET:-}" ]; then
set -- "$ROZI" --socket "$ROZI_SOCKET"
else
set -- "$ROZI"
fi
branch=$(git branch --format='%(refname:short)' | "$@" pick --title "Git branches") || exit 0
[ -n "$branch" ] || exit 0
git switch "$branch"
"$@" notify "switched to $branch"rozi pick reads one row per input line and prints the selected row. It exits 1 when the user cancels, which || exit 0 turns into a quiet exit. See Pickers for JSON rows with groups, disabled entries, and actions.
Open a worktree in a pane
Choose a Git worktree and open a shell in it:
#!/bin/sh
set -eu
ROZI=${ROZI_BIN:-rozi}
if [ -n "${ROZI_SOCKET:-}" ]; then
set -- "$ROZI" --socket "$ROZI_SOCKET"
else
set -- "$ROZI"
fi
worktree=$(
git worktree list --porcelain |
awk '$1 == "worktree" { sub(/^worktree /, ""); print }' |
"$@" pick --title "Git worktrees"
) || exit 0
[ -n "$worktree" ] || exit 0
"$@" split --cwd "$worktree" --focusKeep "$worktree" quoted: picker output is untrusted text, and paths may contain spaces.
Use Yazi as a file router
Pick a file in Yazi inside a popup and open it in your editor in a new pane.
Create
~/.config/rozi/scripts/yazi-router:sh#!/bin/sh set -eu ROZI=${ROZI_BIN:-rozi} choice=$(mktemp "${TMPDIR:-/tmp}/rozi-yazi.XXXXXX") trap 'rm -f "$choice"' EXIT HUP INT TERM yazi --chooser-file="$choice" IFS= read -r selected < "$choice" || exit 0 [ -n "$selected" ] || exit 0 if [ -n "${ROZI_SOCKET:-}" ]; then "$ROZI" --socket "$ROZI_SOCKET" split --focus --argv "${EDITOR:-vi}" "$selected" else "$ROZI" split --focus --argv "${EDITOR:-vi}" "$selected" fiMake it executable with
chmod +x ~/.config/rozi/scripts/yazi-router.Bind it to
Ctrl+A, thenShift+E:toml[keys] "ctrl-a shift-e" = { popup = "~/.config/rozi/scripts/yazi-router", keep_open = false }
The script writes Yazi's choice to a private temporary file, removes it on exit, and passes the path to the editor as a separate argument, never as part of a shell command.
The sidebar's file tree follows the same rule: an on_click action receives the selected path in ROZI_FILE. Read the variable instead of inserting the path into a command:
[sidebar]
tabs = [
{ name = "files", label = "", on_click = { run = '''"${EDITOR:-vi}" "$ROZI_FILE"''' } },
]Watch events
Notify when a background pane rings the bell, at most once every five seconds. Save this as ~/.config/rozi/scripts/bell-watch.py and make it executable:
#!/usr/bin/env python3
import json
import os
import subprocess
import time
rozi = os.environ.get("ROZI_BIN", "rozi")
command = [rozi]
if socket_path := os.environ.get("ROZI_SOCKET"):
command += ["--socket", socket_path]
command += ["subscribe", "focus-changed", "bell"]
focused = None
last_notification = 0.0
with subprocess.Popen(
command,
stdout=subprocess.PIPE,
text=True,
) as process:
assert process.stdout is not None
for line in process.stdout:
event = json.loads(line)
data = event.get("data", {})
if event.get("event") == "focus-changed":
focused = data.get("pane")
elif event.get("event") == "bell" and data.get("pane") != focused:
now = time.monotonic()
if now - last_notification >= 5:
subprocess.run(
["notify-send", "rozi", "A background pane rang"],
check=False,
)
last_notification = nowRun it as a supervised service:
[[services]]
name = "bell-watch"
run = "~/.config/rozi/scripts/bell-watch.py"
restart = "on-failure"Unlike a hook, a subscriber keeps state between events: here it remembers the focused pane and when it last notified. Each event's fields are under data; see Hooks for the event list.
Run a job in a detached session and collect its output
Run a command in a session nobody is attached to, wait for it to finish, and print its output. This needs no UI, so it works from cron, CI, or an SSH login, as long as the session (here dev) is running on the same machine.
#!/bin/sh
set -eu
session=dev
# Fail early if the session is not running.
rozi --session "$session" list-panes >/dev/null
pane=$(rozi --session "$session" split --workspace 9 --title nightly 'cargo test' |
jq -r '.data.id')
# The job is done when the pane's process exits; list-panes then reports `exited (<CODE>)`.
while status=$(rozi --session "$session" list-panes --format json |
jq -r --argjson p "$pane" '.data[] | select(.id == $p) | .status'); do
case "$status" in
exited*) break ;;
"") echo "pane $pane disappeared" >&2; exit 1 ;;
esac
sleep 5
done
rozi --session "$session" capture-pane --target "$pane" --scrollback full --format text
case "$status" in
"exited (0)") exit 0 ;;
*) exit 1 ;;
esacThe script exits 0 only if the job did.
- The pane stays in workspace 9 with its scrollback, so a failure is still there to inspect when someone attaches. A dedicated workspace also keeps the new pane from re-tiling a workspace someone is using.
splitfails withnot-controllerwhile an attached client holds layout control, so run jobs like this in a session nobody is working in.- To run the job on another machine, add
--remote <HOST>before each--session, for examplerozi --remote workbox --session dev list-panes. See Control CLI.
Report a script's progress into a session
Show a long job's progress as the status of the pane it runs in, where the sidebar's Activity list and the pane border display it:
ROZI=${ROZI_BIN:-rozi}
pane=${ROZI_PANE:?run this inside a rozi pane, or pass a pane id}
"$ROZI" --session dev status working --target "$pane" --reason "building"
trap '"$ROZI" --session dev status --clear --target "$pane"' EXITA session endpoint always needs --target, because a pane id does not say which session it belongs to. Pass $ROZI_PANE only to the session that pane is in: against any other session, the same number names a different pane.
Publish a build row
Show a sidebar Activity row that says whether Cargo is running. rozi publish reads complete JSON row snapshots on stdin:
#!/bin/sh
set -eu
ROZI=${ROZI_BIN:-rozi}
if [ -n "${ROZI_SOCKET:-}" ]; then
set -- "$ROZI" --socket "$ROZI_SOCKET"
else
set -- "$ROZI"
fi
publish_rows() {
while :; do
if pgrep -x cargo >/dev/null 2>&1; then
status=working
else
status=idle
fi
printf '{"rows":[{"id":"build","title":"Cargo build","status":"%s"}]}\n' "$status"
sleep 2
done
}
publish_rows | "$@" publishThe row belongs to a pane:
- A publisher running in a pane uses that pane, from
ROZI_PANE. - A supervised service has no pane, so rozi uses the focused live pane when the stream opens. To pin the row to a specific pane, start the publisher with
ROZI_PANEset to a pane ID fromlist-panes.
While a pane has published rows, they decide the displayed state of any agent rozi has detected in that pane, instead of what is on screen, until the publisher sends an empty snapshot or disconnects. A publisher in a pane with no detected agent still gets its Activity row.
Make published rows clickable
List open pull requests as Activity rows, and check one out in a new pane when the user activates its row. rozi publish writes an activation object to stdout for each click:
#!/bin/sh
set -eu
ROZI=${ROZI_BIN:-rozi}
if [ -n "${ROZI_SOCKET:-}" ]; then
set -- "$ROZI" --socket "$ROZI_SOCKET"
else
set -- "$ROZI"
fi
produce_rows() {
while :; do
gh pr list --json number,title,statusCheckRollup |
jq -c '{
rows: map({
id: ("pr-" + (.number | tostring)),
title: ("#" + (.number | tostring) + " " + .title),
status: (
if any(.statusCheckRollup[]?; .conclusion == "FAILURE")
then "blocked"
else "idle"
end
)
})
}'
sleep 30
done
}
produce_rows |
"$@" publish |
while IFS= read -r message; do
number=$(printf '%s\n' "$message" | jq -r '.activate // empty' | awk -F- '{print $2}')
[ -n "$number" ] || continue
"$@" split --focus --argv gh pr checkout "$number"
doneKeep reading stdout for as long as the publisher runs. If unread activations pile up, rozi closes the stream and withdraws its rows.
Record a pane or the whole UI as a GIF or video
To record one pane, use rozi record. It writes every change with its exact time, from inside the session server, with or without a UI, and exports frames with an ffmpeg listing:
rozi --session dev record pane --target 3 --output demo.rozirec # Ctrl+C to stop
rozi record export demo.rozirec --to png-frames frames --scale 2
ffmpeg -f concat -safe 0 -i frames/frames.ffconcat \
-vf "split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=none" demo.gifTo record everything rozi draws, bar and borders included, record the UI instead. Run these from a shell inside rozi; the file lands in the directory you run them in:
rozi record start ui --output ui.rozirec
rozi record stop --ui # when you are done
rozi record export ui.rozirec --to png-frames frames --scale 2
ffmpeg -f concat -safe 0 -i frames/frames.ffconcat \
-vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" -pix_fmt yuv420p -fps_mode vfr ui.mp4Both keep every change with the moment it happened, so the GIF or video plays in real time. See Export frames, a GIF, or a video for the options.
Package a script as an extension
Turn a script into an extension to give it a stable command ID, lifecycle management, and a manifest you can share. This one packages the branch picker from Pick and switch a Git branch:
git-tools/
├── extension.toml
└── scripts/
└── branch-picker[extension]
id = "git-tools"
title = "Git tools"
version = "0.1.0"
api = 1
[[commands]]
id = "pick-branch"
label = "Pick Git branch"
exec = ["{extension_dir}/scripts/branch-picker"]The command's ID is git-tools.pick-branch. Bind it to Ctrl+A, then b, like any other action:
[keys]
"git-tools.pick-branch" = "ctrl-a b"See Extensions for installation, trust, manifests, services, and testing.