Introduction
Lictor rotates through the work running in Herdr: terminal panes, tabs, workspaces, and recognized coding agents. Choose targets from the command line or from a keyboard-driven Ratatui interface.
When several coding agents work in parallel, the hard part is noticing which one has finished, is stuck, or is waiting for you. Lictor turns Herdr into a dashboard you can glance at.
There are two ways to watch your work:
- Focus rotation switches Herdr’s focused target every few seconds, like the original
herdr_rotatescript. - Split observation opens a new Herdr tab containing two viewers. Each viewer cycles through its own selected targets while the original terminals continue in their existing locations.
A workspace or tab viewer reconstructs the entire source tab’s pane arrangement. An agent viewer shows the terminal hosting that agent. The viewers observe existing terminal output; they do not launch another copy of an agent.
Both modes are safe by design: Lictor never sends input to your agents, never moves or resizes panes, and only closes tabs it created.
Lictor starts on Linux. Its TUI uses Vim-style navigation and the Tokyo Night palette. The CLI and this book are in English.
Start with installation and quick start. Read split observation for side-by-side monitoring, target selectors to build a precise rotation queue, or how Lictor works for what happens behind the scenes.
The project is a Rust port and extension of Tiago Correia’s herdr_rotate gist. Lictor is licensed under the MIT License, copyright 2026 Tiago Correia. Herdr is a separate project with its own license.
Installation
You need Linux and a running Herdr session. Building from source requires Rust 1.88 or newer; running a prebuilt executable does not require Rust. The repository pins Rust 1.99.0 for its tested development toolchain. Herdr 0.9.3 is the initial integration baseline. Live observation also uses a compatible herdr command in PATH.
Prebuilt packages
Each release ships three packages for Linux x86_64, with the same statically linked binary, the lictor(1) man page, and bash, zsh and fish completions. Check downloads against the release’s SHA256SUMS:
sha256sum -c SHA256SUMS --ignore-missing
Debian, Ubuntu and derivatives
sudo apt install ./lictor_*_amd64.deb
The package installs /usr/bin/lictor, the man page and the completions, which bash, zsh and fish pick up automatically. It suggests libnotify-bin, tmux and xdotool for the optional features. Remove it with sudo apt remove lictor.
Arch Linux and derivatives
sudo pacman -U lictor-*-x86_64.pkg.tar.zst
The package installs the same files in Arch’s standard locations, with the license in /usr/share/licenses/lictor/. Its optional dependencies are libnotify, tmux and xdotool. Remove it with sudo pacman -R lictor.
Any other distribution
The binary in lictor-VERSION-x86_64-unknown-linux-musl.tar.gz is statically linked, so it runs on any x86_64 Linux: Arch, Fedora, openSUSE, Alpine, or an older glibc system. For your user only:
tar -xzf lictor-*-x86_64-unknown-linux-musl.tar.gz
cd lictor-*-x86_64-unknown-linux-musl
install -Dm755 lictor ~/.local/bin/lictor
install -Dm644 lictor.1.gz ~/.local/share/man/man1/lictor.1.gz
install -Dm644 completions/lictor.bash ~/.local/share/bash-completion/completions/lictor
install -Dm644 completions/lictor.fish ~/.config/fish/completions/lictor.fish
install -Dm644 completions/_lictor ~/.local/share/zsh/site-functions/_lictor
For zsh, make sure that directory is on your fpath before compinit runs, for example in ~/.zshrc: fpath=(~/.local/share/zsh/site-functions $fpath). For a system-wide install, use /usr/local/bin, /usr/local/share/man/man1 and your distribution’s completion directories with sudo.
From source
Install the latest version from GitHub with Cargo:
cargo install --git https://github.com/SyscallBrain/lictor --locked
lictor --help
Or build from a clone:
git clone https://github.com/SyscallBrain/lictor
cd lictor
cargo install --path . --locked
cargo build --release --locked builds target/release/lictor without installing it. Check that Lictor can reach your Herdr session by listing its workspaces:
lictor -W -l
Check both the installed client and running server when diagnosing compatibility:
herdr --version
herdr status server
The core JSON socket client does not require jq, Python, or socat. Optional capabilities use external helpers:
| Helper | Purpose |
|---|---|
herdr | Named-session discovery and read-only terminal observation. |
notify-send | Desktop notifications after stopping. |
tmux | Sidebar key delivery to a Herdr client inside tmux. |
xdotool | Sidebar key delivery to an X11 terminal window. |
On a Wayland desktop, sidebar key delivery needs a supported route such as tmux; X11 injection alone cannot control a native Wayland window.
To read the book locally, install mdBook if it is missing, then build or serve it:
cargo install mdbook --version 0.5.4 --locked
mdbook build
mdbook serve --open
The generated book is written to book/. The repository includes workflows for testing, book publication, and Linux release artifacts; see publishing.
Quick start
Inside a running Herdr session, open the selection interface:
lictor
Move with j and k, switch panels with h and l, and use Space to select targets; the queue fills on the right. Press m for split mode and t for the interval, then Enter to start. Press ? for all keybindings.
For direct CLI use, list available agents, then rotate through them:
lictor -A -l
lictor -A -s 5
To alternate workspaces first and agents afterward:
lictor -W -A -s 5
Reverse the flags to put agents first. Tabs can be mixed into the sequence too:
lictor -A -W -T -s 5
Open a new tab with workspace views on the left and agent views on the right:
lictor -W -A -s 5 -S w,a
Both lanes advance every five seconds, each wrapping through its own queue. Workspace views show complete source tabs; the original terminals remain where they were.
To configure the same behavior in the TUI, press m for split mode (workspaces left, agents right), press a in each lane (h/l switches lanes) to select all its targets, set five seconds with t, then press Enter to start. c changes a lane’s kind and s swaps the lanes, so any kind and order works. Alternatively, run lictor --tui -W -A -s 5 -S w,a and press Enter.
Preview a rotation without changing Herdr:
lictor -W -A -s 5 --dry-run -n 8
Stop active rotations from another shell or a Herdr shortcut:
lictor --stop
The split tab stays open on its last targets and continues showing their output. Add --close-on-exit when starting if you want that tab to close on completion or stop.
See jobs for controller handoff and cleanup, and sessions to select a named session or explicit socket.
Focus rotation
Focus rotation changes the target presented by Herdr. Focusing a pane in another tab or workspace also changes its containing tab/workspace.
lictor -p 1,2 -s 3
lictor -T -p 1,2,4 -s 5
lictor -W -s 10
lictor -A -s 5
To build the queue interactively, open the controller with lictor: pick targets of any kind on the left; the queue on the right is the rotation order, and J/K reorder it. See the selection TUI.
Without a group flag, targets are panes in the baseline tab. -T selects tabs, -W workspaces, and -A recognized agents. Group flags can be combined; the first occurrence of each group determines its position in the default queue.
The baseline context is fixed when the invocation starts. Index selectors resolve against that first snapshot, so later focus changes do not make 1 refer to a different target.
The first eligible target is selected immediately, then Lictor waits the configured interval. The default interval is three seconds. Both -s 2.5 and -s 2,5 mean two and a half seconds.
Use -n to finish after a bounded number of successful focus requests:
lictor -p 1,2 -s 2.5 -n 20
Zero means unlimited. Failed focus requests, removed targets, and agents skipped by a status filter do not consume the limit.
Repeated entries intentionally add dwell time:
lictor -p 1,1,2 -s 5
The first target receives two intervals before the second target. Random order keeps these repeated entries; the first lap follows your queue, and later laps are shuffled.
For compatibility with the Bash script, -W -A uses each workspace’s first tab. Add -c to use its active tab instead. -W alone already uses the active tab. Workspace mirrors in split mode use active source tabs by default.
Before each visit, Lictor refreshes state. Removed targets leave the queue; if all disappear, rotation ends with exit code 3. New targets are not automatically selected. A pane moved to another workspace gets a new public ID and therefore leaves a queue that held its old ID.
The selection TUI
Run lictor without arguments, or add --tui to initialize the interface with CLI settings:
lictor --tui -W -A -s 5
Opening the interface does not start a rotation. Select targets, review the queue/settings, and start deliberately with Enter. A redirected, noninteractive invocation must use CLI arguments instead.
The screen is a board of two panels that matches what will run. In focus mode, a target browser sits on the left and the rotation queue on the right. In split mode, the left and right panels are the two lanes of the observation tab, side by side as they will appear. Narrow terminals stack the panels. A Jobs panel appears below them while jobs exist.
The header shows the mode, interval (⏱), iteration limit (↻), active filter and job count. The footer shows the current mode (NORMAL, VISUAL, SEARCH, EDIT) and only the keys that matter in the focused panel. Press ? for every binding, grouped by purpose.
Rows show the target’s name, its workspace and, where Herdr reports one, an agent status with icon and text. Selection follows the target ID when the list refreshes or changes order; a disappearing target is not replaced by whatever takes its row position. ◆ marks the currently focused Herdr target, and #n marks a target’s position in its lane.
| Key | Normal-mode action |
|---|---|
j, k | Next/previous row. |
h, l | Left/right panel (lane in split mode). |
| Tab, Shift-Tab | Cycle panels, including Jobs. |
gg, G | First/last row. |
Ctrl-d, Ctrl-u | Half-page movement. |
| Space | Toggle a target; in the focus queue, remove it. |
v | Select a range; Space applies it. |
/ | Filter rows; Enter keeps the filter, Esc clears it. |
a, u | Select visible rows / clear the panel’s selection. |
m | Switch between focus and split mode. |
c, C | Change the panel’s kind (workspaces, tabs, agents, panes). |
s | Swap the split lanes. |
J, K | Reorder the focus queue. |
t | Set the rotation interval in seconds. |
i | Edit settings: iterations, random order, statuses, sidebar, notifications. |
| Enter | Start the selection; in Jobs, resume the job. |
p | Pause/resume the selected job. |
x, X | Stop a job / close its retained observation tab. |
R | Refresh live targets. |
? | Show grouped keybinding help. |
| Esc | Leave visual/search mode, clear the filter, or dismiss a message. |
q | Leave the controller; launched jobs continue. |
Ctrl-c | Stop the selected job and leave the controller. |
Search and text fields use their own modes. For example, typing q into search does not quit.
Press t, replace the value (Ctrl-u clears it), and press Enter to save or Esc to cancel. Use positive seconds, including decimals such as 0.5 or 0,5. Invalid values keep the editor open with an error and preserve the previous interval. This setting is equivalent to CLI -s and applies when you start a new rotation; it does not change an existing job’s interval.
To create a split tab entirely from the TUI, equivalent to lictor -W -A -s 5 -S w,a:
- Press
mfor split mode. The lanes start as workspaces (left) and agents (right). - Focus a lane with
h/land presscto change its kind if needed. Every combination and order is supported;sswaps the lanes. Changing a lane’s kind clears its selection. - Press
ain each lane to select all its targets, or Space for individual ones. Each lane needs at least one. - Press
t, type5, and press Enter. Then press Enter again to start.
Or prefill everything from the command line: lictor --tui -W -A -s 5 -S w,a, then press Enter.
Switching from focus to split keeps existing selections of the matching kinds. Switching back combines both lanes into one queue, and the lane kinds return the next time you enter split mode.
The controller stays open after Enter. The new job appears in the Jobs panel, where p, x and X control it; a split job’s observation tab opens and takes focus in Herdr. Focus rotation moves Herdr focus away from the controller’s pane, so return to it to pause or stop the job, or use lictor --stop. When you quit, Lictor prints the IDs of jobs that keep running. Detached job output goes to a private log, shown if a job fails to start. Reopening a job’s controller with --tui --job ID pauses its rotation while you inspect it, so it does not keep stealing focus.
The Tokyo Night theme uses blue for the focused panel and selection, purple for the mode, and status icons alongside status text, so nothing depends on color alone. Terminals narrower than 72 columns stack the panels; below 40 × 10 the controller asks you to resize.
Target selectors
List targets before using numeric selectors:
lictor -l
lictor -T -l
lictor -W -l
lictor -A -l
Indices start at one. Names and identifiers are case-sensitive. Herdr IDs are opaque; use the IDs returned by listing instead of predicting their counters.
| Mode | Selector | Meaning |
|---|---|---|
| Pane | N | Pane N in the baseline tab. |
| Pane | T.P | Pane P in tab T of the baseline workspace. |
| Pane | W:T.P | Pane P in tab T of workspace W. |
| Tab | N | Tab N of the baseline workspace. |
| Tab | W:N | Tab N of workspace W. |
| Workspace | N or a name | Workspace number or label. |
| Agent | N, a name, or a kind | Agent ordinal, unique agent name, or every initial agent of that kind. |
| Explicit ID | w1:p2, w1:t2, w1 | A pane, tab, or workspace handle from Herdr. |
In agent mode, a pane ID must contain a recognized agent. A kind such as claude expands to matching agents; it does not create an agent.
lictor -p '2:1.1,3:1.2' -s 5
lictor -T -p 'api:2,web:1' -s 5
lictor -A -p 'claude,reviewer' -s 5
Untyped mixed workspace/agent selectors preserve the original precedence: a matching workspace wins before an agent. For mixed selections, prefer explicit typed selectors:
lictor -T -W -A --targets 'workspace=api,tab=w2:t3,agent=reviewer'
The typed prefixes are workspace=, tab=, agent=, and pane=. This avoids confusion between a workspace called claude and the claude agent kind.
Split lanes have separate selectors:
lictor -S w,a --left-targets 'api,web' --right-targets 'reviewer,codex'
lictor -S t,a --left-targets 'w1:t1,w2:t2' --right-targets 'reviewer'
Use lane flags instead of -p in split mode. Each explicit ID must match its lane kind. Repeated entries remain repeated; any nonexistent requested target rejects the whole invocation before view creation.
Live split observation
Split observation creates a new Herdr tab with two real panes separated by a vertical divider. Viewer processes inside those panes observe existing terminals.
lictor -W -A -s 5 -S w,a
w,a means workspaces on the left and agents on the right. Both advance every five seconds. Each queue wraps independently, so one lane can repeat while the other is still on its first lap.
Lictor switches Herdr to the new tab as soon as both viewers are ready. Herdr labels the panes by lane (Lictor · left · workspaces); inside, each viewer has a header naming what it shows right now, such as ◧ WORKSPACE api › tab 1 or ◨ AGENT codex · web, plus a ⚠ notice if a source closes or its stream fails. To build the same split in the controller, see the selection TUI.
The observation tab opens in the current workspace, regardless of which source comes first in the rotation queue. Use --target to choose a different workspace context. Sources can belong to any workspace in the selected session.
| Kind | Viewer content |
|---|---|
w | The workspace’s active eligible source tab, with its entire pane arrangement. |
t | A selected tab and its pane arrangement. |
a | The terminal hosting a recognized agent. |
p | A selected terminal pane. |
Select subsets independently:
lictor -S w,a -s 5 --left-targets 'api,web' --right-targets 'claude,codex'
lictor -S t,a -s 5 --left-targets 'w1:t1,w2:t2' --right-targets reviewer
lictor -S a,a --left-targets reviewer --right-targets builder
The viewer tab is excluded from all Lictor target discovery and workspace mirrors. Creating it may make it the active tab in its workspace, so Lictor retains that workspace’s last eligible source tab rather than observing itself.
Full-tab views read layout metadata and observe each source pane separately. Changes inside an already selected source tab, such as a pane split or closure, are reconciled. Newly created top-level targets are not automatically added to the rotation queue.
The views are read-only. They do not send input, move original panes, duplicate processes, or take control of their PTYs. Focusing an agent normally marks its completion seen; observing its output does not directly consume that status.
Sizing and rendering
Source applications keep their original terminal dimensions. Each viewer observes a source pane at that pane’s real size, so it receives the whole screen.
An agent or pane fills its lane without an extra border; a workspace or tab draws each source pane with a thin titled border in its original position. When a source is taller than its space, the viewer keeps the bottom rows, where the newest output and an agent’s prompt are, and drops rows from the top. Wider sources are cropped on the right. For more room, make the Herdr window larger or hide its sidebar (-b hide). Lictor cannot make the source application reflow without resizing that application’s PTY, and it never does.
The observer streams ANSI terminal screen updates, including colors and cursor operations. The initial Herdr 0.9.3 adapter does not forward Kitty graphics, so inline images are outside the initial mirror capabilities.
Limits and stopping
In split mode, -n counts successful advances across both lanes, including initial presentation. A tick can contribute two advances. For an odd remaining limit, lanes are considered from left to right.
lictor -S w,a -s 5 -n 20 --close-on-exit
If an agent status filter temporarily excludes every candidate, that lane waits on its last view while the other lane continues. A lane whose targets disappear shows an empty state. When every lane loses every target, rotation ends with exit code 3.
By default, stopping or reaching the limit leaves the tab open and its last selected terminals live. --close-on-exit releases the viewers and closes only that job’s created tab. Job controls provide cleanup for retained views.
How Lictor works
This chapter follows a run from the command line to the screen. You don’t need it to use Lictor, but it explains why Lictor behaves the way it does: why selections survive a reorder, why closing the controller doesn’t stop a rotation, and why a mirror can never type into your agents.
you ──► lictor (CLI or controller)
│ 1. snapshot + resolve targets
▼
detached job ──────────── focus / layout.apply ──────────► Herdr socket API
│ 2. schedule ▲
│ 3. lane commands (split mode) │ topology
▼ │
┌──────────────── Lictor mirror tab (owned by the job) ─────────────┴──┐
│ left viewer right viewer │
│ └─ herdr terminal session observe └─ herdr terminal session ... │
│ (read-only stream per source pane) │
└───────────────────────────────────────────────────────────────────────┘
1. Talking to Herdr
Lictor speaks Herdr’s newline-delimited JSON socket API directly; there is no jq, Python or
socat in between. It finds the socket from --socket-path, from --session NAME, from the
Herdr environment of the pane it runs in, or from the default session, in that order.
Everything starts with one session.snapshot: workspaces, tabs, panes, agents with their status,
and the layout of every tab. Lictor turns that into a list of targets, each with a kind
(workspace, tab, pane or agent) and Herdr’s own ID, such as w2, w2:t1 or w2:p1. Lictor treats
these IDs as opaque handles and never guesses them.
2. Resolving a selection
Flags and selectors become an ordered queue against that first snapshot:
-W,-A,-Tselect whole groups, in the order they appear on the command line.-pentries are resolved in those groups: numbers, labels,W:T.Ppositions, agent kinds, or typed selectors such asagent=claude. See target selectors.- Relative positions are anchored to a baseline workspace and tab (where Lictor runs, or
--target), captured once, so1keeps meaning the same pane while focus moves.
From then on a target is its ID. If you reorder the queue or the session changes, Lictor still follows the same target. If the target disappears, it leaves the queue; Lictor never substitutes whatever now sits at its old position. New workspaces or agents are not added automatically.
Validation happens before anything changes in Herdr. A contradiction such as -T -S w,a is
rejected with exit code 2 and a hint.
3. Jobs
A rotation runs as a job: a Lictor process that owns the schedule. The CLI runs the job in the
foreground; the controller starts a copy of the lictor executable in its own session, so neither
quitting the controller nor closing its terminal stops the rotation.
Each job writes a private record under $XDG_RUNTIME_DIR/lictor-UID with its process identity
(PID, start time and executable), its state, and any Herdr tab it created. Before acting on a
record, Lictor checks that identity, so a recycled PID is never signalled by mistake. This is what
--jobs, --stop, --pause, --resume and --close-view use. Pausing and resuming are signals
that the job acknowledges by updating its state; stopping lets the job clean up first (restore
the sidebar, release or keep its views) and then notify.
4. Focus rotation
At every interval the job:
- takes a fresh snapshot and drops targets that no longer exist (ending with exit code 3 when none are left);
- picks the next target in the queue, skipping agents whose live status doesn’t match
--status; - asks Herdr to focus it (
pane.focus,tab.focusorworkspace.focus).
Only successful focus requests count towards -n. When no agent matches the status filter, the
job waits instead of spinning. With -r, the first lap follows your order and later laps are
shuffled, keeping repeated entries.
5. Split observation
Split mode never moves focus between your sources. Instead, it builds a place to watch them.
Creating the tab. Both lanes are resolved first. Then a single layout.apply creates a new tab
in the baseline workspace: a left/right split whose two panes run lictor __viewer …, the hidden
viewer mode of the same executable. Lictor only ever creates a fresh tab; it never passes
existing tab or pane IDs, so it cannot replace a running process. The job records the tab, pane
and terminal IDs it got back, focuses the new tab, and waits for both viewers to report that they
are ready.
Driving the lanes. Each lane has a private control file. At every tick the job picks each
lane’s next eligible target, writes it with a new generation number, and waits for the viewer to
acknowledge. A lane change counts towards -n only once the viewer has a usable picture. A lane
whose agents don’t match --status keeps its last view while the other lane moves on.
Observing. For each source pane on display, the viewer runs
herdr terminal session observe PANE at the source pane’s own size. Herdr sends a full frame and
then ANSI updates; the viewer replays them in order into a virtual terminal (one per pane, using
the vt100 crate). Observation is read-only: Lictor doesn’t attach, take control, send input or
resize anything.
Drawing. A lane shows a header with what it is showing, for example
◧ WORKSPACE api › tab 1 or ◨ AGENT codex · web. Below the header:
- An agent or pane fills the lane.
- A workspace or tab is rebuilt from Herdr’s layout data: every pane in its relative position and size, with a thin border and its title. A zoomed source shows only its zoomed pane.
- When a source is taller than its space, the viewer keeps the bottom in view, where the newest output and agent prompts are. Wider sources are cropped on the right.
Keeping up. Every second the viewer compares the source layout with what it is drawing. If a pane was split, closed or moved, or an observer ended, it reconnects and starts again from a fresh full frame. A workspace lane follows that workspace’s active tab, skipping Lictor’s own tabs, so a mirror never ends up mirroring itself. Lictor-owned tabs and panes are also hidden from every target list.
Stopping. When the job stops or reaches -n, the lanes stop advancing but the tab stays open
and live, recorded as a view-only job, until you close it with --close-view, X in the
controller, or by closing the tab. With --close-on-exit the job closes the tab itself, after
checking that it still holds exactly the panes and terminals it created.
6. The controller
The controller is a Ratatui application on top of the same pieces. It
refreshes the snapshot every two seconds and the job list twice a second, keeps your selections
by target ID, and builds the exact command line it would run. Pressing Enter validates that command
line with the same rules as the CLI, then starts it as a detached job. That’s why anything you
build in the controller can also be written as a lictor command, and the other way round with
--tui.
CLI reference
lictor -h prints a compact summary; lictor --help adds details, selectors, more examples and exit codes. No arguments opens the TUI; with arguments Lictor runs directly, unless --tui is given to review them in the controller first.
lictor -W -A -s 5 # focus each workspace, then each agent, every 5 seconds
lictor -W -A -s 5 -S w,a # new tab: workspaces mirrored left, agents right
lictor --tui -W -A -s 5 -S w,a # same, reviewed in the controller first
lictor -A --status blocked,done # visit only agents waiting for you
lictor --jobs # running jobs and retained mirror tabs
lictor --stop # stop running rotations
Targets
| Option | Meaning |
|---|---|
-W, --workspaces | Rotate workspaces. |
-A, --agents | Rotate recognized agents. |
-T, --tabs | Rotate tabs of the current workspace. -w/--windows remain as hidden legacy aliases. |
-p, --targets LIST | Only these targets, in this order; repeat one for extra dwell. --panes is an alias. |
--status STATES | Only visit agents in these states (idle, working, blocked, done, unknown), checked live. Implies -A. |
-c, --active-tab | With -W -A, show each workspace’s active tab instead of its first. |
-t, --target ID | Resolve relative selectors from this workspace, tab or pane. |
See selectors and agent status. Group flags may be combined; their order on the command line is the rotation order. When -T is mixed with -W or -A, prefix -p entries with their kind (tab=, workspace=, agent=, pane=).
Timing
| Option | Meaning |
|---|---|
-s, --seconds SECONDS | Seconds on each target; default 3; dot or comma decimals. --interval is an alias. |
-n, --iterations N | Stop after N successful moves; 0 (default) runs until stopped. In split mode every lane advance counts. |
-r, --random | Shuffle the order after every lap, keeping repeated entries. |
Split view
| Option | Meaning |
|---|---|
-S, --split LEFT,RIGHT | New tab with two live mirrors; one kind per lane: w, t, a or p (e.g. w,a). |
--left-targets LIST | Targets for the left lane; default all of its kind. |
--right-targets LIST | Targets for the right lane; default all of its kind. |
--close-on-exit | Close the mirror tab when the rotation stops instead of leaving it live on the last targets. |
Split lane kinds imply their target groups; extra group flags must match a lane. -p does not select split lanes. See split observation.
Preview
| Option | Meaning |
|---|---|
--tui | Open the keyboard controller with these settings. |
-l, --list | List matching targets and exit. |
-a, --all | With --list, include the whole session. |
-d, --dry-run | Show the targets and first steps (5, or -n) without changing Herdr. |
Listing and dry-run never focus targets or create tabs. Dry-run uses current agent states and cannot predict later changes.
Jobs
| Option | Meaning |
|---|---|
--jobs | List running jobs and retained mirror tabs. |
-k, --stop | Stop running rotations. |
--pause, --resume | Pause or resume rotations. |
--close-view | Close retained mirror tabs. |
--job ID | Act on one job with the options above; alone, print its state; with --tui, open the controller on it. |
Without --job, the actions apply to all of the current user’s jobs. Use only one action at a time. See jobs.
Herdr
| Option | Meaning |
|---|---|
-L, --session NAME | Use a named Herdr session. |
--socket-path PATH | Use this Herdr API socket; exclusive with --session. |
-b, --sidebar ACTION | show, hide or toggle the sidebar while rotating; restored on stop. |
--sidebar-keep | Keep the sidebar as set after stopping. |
--no-notify | No desktop notification when the rotation stops. |
See sidebar controls for platform support.
Output and general
| Option | Meaning |
|---|---|
-v, --verbose | Print every move with a timestamp. |
-q, --quiet | Print less. |
-h, --help | Compact help; --help for details. |
-V, --version | Show the version. |
Invalid combinations are rejected before anything changes, with a hint on how to fix them.
Packaging
lictor --generate FORMAT prints the man page or a shell completion script to standard output. FORMAT is one of man, bash, zsh or fish:
lictor --generate man | gzip -9n > lictor.1.gz
lictor --generate zsh > _lictor
It is hidden from --help because the release packages already install these files; use it when packaging Lictor yourself. It cannot be combined with other options.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success or clean stop. |
| 1 | General runtime failure. |
| 2 | Invalid arguments. |
| 3 | Missing targets, every target disappeared, or no job to control. |
| 4 | Herdr/server/socket unavailable. |
Sessions and sockets
All identifiers belong to one Herdr server. The same w1:p1 can exist in two named sessions and represent different terminals.
Select a named session:
herdr session list
lictor --session work -A -l
lictor --session work -S w,a -s 5
Or select an explicit API socket:
lictor --socket-path /path/to/herdr.sock -W -l
--session and --socket-path are mutually exclusive. Named-session selection discovers an existing server; it must not start or replace one silently.
Without either option, Lictor uses the caller’s HERDR_SOCKET_PATH when present, then Herdr’s configuration-based socket location. Herdr configuration follows HERDR_CONFIG_PATH, otherwise the XDG configuration directory or ~/.config/herdr/config.toml.
Relative selection context follows:
- An explicit
--targetworkspace or tab. - The caller’s
HERDR_TAB_IDwhen its environment identifies the selected server. - The selected server’s focused workspace/tab.
This context is captured before rotation. Switching focus during rotation does not re-resolve initial numeric selectors.
Herdr has separate API and client sockets. Lictor sends NDJSON API requests to the API socket; observer subprocesses use Herdr’s matching client socket resolution. An explicit API path must therefore point to the correct server installation and its accompanying client socket.
The initial release targets local Linux sessions. SSH machine routing and Windows named pipes are outside the initial platform contract.
Agent status
Lictor uses Herdr’s recognized-agent records rather than interpreting terminal text itself.
| Status | Meaning |
|---|---|
working | The agent is busy. |
blocked | Herdr recognized a request for attention, such as approval/input. |
done | The agent is idle with a completion not yet marked seen. |
idle | The agent is ready and its completion has been seen. |
unknown | An agent is present but Herdr cannot classify its state confidently. |
Rotate only through agents needing attention:
lictor -A --status done,blocked -s 5
--status implies the agent group. The filter is checked on live state before each advance; agents can become eligible or ineligible after startup. If none match, rotation waits without exiting or spinning.
In a mixed queue, the filter affects agents only. Workspace and tab targets continue to be visited. In split observation, an ineligible agent lane waits while the other lane keeps cycling.
Explicit focus can mark a completion seen, changing done to idle. Read-only observation does not directly acknowledge the observed agent’s completion. A server’s status and a particular Herdr client’s badge can differ because clients track viewed completions separately.
If an agent exits a selected pane, that agent target is removed. Lictor does not treat a later unrelated occupant of the same pane as the selected agent automatically. Unknown status is not evidence that a task completed successfully.
Jobs and stopping
A managed job owns its rotation queue, timers, local control state, optional sidebar restoration state, and any observation views it creates. Controller lifetime is separate from job lifetime.
The TUI hands a started rotation to a detached job and stays open, listing it in its Jobs panel. This matters for focus rotation: the user may immediately be looking at a different pane. lictor --jobs lists every job with its state and mirror tab, so you can find its ID later.
lictor --stop
lictor --job JOB_ID --pause
lictor --job JOB_ID --resume
lictor --job JOB_ID --stop
lictor --tui --job JOB_ID
Reopening the controller pauses the selected job for inspection. Resume deliberately with Enter or the resume command.
--stop without a job ID addresses every active Lictor rotation for the current user, across Herdr sessions. It does not stop the Herdr server or source agents. No matching active rotation returns exit code 3.
Ctrl-C, SIGTERM, and SIGHUP stop scheduling. Cleanup and sidebar restoration finish before the stop acknowledgement/notification. Pending socket waits and timers must not cause another focus change after acknowledgement.
Retained observation views
By default, a split stop ends rotation while preserving the last selected source views. Viewer processes remain tracked as view-only resources and continue to show source output. They are not reported as active rotations.
Close them through the TUI’s close-view action or by closing the generated Herdr tab. Starting with --close-on-exit requests automatic cleanup on stop/completion. Cleanup closes only resources created by that job.
The private registry lives beneath $XDG_RUNTIME_DIR/lictor-UID when usable, otherwise a user-private temporary directory. Do not delete its files to stop a job; use controls so source observers and sidebar state can be released correctly.
A Herdr stop shortcut
Use the installed executable’s absolute path in your Herdr configuration:
[[keys.command]]
key = "prefix+shift+k"
type = "shell"
command = "/absolute/path/to/lictor --stop"
description = "Stop Lictor rotations"
Check Herdr’s current configuration syntax before adding this binding. The global command stops all of your active Lictor rotations; include --job if you need a single job.
Sidebar and notifications
To hide the Herdr sidebar while rotating:
lictor -W -s 5 --sidebar hide
The actions are hide, show, and toggle. Lictor records the initial state where possible and restores it on completion or stop. --sidebar-keep leaves the requested state in place.
Herdr 0.9.3 has no public sidebar state/set API. The Linux adapter sends the configured Herdr prefix/toggle binding to supported clients through tmux or X11/xdotool. It reads Herdr’s TOML key configuration rather than assuming everyone uses the default ctrl+b, then b.
Visibility is estimated from terminal width versus the pane layout. If that estimate is unavailable, exact restoration cannot be guaranteed. Multiple attached clients and concurrent manual sidebar changes can affect the estimate. An unsupported sidebar route reports a warning while core rotation remains usable.
These limitations concern sidebar control, not the ability to observe existing terminal content.
Stop notifications
When a rotation is stopped, Lictor can send a desktop notification through notify-send. It sends the message after scheduling has stopped and sidebar restoration has finished, so the notification is a useful indication that automatic focus changes are over.
lictor -A -s 5 --no-notify
Missing notification helpers do not prevent rotation. Normal finite completion does not need the same stopped-by-signal notification.
Troubleshooting
Herdr is unavailable
Inspect the selected session and socket rather than restarting a working server:
herdr session list
herdr status server
lictor --session work -W -l
A socket path can exist while its server is unavailable. Client and server versions can also differ after an update. Capability errors identify which required feature is missing. Replacing a running Herdr server stops its terminals, so upgrade/restart it deliberately through your own workflow.
A target does not resolve
List the relevant group and use the returned ID:
lictor -A -l -a
lictor -T -l -a
Pane/tab indices are relative to the captured baseline context. Workspace labels and agent kinds can collide in legacy mixed selection; use typed selectors. Agent targets require recognized live agents, not just a pane whose title resembles one.
A status-filtered lane does not advance
Check whether any selected agent currently has an allowed status. Waiting is expected when none match. Observing a completed agent does not directly mark it seen; focus and client viewing policies have different effects on status.
The mirror looks cropped
The source PTY stays at its original size, so a smaller viewer cannot display every source cell at once. Viewers keep the bottom of a tall source in view (the newest output) and crop wide sources on the right. Make the Herdr window larger, hide its sidebar with -b hide, or mirror agents or panes instead of whole workspaces. Layout structure and process preservation are more important than forcing a source application to reflow.
If a mirror shows an older screen than its source, check its header for a ⚠ notice: the viewer reconnects automatically when a stream ends or the source layout changes.
Inline Kitty graphics are not forwarded by the initial Herdr observer adapter.
Rotation hid the controller
Focus rotation moves Herdr away from the controller’s pane; the controller keeps running there. Go back to it to pause (p) or stop (x) the job, run lictor --stop from another shell or your Herdr shortcut, or use lictor --jobs to find the job’s ID and lictor --tui --job ID to reopen a controller on it, which pauses it while you look. The controller and rotation are separate, so leaving the UI does not strand the job.
Nothing seems to happen after starting a split
The mirror opens in a new tab named Lictor mirror in the current workspace and Herdr switches to it. If it doesn’t appear, check lictor --jobs and the controller’s message line; a job that fails to start reports the last line of its log and the log’s path.
Sidebar control is unavailable
On Linux it needs a supported tmux or X11 route. Native Wayland window key injection is not provided by xdotool. Rotation and observation still work without sidebar control.
A retained view is still running
Stopping rotation intentionally keeps its final live views open. Close the generated tab or use the TUI’s close-view action. Start future rotations with --close-on-exit for automatic view cleanup.
Reporting an issue
Include Lictor/Herdr versions, the command or TUI sequence, expected/actual behavior, and whether the issue reproduces in a synthetic named session. Remove private terminal output, repository names, prompts, environment secrets, and socket paths you do not want to share.
Migrating from Bash
Lictor starts from the behavior of herdr_rotate, preserving target selection, timing, ordering, dynamic agent status, sidebar controls, and the stop-shortcut workflow.
| Original behavior | Lictor change |
|---|---|
-S PATH selects the socket. | Use --socket-path PATH; -S now means split lanes. |
| Bare invocation starts default pane rotation. | Bare lictor opens the TUI; pass CLI arguments for direct rotation. |
-T cannot combine with workspaces/agents. | Mixed groups are supported, with typed selectors for ambiguous mixes. |
| Bash messages/comments are Portuguese. | Lictor messages and documentation are English. |
| Dry-run ignores random/status decisions. | Preview uses the planner against a stated snapshot. |
JSON calls use jq plus Python or socat. | A native Rust socket client handles API calls. |
| Stop discovery uses PID/name markers. | Managed jobs use private ownership/control records. |
| Output strategy changes global focus. | Focus remains available; split mode observes sources without moving their focus. |
Existing command examples with -p, -T, -W, -A, -s, -n, -r, --status, -l, and --stop have corresponding CLI behavior. Update executable paths in shell aliases and Herdr bindings.
The legacy -W -A first-tab policy is retained for focus mode; add -c for active tabs. Split workspace viewers use eligible active source tabs by default.
Lictor intentionally preserves duplicate queue entries as extra dwell time and frozen initial selection membership. Closing a selected target removes it; creating another target at the same numeric position does not silently select it.
Architecture and development
Lictor has one Rust package and executable. Domain targets/queues are shared by CLI, managed jobs, the controller, and split viewers.
The integration has two surfaces:
- A native NDJSON client over the Herdr API Unix socket for topology, focusing, and creating owned layouts.
- An argv subprocess adapter for
herdr terminal session observe, which converts the matching Herdr binary client protocol into read-only NDJSON ANSI frames.
Each observed source terminal has persistent virtual-terminal state. Full frames establish a baseline; subsequent ANSI deltas are applied in order. Full-tab mirrors compose those terminal screens according to authoritative layout data. Drawing can coalesce rendered states; dropping an intermediate ANSI delta corrupts the baseline.
Focus rotation and split lanes share selection/status/liveness rules. Jobs own created resources and private control state. Linux job controls use identity-checked signals with acknowledgement; split viewers use private files for lane commands and acknowledgements. A stopped split job retains its last observation views as tracked view-only resources until closed.
How Lictor works walks through a run end to end. In the source tree:
| File | Responsibility |
|---|---|
src/cli.rs | Arguments, grouped help, and validation. |
src/domain.rs | Targets and snapshots parsed from Herdr. |
src/herdr.rs | Socket discovery, the NDJSON client, and the observer subprocess. |
src/rotation.rs | Selector resolution and the focus rotation loop. |
src/runtime.rs | Job registry, signals, pause/resume, lane control files, and owned-view filtering. |
src/mirror.rs | Observer frame decoding into virtual terminals. |
src/main.rs | Entry point, split jobs, and the viewer. |
src/tui.rs | The controller. |
src/platform.rs | Notifications and sidebar control. |
Read IMPLEMENTATION_PLAN.md in the repository for the agreed behavior and implementation milestones. CONTRIBUTING.md describes local development. The application source is organized around CLI, domain, integration, rotation, UI, mirror, and runtime/platform responsibilities.
Checks
cargo fmt --check
cargo clippy --all-targets --locked -- -D warnings
cargo test --locked
cargo build --release --locked
python3 tests/cli_acceptance.py target/release/lictor
python3 tests/controller_acceptance.py target/release/lictor
python3 tests/lifecycle_acceptance.py target/release/lictor
mdbook build
python3 docs/check_links.py book
Fast tests use synthetic topology/stream fixtures and fake sockets. Controller state tests check stable selection identities and queue validation; PTY checks exercise terminal setup/teardown, modal key flows, and a detached finite job.
Real protocol tests run in a dedicated named Herdr session with synthetic processes. They must never target the owner’s default session. An integration proof should verify source PTY dimensions, focus, identities, observer full/delta handling, fresh argv-backed layouts, and owned cleanup.
Live acceptance scripts
Use an explicitly owned named session beginning with lictor-proof-. It must
contain two synthetic source panes printing SOURCE-A and SOURCE-B repeatedly,
with changing output. Record the source tab/pane IDs from its snapshot. If nested
sessions require experimental.allow_nested, put that setting in a temporary
HERDR_CONFIG_PATH while creating the test session.
export LICTOR_PROOF_SESSION=lictor-proof-your-suffix
export LICTOR_PROOF_SOURCE_TAB=RETURNED_TAB_ID
export LICTOR_PROOF_SOURCE_PANE=RETURNED_PANE_ID
export LICTOR_BIN="$PWD/target/release/lictor"
tests/herdr_protocol_proof.sh
python3 tests/split_acceptance.py target/release/lictor
The first script verifies fresh argv-backed layout creation, independent observer streams, geometry/focus preservation, and full-tab composition. The second covers split creation in the current workspace with both lanes advancing, workspace active-tab preservation, the one-advance placeholder, pause/resume, stop with continued live output, and close-on-exit. Their cleanup closes only returned owned view/job IDs. Stop/delete your owned test session after the run to remove its remaining synthetic sources and layout proof panes.
Attribution and compatibility
The Bash reference is pinned to gist revision 4cb57553f6ed8996f0b22416333ed2383e0160fe. Initial observation research used Herdr v0.9.3, protocol 22. The book should describe actual tested behavior rather than assuming future Herdr versions keep internal binary compatibility.
Lictor’s observation adapter delegates that binary compatibility to the installed Herdr CLI. It does not embed Herdr source code or claim ownership of source terminals. All Lictor project code/docs use the MIT License, copyright 2026 Tiago Correia.
Publishing the book
The repository includes three GitHub Actions workflows:
ci.ymlformats, lints, tests, and builds the Rust application.book.ymlbuilds the book and checks its local links on pull requests and pushes.release.ymlbuilds and validates Linux x86_64 binary/checksum artifacts for tags or manual runs, and publishes a GitHub release forv*tags.
Actions are pinned by commit, and mdBook by version. Workflows use GitHub’s repository token with job-scoped permissions, not a personal access token or a live Herdr connection.
Local preview
mdbook build
python3 docs/check_links.py book
mdbook serve --open
book.toml sets site-url = "/lictor/" so navigation and the error page work under a project Pages path. Change this value if the public repository uses another name or a custom domain.
Enable Pages
The public book lives at https://syscallbrain.github.io/lictor/; Pages is enabled with the repository variable PUBLISH_BOOK=true. For another repository:
After creating the intended repository and reviewing the content:
- In repository settings, select GitHub Actions as the Pages source.
- Run the book workflow manually with its deploy input enabled, or set repository variable
PUBLISH_BOOKtotrueto deploy futuremainbuilds. - Review the
github-pagesdeployment environment and its branch restrictions.
Pull requests build and validate; they do not deploy. Deployment needs only pages: write and id-token: write in the deploy job, in addition to read access for the build. The workflow will not create a repository or enable Pages by itself.
Release artifacts
The release workflow builds with Cargo.lock and runs the full test suite. Then scripts/dist.sh builds a static x86_64-unknown-linux-musl binary and packages it three times: a .tar.gz with the binary, license, readme, man page and shell completions; a Debian package made with cargo-deb from [package.metadata.deb] in Cargo.toml; and an Arch Linux package that scripts/arch-package.sh builds with makepkg from packaging/arch/PKGBUILD, natively on Arch or in an archlinux:base-devel container elsewhere. As a smoke test, the workflow extracts and runs the archived binary, installs, checks and removes the Debian package, and in an Arch container lints the Arch package with namcap, then installs, checks and removes it. It also writes SHA256SUMS. Run scripts/dist.sh locally to get the same files in dist/; it needs rustup target add x86_64-unknown-linux-musl, cargo install cargo-deb, and either makepkg or Docker. The man page and completions are generated by the hidden lictor --generate FORMAT option (man, bash, zsh or fish; see the CLI reference) from the same definitions as --help. A manual run only uploads Actions artifacts for review. Pushing a v* tag also publishes a GitHub release with those files, using .github/release-notes/TAG.md as its notes when that file exists (generated notes otherwise). The tag must match the Cargo.toml version.
To cut a release:
# bump version in Cargo.toml, write .github/release-notes/vX.Y.Z.md, commit
git tag -a vX.Y.Z -m "Lictor vX.Y.Z"
git push origin vX.Y.Z
Before tagging, run isolated integration checks against supported Herdr versions and test the archive on the declared Linux platform. Record compatibility and known rendering/sidebar limits in the release notes.