Venom
RFC-0023venom-spec

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 tag example uses the verb’s [{key,value}] form for --set and the reply.
  • 2026-11-12 (venom-org/venom-spec#164, normative, compatible; accepted by the maintainer): §3 names the owner-only exec error denied.owner, and §6 adds conflict.config_rule for a runtime rule on a config rule’s id, which the owner’s user namespace (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’s status.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 call status.set again 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:

  1. exec is acceptable in v1, as a stopgap until a later RFC adds scripting. Programs pushing through verbs remain the main path.
  2. Breaking config is fine while venomx is in heavy development.
  3. Event rules come both from config and from a runtime verb.
  4. 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 user namespace. 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) under user.*.
  • Config entries MAY override slot fields (side, priority, style, show, group, click) of any id, including venomx.* 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 .panes for venomx.panes) from config.
  • An exec source runs a program as the daemon’s user. Like an .exec key binding, it can only come from the config files, and changing an exec entry with config set (adding, removing or changing one) requires the owner principal. Any other principal, even one with admin, gets denied.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. priority no 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 = false hides it. Both are ordinary config, changed with config set and hot-reloaded. The client local action toggle-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. key names a top-level field of the event’s data; the extra keys type and resource name the envelope fields. op is .eq, .ne or .prefix. Values compare as strings, and an integer is compared in its decimal form. A missing field is not equal to any value, so .ne holds and .eq and .prefix don’t. There are no other operators: a rule is data, not code.
  • Firing. On a matching event the daemon renders .format with the event.* variables (section 8) and sets the segment. A new revision emits status.changed and restarts ttl_ms. A clear_on event, for the same resource as the event that last fired, clears it (status.cleared, reason rule). The daemon MAY coalesce firings to at most 10 per second per rule.
  • Lifetime. A rule needs ttl_ms, clear_on or both, so a rule can’t leave a segment up for ever by accident.
  • Scope. scope = event puts the segment in the session of the event’s resource, and falls back to server when the resource is in no session. The default is server.
  • Visibility (normative). A rule only receives events that its registering principal could receive from the events verb with its current read grants (RFC-0003, RFC-0005). Setting the resulting segment needs the same write grant as status.set on 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.clear on a rule’s id clears the current text, and the rule stays.
  • bind works 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} and status.rule_cleared {id, reason}, with reason clear, disconnect or revoked, on resource status:<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 also user (RFC-0016 section 5.1). So status.rule.set and status.rule.clear on a config rule’s id fail with conflict.config_rule (exit 5), and when config defines an id that a runtime rule holds, the config rule replaces it (status.rule_cleared, reason clear).

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): .verb runs with the client’s own identity, and an .exec child gets no venomx token. So a product may set click through status.set and status.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 click does 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 check and status.rule.set with invalid_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_ms set): venomx runs the command every every_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 in doctor.
  • 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, reason exec). 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 gets VENOMX_SEGMENT=<id>. A program that wants to push richer updates uses status.set with a token of its own.
  • The segment is bind = daemon, in the user namespace, scope = server. At most 16 exec sources run at once (config check fails 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

  1. With the default config, the line is identical to RFC-0022 section 2.8’s shape.
  2. .left without venomx.windows and .unlisted = .hide hides the windows; a config set of .left moves segments without a restart.
  3. venomx.panes with .panes = .focused, .format = "{pane.id}" shows only the focused pane’s ID, and follows a focus change on that client only.
  4. {pane.tag.branch|-} shows -, then main after pane.tag sets it.
  5. [ › {pane.process}] drops the whole bracket when the process is empty.
  6. An unknown variable fails config check with invalid_params.template.
  7. A rule on command.finished with exit_code .ne 0 fires on false, not on true. It clears on its clear_on event and when ttl_ms runs out.
  8. A rule set by a principal with read on session s0 only never fires on an event from session s1.
  9. A bind = connection rule and its segment go when the connection closes.
  10. 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.
  11. config set of an exec entry by a non-owner principal is denied.
  12. Two pushed segments in group tray show as icons inside venomx.tray, and the tray is not drawn when both are cleared.
  13. A click on a segment with .verb runs it with the clicking client’s identity; a pushed segment’s click can’t do more than that client can.
  14. status.hide in config fails config check with invalid_params.config_key.
  15. 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.clear and status.rule.list. Changed: status.set gains optional group, icon and click, and side gains center. All are schema verbs, so CLI, MCP, docs and completions are generated.
  • --json and 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) and denied.owner (4). Others follow RFC-0016 section 5.2.
  • Events. New: status.rule_changed and status.rule_cleared. status.cleared gains the reasons rule and exec.
  • skill documents sources, templates, rules and groups for agents. doctor reports failing exec sources and rules that never matched because of grants. config: status.left, status.center, status.right, status.unlisted and status.segments are new, and status.hide and status.priority are 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 can read. 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, so status.list shows their definitions and no rendered text.
  • OSC 7501 / 133. No change. Their state is readable through pane.program and through rules on program.status_changed and command.*.

Alternatives

  • Do nothing. status.set already 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, and exec is an explicit, supervised source.
  • Keep status.hide and status.priority next 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.clicked event 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.