Status bar sources and layout
| Field | Value |
|---|---|
| Status | Accepted |
| Accepted | 2026-11-12, explicitly approved by the maintainer |
| Authors | commixon (commixon), with Claude |
| Created | 2026-11-12 |
| Issue | venom-org/venom-spec#162 |
| Supersedes | RFC-0016 section 5.3’s status.hide and status.priority keys and section 5.1’s built-in segment table (breaking, accepted). The rest of RFC-0016 section 5 stands. |
| Superseded by | None |
History
- 2026-11-13 (venom-org/venom-spec#176, editorial): §8’s
pane tagexample uses the verb’s[{key,value}]form for--setand the reply. - 2026-11-12 (venom-org/venom-spec#164, normative, compatible; accepted by
the maintainer): §3 names the owner-only
execerrordenied.owner, and §6 addsconflict.config_rulefor a runtime rule on a config rule’s id, which the owner’susernamespace (RFC-0016 §5.1) makes possible.
Summary
venomx’s status line becomes the bar of the whole venom-os desktop, and stays useful to a standalone venomx user. This RFC splits every status segment into a slot, which says where and when it is drawn, and a source, which says what it shows. There are six sources:
static: fixed text.template: venomx’s own state, read through a closed set of variables.rule: the daemon reacts to an event.pushed: RFC-0016’sstatus.set, unchanged.exec: a command that venomx supervises.group: other segments, drawn compactly (the tray).
Built-in segments become ordinary segments with a default source, so a user
can move, hide or re-template any of them. Layout moves from priority-sorted
sides to explicit .left, .center and .right lists. A segment can carry
a click target. venom-term implements templates and layout in chrome.status,
venomx implements the daemon side, and venom-desk renders the same segments
in its rail.
Motivation
RFC-0016 section 5 gave every product one shared status line, but the line itself is fixed:
- The built-ins (
venomx.session,venomx.windows, the pane blocks of RFC-0022 section 2.8) can be hidden, but not changed. A user who wants only the focused pane’s process, or its pane ID, or a tag that a program set, can’t have it. - Content is either built in or pushed by a connected program with
status.set. Simple cases have to hold a connection open and callstatus.setagain on every change, or draw nothing at all. For example: “show ✕ when a command fails”, “show the hostname”, “show the battery”. - venom-os needs this line as its desktop bar. A desktop bar needs a centre, a tray of small indicators from many programs, and click actions, and the current model has none of them.
The maintainer decided on 2026-11-12:
execis acceptable in v1, as a stopgap until a later RFC adds scripting. Programs pushing through verbs remain the main path.- Breaking config is fine while venomx is in heavy development.
- Event rules come both from config and from a runtime verb.
- The template vocabulary is defined here and implemented once in venom-term.
Waiting on this RFC: venomx’s configurable status line, venom-os’s desktop bar (tray, notification centre), and venom-desk’s rail.
Design
1. Overview by example
.status = .{
.position = .top,
.left = .{ "venomx.session", "venomx.nested", "venomx.mode", "venomx.windows", "venomx.panes", "venomx.pane" },
.center = .{ "venomx.clock" },
.right = .{ "venomx.message", "venomx.notify", "user.failed", "venomx.tray", "venomx.attention" },
.segments = .{
// Re-template a built-in: only the focused pane, process then ID.
.{ .id = "venomx.panes", .panes = .focused, .format = "{pane.process} [{pane.id}]" },
.{ .id = "venomx.clock", .format = "{clock:%a %d %H:%M}" },
// Static text.
.{ .id = "user.host", .text = "devbox", .side = .left, .priority = 20 },
// A rule: flash the exit code of a failed command for 5 s.
.{ .id = "user.failed", .rule = .{
.on = .{ "command.finished" },
.match = .{ .{ .key = "exit_code", .op = .ne, .value = "0" } },
.clear_on = .{ "command.started" },
}, .format = "{event.command} {event.exit_code}", .style = .@"error", .ttl_ms = 5000 },
// An exec source in the tray, refreshed every 30 s.
.{ .id = "user.battery", .exec = .{ .argv = .{ "venom-os-battery", "--short" }, .every_ms = 30000 }, .group = "tray" },
// Slot override for another namespace's pushed segment.
.{ .id = "venom.agents", .priority = 70, .click = .{ .verb = "pane.popup", .args = .{ .preset = "venom.inbox", .argv = .{ "venom", "inbox" } } } },
},
},
work ◤ 1:shell ▸2:agents │ claude [p4] │ devbox Thu 12 14:02 ✕ cargo 101 │ 🔋 81% │ ▲ 1 attention
A program uses only the runtime verbs:
$ venomx status set --id venom-os.wifi --text "wifi" --icon "📶" --group tray --json
{"id":"venom-os.wifi","revision":1}
$ venomx status rule set --id venom.waiting --on '["program.status_changed"]' \
--match '[{"key":"reason","op":"eq","value":"blocked"}]' \
--format '▲ agent waiting' --style warn --ttl-ms 60000 --json
{"id":"venom.waiting","revision":1}
2. Slots and sources
Every segment has one slot and exactly one source.
Slot fields. These are RFC-0016 section 5.1’s fields, plus group,
icon and click:
| Field | Type / default | Meaning |
|---|---|---|
id |
<namespace>.<name> |
As RFC-0016 section 5.1 |
side |
.left | .center | .right, default .right |
Used when the id is in no layout list (section 4) |
priority |
0–100, default 50 | Higher stays longer when space is short |
style |
RFC-0016 roles, default .normal |
|
when |
Tag map | As RFC-0016 section 5.1 |
scope |
server | a session ID |
As RFC-0016 section 5.1; rules add event (section 6) |
show |
Boolean, default true |
false keeps the segment but never draws it |
group |
[a-z][a-z0-9-]{0,31}, optional |
Drawn inside that group’s segment instead of on its own (section 5) |
icon |
UTF-8, at most 2 cells, optional | What a group draws for it; the text is used when absent |
click |
A binding target, optional | Section 7 |
Sources:
| Source | Config field | Evaluated by | Shared or per client |
|---|---|---|---|
static |
.text |
Client | Shared text |
template |
.format |
Client | Per client: depends on its focus and window |
rule |
.rule plus .format |
Daemon | Shared: the result is a segment like a pushed one |
pushed |
none: status.set |
The setter | Shared (RFC-0016) |
exec |
.exec |
Daemon | Shared |
group |
.group_of |
Client | Per client |
A config entry with no source field is a slot override for that id
(section 3). A user.* entry MUST have exactly one source field;
config check fails invalid_params.segment otherwise.
3. Who may define what
- Config entries MAY define sources only for ids in the
usernamespace. A product defines its segments at runtime under its own namespace (status.set,status.rule.set), or in a defaults file (RFC-0016 section 11.1) underuser.*. - Config entries MAY override slot fields (
side,priority,style,show,group,click) of any id, includingvenomx.*and other namespaces. A config override always wins over the value a product set. The owner has the last word over their own bar. venomx.*ids that accept a template (section 8) MAY also take.format(and.panesforvenomx.panes) from config.- An
execsource runs a program as the daemon’s user. Like an.execkey binding, it can only come from the config files, and changing anexecentry withconfig set(adding, removing or changing one) requires theownerprincipal. Any other principal, even one withadmin, getsdenied.owner(exit 4).
4. Layout
.status = .{
.position = .top, // .top (default) | .bottom | .off, as RFC-0016
.left = .{ "venomx.session", "venomx.windows" }, // ids, in drawing order
.center = .{ "venomx.clock" },
.right = .{ "venomx.tray" },
.unlisted = .show, // .show (default) | .hide
.segments = .{}, // definitions and slot overrides
},
- Each list gives drawing order on its side.
priorityno longer orders segments; it only decides which ones are dropped when the line is short. - A segment in no list goes to its slot’s
side, after the listed ones, ordered by priority, then id. With.unlisted = .hide, segments that aren’t listed are not drawn. A distribution that wants a fixed bar sets this. - An id listed twice, or on two sides, fails
config check(invalid_params.status_layout). Listing an id that doesn’t exist (yet) is fine: a product’s segment takes that place once it is set. - Centre. The centre is placed at the middle of the line, then pushed
sideways just enough not to overlap either side. When the line is short,
segments are dropped from all three sides together, lowest priority
first. The last one placed on each side may be truncated with
…(RFC-0016 section 5.3). - Toggling. Leaving an id out of every list (with
.unlisted = .hide) or setting.show = falsehides it. Both are ordinary config, changed withconfig setand hot-reloaded. The client local actiontoggle-segment <id>hides or shows a segment on that client only, until it detaches.
status.hide and status.priority are removed. config check rejects
them with invalid_params.config_key, naming the replacement: a
.segments entry with .show = false or .priority.
5. Groups (the tray)
A segment with .group = "tray" is not placed by itself. The segment whose
source is .group_of = "tray" draws all members: each one’s icon, or its
text when it has none, ordered by priority, then id, separated by one space.
venomx.tray is the built-in .group_of = "tray" (section 8). A user can add
more, e.g. .{ .id = "user.sys", .group_of = "sys" }.
- A group with no visible members is not drawn.
- When the line is short, a group drops its lowest-priority members before the group itself is dropped.
- A member keeps its own
click(section 7). The member under the pointer receives it. - venom-desk MAY draw a group as a list in its rail.
6. Rules
A rule turns events into a segment. The daemon evaluates it and sets the
result like status.set, so every client sees the same thing.
.rule = .{
.on = .{ "command.finished" }, // RFC-0003 event types, 1–8
.match = .{ .{ .key = "exit_code", .op = .ne, .value = "0" } }, // all must hold, 0–8
.clear_on = .{ "command.started" }, // optional, 0–8
},
- Matching.
keynames a top-level field of the event’sdata; the extra keystypeandresourcename the envelope fields.opis.eq,.neor.prefix. Values compare as strings, and an integer is compared in its decimal form. A missing field is not equal to any value, so.neholds and.eqand.prefixdon’t. There are no other operators: a rule is data, not code. - Firing. On a matching event the daemon renders
.formatwith theevent.*variables (section 8) and sets the segment. A new revision emitsstatus.changedand restartsttl_ms. Aclear_onevent, for the same resource as the event that last fired, clears it (status.cleared, reasonrule). The daemon MAY coalesce firings to at most 10 per second per rule. - Lifetime. A rule needs
ttl_ms,clear_onor both, so a rule can’t leave a segment up for ever by accident. - Scope.
scope = eventputs the segment in the session of the event’s resource, and falls back toserverwhen the resource is in no session. The default isserver. - Visibility (normative). A rule only receives events that its
registering principal could receive from the
eventsverb with its currentreadgrants (RFC-0003, RFC-0005). Setting the resulting segment needs the samewritegrant asstatus.seton its scope. A config rule belongs to the owner.
Runtime rules:
| Verb | Params | Result | Grant |
|---|---|---|---|
status.rule.set |
id, on, optional match, clear_on, format, ttl_ms, slot fields, bind |
{id, revision} |
write on scope (server:root for server and event) |
status.rule.clear |
id |
{} |
Same as set; only the namespace owner |
status.rule.list |
none | {rules:[…]} |
read |
- Ids follow RFC-0016 section 5.1’s namespace rule. A rule and the segment
it produces share the id, so
status.clearon a rule’s id clears the current text, and the rule stays. bindworks as for segments:connection(the default) removes the rule and its segment when the connection closes.- Limits: 32 rules per namespace and 128 in total, separate from the
segment limits (
conflict.rule_limit, exit 5). - Events:
status.rule_changed {id, revision, rule}andstatus.rule_cleared {id, reason}, with reasonclear,disconnectorrevoked, on resourcestatus:<id>. - A config rule and a runtime rule never share an id. Config only defines
user.*(section 3), but the owner principal’s runtime namespace is alsouser(RFC-0016 section 5.1). Sostatus.rule.setandstatus.rule.clearon a config rule’s id fail withconflict.config_rule(exit 5), and when config defines an id that a runtime rule holds, the config rule replaces it (status.rule_cleared, reasonclear).
7. Clicks
click is a binding target, exactly as RFC-0016 section 7.3 defines for
keys: one of .local, .verb with .args, or .exec, with the same
placeholders, {popup_ticket} included.
- A left click on a segment (or on a group member, section 5) runs its
target as the clicking client, as a key press would. A target holds
no authority (RFC-0016 section 7.4):
.verbruns with the client’s own identity, and an.execchild gets no venomx token. So a product may setclickthroughstatus.setandstatus.rule.set(new optional param), as it may register key tables. - Other buttons are reserved. A client without mouse reporting ignores
click. Every clickable segment’s target SHOULD also be reachable by a key binding (RFC-0001 parity). - Clicking a segment with no
clickdoes nothing, and the click doesn’t reach the pane underneath.
8. Templates
A template is literal text with variables. It has no expressions, loops or calls.
template = { literal | var | optional }
var = "{" name [ ":" spec ] [ "|" fallback ] "}"
optional = "[" { literal | var } "]" ; dropped whole if any var inside is empty
literal = any text; "{{" "}}" "[[" "]]" are literal braces and brackets
fallback = literal text, no "{" or "["
- An empty variable renders as its fallback, or as nothing. A segment whose whole template renders empty is not drawn.
- An unknown variable, a bad spec or unbalanced brackets fail
config checkandstatus.rule.setwithinvalid_params.template(exit 2), naming the offset. - The output is cleaned like a title line (RFC-0022 section 3.2), then capped at 256 bytes. The layout truncates it like any segment.
Variables. This set is closed. A new variable needs an RFC change.
| Variable | Value |
|---|---|
session.id, session.name |
The attached session; name falls back to the ID |
window.index, window.name, window.id |
RFC-0022 section 2.8’s 1-based index; the name may be empty |
window.label |
<index>:<name>, or <index> alone (RFC-0022 section 2.8) |
pane.id, pane.cwd, pane.process, pane.command, pane.title |
cwd is ~-shortened; process follows RFC-0007 section 5’s naming order |
pane.program |
The OSC 7501 state: idle, working, done, error, blocked, or empty |
pane.label |
RFC-0022 section 2.8’s cwd › process rule, with its fallbacks |
pane.tag.<key> |
That pane tag’s value (RFC-0016 section 4) |
attention.count |
Panes needing attention that this client shows |
host, user |
Short hostname and login name of the daemon’s host |
clock |
Local time; spec is a strftime subset: %H %M %S %d %m %Y %y %a %b %p %% (default %H:%M) |
event.type, event.resource, event.<field> |
Rules only: the event envelope and the top-level string or integer fields of data |
pane.* means the client’s focused pane, except inside venomx.panes,
where it means each pane in turn. window.* means the shown window, except
inside venomx.windows, where it means each window in turn.
A program in a pane changes a template’s output by setting a pane tag:
$ venomx pane tag --pane "$VENOMX_PANE" --set '[{"key":"branch","value":"main"}]' --json
{"pane":"p4","tags":[{"key":"branch","value":"main"},{"key":"owner","value":"user"}]}
With .{ .id = "venomx.pane", .format = "{pane.tag.branch|}" } the line
then shows main. This is the “a running process changes it” case, and it
needs no new verb.
Built-ins. This table replaces RFC-0016 section 5.1’s table of built-in segments.
| ID | Default list | Priority | Source and default | Templatable |
|---|---|---|---|---|
venomx.session |
left | 100 | {session.name}; a client attached to one pane shows {pane.id} |
yes |
venomx.nested |
left | 96 | fixed NESTED (RFC-0016 section 2) |
no |
venomx.mode |
left | 95 | fixed mode label (RFC-0022 section 2.7) | no |
venomx.windows |
left | 90 | per window {window.label}, active one marked |
yes, per item |
venomx.panes |
left | 85 | per pane {pane.label}, focused one in the signal; .panes = .all (default) or .focused |
yes, per item |
venomx.pane |
left | 84 | fixed: the focused pane’s notes (RFC-0022 section 2.8) | yes; empty template means the fixed notes |
venomx.message |
right | 99 | fixed: the client’s messages | no |
venomx.notify |
right | 85 | fixed: the newest notification (RFC-0016 section 6) | no |
venomx.tray |
right | 60 | .group_of = "tray" |
no |
venomx.attention |
right | 80 | fixed: attention counts with marks | no |
venomx.clock |
right | 10 | {clock:%H:%M} |
yes |
The default lists are the order of this table. With the default config the
line looks exactly as RFC-0022 section 2.8 draws it. That section’s rules
for running out of room (cwds first, blocks keep 12 cells) apply while
venomx.panes uses {pane.label}. A custom item template is cut from its
end instead.
9. Exec sources
.exec = .{
.argv = .{ "venom-os-battery", "--short" }, // no shell, argv[0] looked up in PATH
.every_ms = 30000, // optional, 1000–86400000
},
- Interval mode (
every_msset): venomx runs the command everyevery_ms. The last complete stdout line before it exits is the new text. A run still going at the next tick is killed. A non-zero exit keeps the previous text and shows the failure indoctor. - Stream mode (no
every_ms): venomx keeps the command running, and each complete stdout line replaces the text. When it exits, the segment is cleared (status.cleared, reasonexec). It is restarted under the venom-std supervisor, with backoff from 1 s to 60 s. - Line format. Plain UTF-8 is the text. A line that starts with
{is read as JSON{"text":…, "style":…, "icon":…}, and each field is optional. An invalid line is ignored. Lines are capped at 4096 bytes, and the text at 256. - Environment. stdin is the null device, stderr goes to the daemon log,
and the cwd is
$HOME. The child gets no venomx token (as RFC-0016 section 7.3’s.exec). It getsVENOMX_SEGMENT=<id>. A program that wants to push richer updates usesstatus.setwith a token of its own. - The segment is
bind = daemon, in theusernamespace,scope = server. At most 16 exec sources run at once (config checkfails beyond that). Exec sources start with the daemon and stop with it.
Scripting may replace exec in a later RFC. Until then this is the only way
for config to run a program.
10. Pushed segments
status.set is unchanged, except for new optional params group, icon
and click. The text is still verbatim: a pusher formats its own text, and
templates are for config and rules. RFC-0016 section 5.2’s errors apply,
plus invalid_params.segment for a bad group, icon or click.
11. Who implements what
| Part | Repo |
|---|---|
Template parser and renderer, variable table, layout with center, ordered lists, groups and truncation, in chrome.status |
venom-term |
Config schema and checks, rules (with grant-filtered matching), exec supervisor, status.rule.* verbs, group/icon/click on status.set, built-ins on the new model, clicks |
venomx |
| Rendering segments, groups and clicks in the rail | venom-desk |
12. Conformance
- With the default config, the line is identical to RFC-0022 section 2.8’s shape.
.leftwithoutvenomx.windowsand.unlisted = .hidehides the windows; aconfig setof.leftmoves segments without a restart.venomx.paneswith.panes = .focused, .format = "{pane.id}"shows only the focused pane’s ID, and follows a focus change on that client only.{pane.tag.branch|-}shows-, thenmainafterpane.tagsets it.[ › {pane.process}]drops the whole bracket when the process is empty.- An unknown variable fails
config checkwithinvalid_params.template. - A rule on
command.finishedwithexit_code.ne0fires onfalse, not ontrue. It clears on itsclear_onevent and whenttl_msruns out. - A rule set by a principal with
readon sessions0only never fires on an event from sessions1. - A
bind = connectionrule and its segment go when the connection closes. - An interval exec source updates its text, and keeps the old text after
a failing run. A stream exec source updates line by line and is restarted
after it exits. Its child has no
VENOMX_TOKEN. config setof anexecentry by a non-owner principal is denied.- Two pushed segments in group
trayshow as icons insidevenomx.tray, and the tray is not drawn when both are cleared. - A click on a segment with
.verbruns it with the clicking client’s identity; a pushed segment’s click can’t do more than that client can. status.hidein config failsconfig checkwithinvalid_params.config_key.- On an 80-column client, centre, left and right drop together by priority, and the centre never overlaps a side.
Agent-contract impact
- Verbs. New:
status.rule.set,status.rule.clearandstatus.rule.list. Changed:status.setgains optionalgroup,iconandclick, andsidegainscenter. All are schema verbs, so CLI, MCP, docs and completions are generated. --jsonand exit codes. New errors:invalid_params.template(2),invalid_params.status_layout(2),invalid_params.config_key(2) for removed keys,conflict.rule_limit(5),conflict.config_rule(5) anddenied.owner(4). Others follow RFC-0016 section 5.2.- Events. New:
status.rule_changedandstatus.rule_cleared.status.clearedgains the reasonsruleandexec. skilldocuments sources, templates, rules and groups for agents.doctorreports failing exec sources and rules that never matched because of grants.config:status.left,status.center,status.right,status.unlistedandstatus.segmentsare new, andstatus.hideandstatus.priorityare removed. All are hot-reloadable, with defaults in section 8.- Grants. The rule verbs need the same grants as
status.set. Rules only see events their principal canread. Click targets hold no authority. Exec sources only come from config, and only the owner changes them. - Headless. Rules and exec sources run with no client attached, and
their segments are visible to
status.list. Templates are client-only, sostatus.listshows their definitions and no rendered text. - OSC 7501 / 133. No change. Their state is readable through
pane.programand through rules onprogram.status_changedandcommand.*.
Alternatives
- Do nothing.
status.setalready lets a program show anything. But every small indicator would need a program running permanently, the built-ins would stay fixed, and venom-os’s tray and clicks wouldn’t exist. - tmux-style format strings with conditionals and shell interpolation
(
#{?…},#(cmd)). Conditionals turn into a small language, which v1 rules out. Interpolating shell output into any segment hides process spawning inside text. Instead, optional brackets cover the common conditional, andexecis an explicit, supervised source. - Keep
status.hideandstatus.prioritynext to the lists. That means two ways to do one thing, and order depending on priority surprises users. Breaking was accepted. - Rules only in config. venom and venom-os would then have to keep a
connection open and re-push on every event, which the daemon already
sees. Runtime rules are data and are grant-filtered, so they are as safe
as
status.set. - A
status.clickedevent sent to the segment’s owner instead of binding targets. That works only while the owner is connected, and it duplicates the key-table trust model, which is already accepted.
Open questions
None open. The four questions from the 2026-11-12 brainstorm were resolved
by the maintainer, as listed under Motivation. Scripting that could replace
exec is out of scope and will be a future RFC.