Skip to content

CLI reference

Every command and flag, how each behaves at the edges, the msg control channel, and the agent interface.

Every scootbar command and flag, and how it behaves at the edges. What scootbar is and why is in Overview. Every option lives in the config file too ($XDG_CONFIG_HOME/scoot/bar.toml); the flags stay and override its values, one by one, at start-up and on every reload.

Early days: the bar shows a clock in the center, and workspaces wherever they are placed (--left workspaces). Modules answer the pointer (click, scroll and hover).

Terminal window
scootbar daemon # a 28-pixel bar along the top of every output, the clock in the middle
scootbar daemon --left workspaces --center clock # each output's workspace numbers on the left, the clock in the middle
scootbar daemon --clock-format '%H:%M' # a 24-hour clock (the default is 12-hour: 3:07 pm)
scootbar daemon --font ~/.local/share/fonts/Inter.ttf --font-size 13
scootbar daemon --right clock # the clock at the right end
scootbar daemon --edge bottom --height 32 # along the bottom, 32 logical pixels tall
scootbar daemon --layer overlay --exclusive false # over everything, reserving nothing
scootbar daemon --margin 8 # floating 8 pixels in from its edge and both sides
scootbar daemon --margin 8,12 # 8 above and below, 12 either side
scootbar daemon --background '#101014' --foreground '#e0e0e0'
scootbar daemon --outputs DP-1,eDP-1 # a bar only on those two outputs
scootbar daemon --config ~/alt-bar.toml # another file than the default
scootbar msg query # every placed module's state as JSON
scootbar msg layout # where each module is on screen, for a click
scootbar msg invoke volume raise 5 # run a module's action, as its click would
scootbar msg invoke volume popup # open (or close) the volume slider popup
scootbar msg subscribe # stream changes, one JSON line each
scootbar msg reload # re-read the file and live-apply it
scootbar msg toggle # hide the bar (and release its space), or show it
scootbar --help # and `scootbar daemon --help`, `scootbar msg --help`
scootbar --help --json # the same content as JSON (see below)
scootbar --version

The binaries document themselves for agents: scootbar help daemon and scootbar help msg print each command’s page, and scootbar --help --json emits commands, daemon flags (with types and defaults), msg commands, the modules in the build, exit codes and environment — see Generated CLI pages for the contract.

scootbar daemon runs in the foreground until the compositor goes away (start it with &, or from your compositor’s autostart). It connects to the compositor named by $WAYLAND_DISPLAY, which must support wlr-layer-shell (scoot, sway, niri, Hyprland and most wlroots compositors do).

Each flag at most once, as --flag VALUE or --flag=VALUE.

Flag Takes Default What it does
--outputs all, or connector names, comma-separated all Which outputs get a bar (DP-1,eDP-1). See Outputs.
--edge top or bottom top The output edge the bar runs along. Vertical bars (left and right) are a deliberate omission: the layout is horizontal, and the modules, the hit-testing and the text all assume it.
--layer bottom, top or overlay top The layer-shell layer. See Layers and the zone.
--exclusive true or false true Whether the bar reserves its height, so windows are arranged beside it, or floats over them, reserving nothing.
--height 1 to 1024 28 The bar’s height in logical pixels. On a scaled output it is drawn at the output’s real pixels: 28 at scale 1.5 is 42 device pixels.
--margin one to four of 0 to 1024, comma-separated 0 Space between the bar and the output’s edges, in logical pixels, in CSS order: ALL, VERTICAL,HORIZONTAL, TOP,HORIZONTAL,BOTTOM or TOP,RIGHT,BOTTOM,LEFT. See Margins.
--background '#rrggbb' '#1e1e2e' The bar’s color, six hex digits in either case. Quote it: the shell reads # as a comment. Its opacity is the file’s [bar] opacity: see Shape and opacity.
--foreground '#rrggbb' '#cdd6f4' The text’s color (the theme’s fg token: see Colors).
--font a path the first well-known font found The font file, TrueType or OpenType (.ttf, .otf; a collection’s first face). Any bytes: a path need not be UTF-8.
--font-size 1 to 256 14 The text’s size, the em, in logical pixels; drawn at the output’s real pixels like the bar.
--left, --center, --right module ids, comma-separated, or empty the clock in the center The modules along each part of the bar, in order. Giving any of the three sets the whole layout: a part not given is empty, so --right clock moves the clock rather than adding a second one. --center '' places nothing: a plain bar that needs no font. See Layout.
--padding 0 to 1024 8 Logical pixels either side of each module’s content.
--spacing 0 to 1024 0 Logical pixels between neighbouring modules.
--clock-format a format, at most 256 bytes '%-I:%M %P' What the clock shows; see The clock.
--config a path $XDG_CONFIG_HOME/scoot/bar.toml (~/.config/scoot/bar.toml without it) The config file to read instead of the default; see below.
--check nothing (a switch) off Validate and exit instead of running: see --check.

A malformed or out-of-range value, an unknown flag or a flag given twice is a usage error (exit status 2), and nothing starts; so is an unknown module id, a module placed twice, or a clock format with an unknown specifier or a control character.

scootbar daemon reads $XDG_CONFIG_HOME/scoot/bar.toml (~/.config/scoot/bar.toml when XDG_CONFIG_HOME is unset or empty), its own file separate from scoot’s config.toml, so it works on other compositors and a bar change never breaks scoot. --config PATH reads another file instead. A missing file is the defaults; an explicit --config naming nothing is a refusal.

Every section is optional; absent is the default. Precedence is defaults, then the file, then the flags: a flag given replaces the file’s value for its own option, any of --left/--center/--right replaces just that section of the file’s layout, and a reload keeps the flags over the file, as at start-up.

outputs = "all" # or ["DP-1", "eDP-1"]: see Outputs below
left = ["workspaces"]
center = ["clock"]
[bar]
edge = "top" # top or bottom
layer = "top" # bottom, top or overlay
exclusive = true # false: float over the windows, reserving nothing
height = 28 # 1 to 1024
margin = "8,4" # one number, or the CSS shorthand "VERTICAL,HORIZONTAL", ...
radius = 8 # 0 to 512, and at most half the height; 0 is square
popup-radius = 8 # 0 to 512; unset is the bar's radius: see Popups
opacity = 0.9 # 0 (transparent) to 1 (opaque), the background's alpha
font = "/path/to/Font.ttf"
fallback-fonts = ["/path/to/Symbols.ttf", "/path/to/Cjk.otf"] # at most 2: see Fonts
font-size = 14 # 1 to 256
padding = 8 # 0 to 1024
spacing = 0 # 0 to 1024
separator = 0 # a line in the gap between modules, 0 to spacing: see Spacing
tooltip-delay = 500 # ms the pointer rests on a module before its tooltip shows, 0 to 10000; 0 is off: see Tooltips
[colors]
background = "#1e1e2e"
foreground = "#cdd6f4"
accent = "#f9e2af"
dim = "#6c7086"
urgent = "#f38ba8"
[clock]
format = "%-I:%M %P"
on-click = { exec = ["foot", "-e", "calcurse"] } # a command; or on-right-click, on-middle-click, on-scroll-up, on-scroll-down
icon = "\U000f0e65" # one glyph before the time, from a symbol font: see Fonts
# icon-path = "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" # or SVG path data, or:
# icon-viewbox = "0 0 24 24" # the path's plane (that is the default)
# icon-image = "/abs/path/icon.png" # or a PNG (--features icon-image): see Icons
margin = 0 # extra room on each side of the module, 0 to 1024: see Spacing
[workspaces]
margin = 0 # as the clock's
pill-shape = "rect" # the active number's pill: rect, pill or circle
pill-radius = 0 # a rect's corner radius, 0 to 1024
pill-inset = 0 # the pill's gap from the bar's top and bottom, 0 to 1024
on-scroll-up = "previous" # the interaction keys, on every module: see Pointer input
on-scroll-down = "next"
[window-title]
show-app-id = false # the app id after the title: "title - app"
max-width = 480 # the most logical pixels wide the title's span may be
placeholder = "" # shown when no window is focused (empty takes no space)
allow-close = false # a middle click (or the close action) closes the window
margin = 0 # as the clock's
[volume]
step = 5 # percent points per scroll notch and per raise, 1 to 50
max-volume = 100 # the cap a raise stops at: 100 is full scale, to 150 is over-amplification
on-right-click = { exec = ["pavucontrol"] } # a mixer, or any command
on-click = "popup" # a slider popup under the module, instead of mute: see Popups
margin = 0 # as the clock's
[microphone] # the default source, same keys as [volume]
step = 5
margin = 0
[battery]
warn-below = 20 # the class turns warn at or below this percent, 0 to 100
urgent-below = 10 # urgent at or below this percent; the on-low crossing
batteries = "combine" # the mean, or "first" for the first battery
on-low = { exec = ["notify-send", "Battery low"] } # run once per downward crossing of urgent-below
margin = 0 # as the clock's
[brightness]
device = "apple-panel-bl" # the backlight to follow; absent is the first usable one
step = 5 # percent points per scroll notch and per raise, 1 to 50
margin = 0 # as the clock's
[media]
player = "spotify" # the player to prefer when several run; absent is the one that played last
max-width = 320 # the most logical pixels wide the module's span may be, 1 to 4096
margin = 0 # as the clock's
[bluetooth]
menu-command = ["fuzzel", "--dmenu"] # the device picker, fed the device list on stdin
margin = 0 # as the clock's
[button.launcher] # modules the file defines, placed by name in the lists above
icon = "\U000f0e65"
on-click = { exec = ["scootlaunch"] }
[exec.weather]
command = ["sh", "-c", "while :; do curl -s 'wttr.in?format=1'; sleep 600; done"]
icon = "\U000f0e65" # one glyph before the text, from a symbol font: see Icons
[push.status]
placeholder = "..."
icon = "\U000f0e65" # as the exec's
[output."eDP-1"] # what differs on one output: see Outputs below
height = 36
right = ["clock"]

margin takes an integer (every side) or the --margin shorthand string. radius, popup-radius, opacity, separator, the modules’ margin and the pill’s keys are file-only: they have no flags (see Shape and opacity, Spacing and the pill). The module lists take the ids in Modules; giving any of the three sets the whole layout, as the flags do. An unknown key anywhere is a loud error naming it, as is a bad value, which names its dotted key (bar.height, colors.background, left, clock.format; output."eDP-1".height in an output table). A bad file refuses to start the daemon (exit status 1); a bad reload is refused and the running bar stands undisturbed.

Terminal window
scootbar daemon --check --config ./bar.toml # prints `ok` and exits 0, or says why and exits 1
scootbar daemon --check # the default file, as a start would read it

Does what a start does before it connects to anything, and stops there: reads the config file, applies the other flags over it, starts the placed modules and loads the font. It prints ok on stdout and exits 0, or prints the error a start would print (a bad key, a bad value, a font that cannot load, a missing --config file) on stderr and exits 1; a flag the file clashes with is a usage error, status 2, as at a start. It never connects to the compositor and never claims the control socket, so it needs no WAYLAND_DISPLAY and no runtime directory, and it runs beside a live bar without touching it. Its use is a build step or a pre-flight over a file about to be installed: the Nix module’s check runs it over every rendered file (checks.<system>.scootbar-modules, docs/nix.md). Modules that report themselves unavailable at start (the clock without a time zone database, say) print their note as a start does and are left out; that is not a failure. What the flag costs (aarch64, release, stripped): +2.4 KB of text (code and help; 1,200,636 to 1,203,036 bytes), no change in data or bss, the file the same size to the byte (segments are page-aligned), and an idle daemon’s VmRSS unchanged within noise (3,360 to 3,376 kB, one 3,552 outlier, five runs each). It moves startup code into a function the start also uses; nothing on a per-frame path.

Asks the running daemon over its control socket, $XDG_RUNTIME_DIR/scootbar-DISPLAY.sock, a lock-guarded, owner-only socket with line-framed JSON, one daemon per display: a second daemon refuses, saying one already runs.

Terminal window
scootbar msg query [ID] # every placed module's state as JSON (or one module's)
scootbar msg layout # each module's rectangle in global logical pixels
scootbar msg invoke ID ACTION [N] [--output NAME] # run an action as a click would
scootbar msg subscribe [module] [output] # stay connected, print events
scootbar msg reload # re-read the file and live-apply it
scootbar msg hide # destroy the bar's surfaces and buffers, release its space
scootbar msg show # make them again
scootbar msg toggle # hide if shown, show if hidden
scootbar msg version # the daemon's version and protocol, as JSON
scootbar msg kill # stop the daemon, once its reply is sent
scootbar msg set ID JSON # write a push module's text, class, tooltip and icon (below)

query prints one JSON object: one entry per placed module per output that shows it (an output with no bar, or one whose table leaves the module out, has none), with its id, section (left, center or right), output (the compositor’s wl_output.name, null where it never sent one), the text it shows and its class (normal, warn, urgent, muted), plus icon where the module shows a glyph icon (absent otherwise, and for a path or image icon, which is not text), tooltip (absent while empty) and a value where it has one (the workspaces module: {"active": 2, "workspaces": [1, 2, 3]} for that output, active null when none is; the window-title module: {"title": "editor", "app_id": "foot", "fullscreen": false} for the focused window, absent when none is focused; the volume module: {"volume": 49, "muted": false, "sink": "alsa_output..."} for the default sink, absent while no server answers (the microphone variant reports "source" instead of "sink"); the battery module: {"percent": 72, "state": "discharging", "batteries": 1}, absent where there is no battery; the network module: {"state": "wifi", "ssid": "Wimbly", "signal": -54, "bars": 4, "interface": "wlan0", "vpn": false}, "ethernet" and "vpn" with the interface, or {"state": "disconnected"}; the brightness module: {"percent": 49, "device": "apple-panel-bl"}, absent where there is no backlight; the bluetooth module: {"state": "connected", "adapters": 1, "powered": true, "connected": 1, "device": "Headset", "battery": 72} (state is off, on or connected; battery only when BlueZ reports one), absent where there is no adapter). This is the agent hook: the bar read as data instead of OCR. query ID lists only that module (an id that is not placed is an error naming the ones that are). The reply is bounded (512 KiB; an agent’s bar is a few hundred bytes a module, text and tooltip each capped at 256 bytes), and is written from the very state the screen is drawn from, so it cannot disagree with a screenshot. reload re-reads the file and live-applies it — geometry, style, layout, modules, the font — after fully validating it first; a bad file is refused and the running bar stands. hide, show and toggle are below. query, layout, version, reload, hide, show and toggle print the reply; kill, set and invoke print nothing on success, and subscribe prints what the daemon sends until it closes (below). set writes to a push module: an id that is not placed, a module that takes no value (every one but push) and a value it refuses are each a loud error naming why, never a silent ok. Without a daemon, every command fails saying so (exit status 1).

What an agent (or a script) uses to read the bar and press it, with no screenshot to read and no pixels to hunt. All of it is on the bar’s own socket, separate from scoot’s IPC.

layout prints, per output, its output name, origin (in the compositor’s global logical pixels), scale, the bar rectangle (null while the bar is hidden or not yet configured) and the modules that show something, left to right, each with id, section and an x, y, width, height rectangle in the same global logical pixels. It is the layout as last drawn: the spans (in device pixels) the last committed frame used, converted with the output’s current scale (the reply does not record the scale a frame was drawn at, so a scale change that has not been redrawn yet is the one moment a rectangle and the pixels can disagree) and rounded outward, so a pointer anywhere on a drawn pixel of a module is inside its rectangle. Aim scoot’s pointer injection at the middle of a rectangle (scoot msg pointer click X Y, which moves there and presses and releases the left button; right and middle name the others) and the module is pressed; a test clicks the first and last logical pixel of every rectangle and one pixel outside it, on two outputs at scales 1 and 1.5. A hidden bar has no rectangles at all. A module whose text is empty takes no space and is not listed.

invoke ID ACTION [N] [--output NAME] runs an action exactly as a click or scroll would: the same code a pointer press ends in, so what an agent does and what a user does cannot diverge. ACTION is one of the module’s own actions (scootbar msg --help, or the refusal, lists them: the workspaces module’s activate N, activate-position N, previous, next), or a trigger (click, right-click, middle-click, scroll-up, scroll-down), which runs the binding configured for it. A scroll’s N is its steps (1 to 32, default 1); a module’s own action takes the number it asks for. --output names the output whose module is meant (default: the first that shows it). A module that is not placed, an action it does not have (or a trigger it has no binding for), a number where none is taken or a missing or out-of-range one, and an output that does not show the module are each a named error and run nothing. Success prints nothing.

subscribe [module] [output] keeps the connection open and prints one JSON line per event, after one {"type":"subscribed","events":[...]} line. No kind named is both. There is no snapshot: a subscription starts from now and only changes follow, so to read the state and then follow it, subscribe first, then query (the other order can miss a change between the two; this one at worst repeats one the query already shows). A module event is a query entry with "type":"module", sent when that module’s view changed, once per output that shows it; an output event is {"type":"output","change":"added"|"removed","name":...}. Events are coalesced to the frame rate: a module that changes a hundred times in a frame is told once, with its latest view, and a batch goes out at most every 16 ms (the loop sleeps only until a held batch is due). A reload tells every module again. A subscribed connection serves no further requests (it gets one error line, however many it sends), at most 4 may be subscribed at once (a fifth is refused saying so), and a subscriber that stops reading is disconnected, never buffered: each batch is one nonblocking write, and one the socket cannot take whole ends the connection. With no subscriber the daemon does one branch a loop turn.

How a subscription ends, and what a script may conclude. The command prints whole lines only, and there are endings it can tell apart and one it cannot:

  • A {"type":"dropped"} line, the last one (printed, then the command exits 1 with a line on stderr saying so): the daemon dropped this subscriber and said so. It is sent only when the daemon can write it without waiting, which is the rare drop: closing a subscriber when out of file descriptors with nothing but subscribers to close. A flood of ordinary connections never does it: the oldest non-subscriber is closed first, and subscribers are at most 4 of the 16 connections the daemon holds.
  • A line cut in the middle (exit 1, stderr says the connection ended in the middle of a line): the daemon dropped the subscriber part-way through writing a batch. The partial line is discarded, never printed as if whole.
  • An error reply to the subscribe itself (exit 1): refused, with why.
  • Anything else is exit 0, and it does not mean the daemon went away. The stream just ends. That is what the daemon exiting looks like, and also what a subscriber dropped for being too slow, or stopped (SIGSTOP, a hung pipe: its socket is full, so there is no room for a dropped line) looks like, as does a drop whose partial write happened to stop on a line boundary. A script cannot tell these apart, so after any end, exit 0 included: subscribe again, then query.
  • Side margins wider than the output leave the bar no width. It is then drawn one pixel wide rather than not at all, on scoot and on sway (which sends the negative width it works out as a huge unsigned one; scootbar treats any side past i32::MAX as “yours to choose”). The zone is still reserved.
  • A bar taller than the output is the compositor’s to clamp, and they differ: scoot gives the bar the output’s height and reserves all of it; sway 1.12 configures the full --height (1024 on a 720-tall output), so the bar runs off the edge, and reserves the whole output too. Either way no window has room: this is a value to avoid, not a layout.
  • No flag value can make a buffer overflow: the bounds keep every size far from it. A buffer too large for wl_shm (a compositor asking for an absurd surface) is refused with a message; the draw is tried a few more times, then not again until the size or scale changes.
  • The compositor going away (it exits, crashes, or sends a protocol error) ends the daemon with exit status 1 and one line on stderr saying why. There is no reconnect; your session’s autostart starts it again with the compositor.
  • SIGTERM and SIGINT end it at once, running no destructor: it keeps no state, and the compositor removes its surfaces with the connection. Its exec commands end with it all the same (the kernel’s parent-death signal, see exec above); only what such a command started in turn is left to finish. SIGHUP keeps its default action (it ends the daemon) rather than reloading: catching one would need unsafe signal registration, which the crate forbids (#![forbid(unsafe_code)]), so a reload is scootbar msg reload.
  • Vertical bars (left and right edges) are not offered: a deliberate omission, not a gap. The layout, the modules and the click hit-test are horizontal; a vertical bar would be a second layout, not an option.
  • One daemon per display holds the control socket: a second daemon for the same display refuses at start-up, saying one already runs. A socket file left by a crash is recognised by its free lock and replaced; the lock file itself is never removed.
  • Linux only: it does not build on any other system (the build stops with “scootbg-mem, scootbg and scootbar run on Linux only”).
Status When
0 --help or --version, or daemon --check found nothing wrong
1 no usable font (with a module placed), cannot connect, the compositor lacks wl_compositor v4, wl_shm or zwlr_layer_shell_v1, the compositor went away, poll(2) failed, the config file is malformed (daemon --check too), or a msg command failed (no daemon, or the daemon refused), or a msg subscribe ended on a dropped line, in the middle of a line, or on stdout closing
2 a usage error