flash

Desktop widgets

A desktop widget is a status format drawn as stacked lines on the desktop: above the wallpaper, below the Finder’s desktop icons and every app window, on every Space. It is Flash’s answer to conky. Widgets use the same language, values, plugin segments, named sources and #() jobs as the status bar, and they work whether the bar is on or off.

Widgets are passive. Their windows are click-through, never take keyboard focus and have no chrome or preferences UI; link=, popup= and range= markers do nothing in them, and the config loader warns when a widget uses one.

Quick start

Add this to ~/.config/flash/flash.toml and save:

[widgets.cpu]
anchor = "top_right"
font_size = 16
template = "CPU #[meter=20]#{flash.plugin.cpu.percent}#[nometer] #{flash.plugin.cpu.percent}%%"
CPU ███████▍             37%

Each [widgets.<name>] table (names use letters, digits, _ and -) is one widget. It appears on save; set enabled = false or delete the table and it goes away. Ready-made panels live in examples/widgets.

Lines

Every line of template is its own tmux status line, laid out at the widget’s width: #[align=left|centre|right|notch] (absolute-centre is tmux’s name for notch), fill= and lists work per line. On a widget, centre is the middle of that line. The status bar draws #[align=centre] as its notch. Two rules connect the lines:

Blank lines at the top and bottom of the template are dropped; blank lines in between are kept. \r\n line endings work. In a TOML """ string, a trailing backslash joins the next line onto this one and drops that line’s leading whitespace, so long lines can be wrapped in the file without wrapping on screen:

template = """
CPU #{flash.plugin.cpu.percent}%% \
 MEM #{flash.plugin.memory.percent}%%
"""

As in the bar, write %% for a literal percent sign: the template is expanded by strftime, so %H:%M in it is the time. A conditional branch is expanded a second time, as in tmux, so a percent sign inside #{?…} is written %%%%.

A multi-line value — a plugin table such as processes.top_cpu, a source that prints several lines, #{flash.calendar} — spreads over as many widget lines.

Styling

The widget’s own box is set by its keys: fg is the default text colour, bg the background and border the border colour. bg and border take #RRGGBB or #RRGGBBAA, where AA is the alpha: #2E3440CC is 80% opaque, #2E344000 (the default background) fully transparent, so the text sits directly on the wallpaper. corner_radius, border_size and padding shape the box.

Inside the template, every tmux style works: #[fg=…,bg=…] with ANSI names, colourNNN or #RRGGBB, bold, dim, italics, underline variants, reverse, fill= for a whole line. A widget has one font and one size, set by font (an installed monospaced font) and font_size; for a large clock above small text, use two widgets and stack them with gap_y.

Placement and multiple displays

anchor pins the widget to one of nine points of its display, and gap_x / gap_y keep it that many points from the anchored edges (a centred axis ignores its gap). screen picks the display: "primary" is the one with the menu bar, a number counts displays left to right from 1, and "all" draws one copy of the widget on every display. A number past the last display shows nothing until that display is connected.

A widget is placed in the part of its display Flash considers usable: the visible frame (without the Dock) minus the Flash bar’s band when the bar reserves space on that display. It never extends past that frame; a widget larger than it keeps its top-left corner in view. Widgets follow displays as they are connected, removed or rearranged, and are drawn at each display’s own scale. Two widgets at the same anchor overlap; stack them with different gap_y values.

Meters, sparklines and rules

Two Flash-only style markers turn numbers into Unicode block drawings. They are ordinary text afterwards, so they also work in the bar and in popups.

#[meter=W] … #[nometer] draws the first number of the enclosed text as a bar exactly W cells wide (1–200), in eighths of a cell (▏▎▍▌▋▊▉█). #[meter=W/MAX] scales to MAX instead of 100. Values clamp to 0…MAX. The empty part of the bar is spaces, so a bg= in the same marker draws the track:

Format Draws
[#[meter=10]37#[nometer]] [███▊ ]
[#[meter=10/8]2.5#[nometer]] [███▏ ]
#[meter=10 fg=green bg=colour236]#{flash.plugin.memory.percent}#[nometer default] A green bar on a grey track

#[spark] … #[nospark] draws every number of the enclosed text as one of ▁▂▃▄▅▆▇█, scaled from 0 to the largest of them — the same scale as the Rust SDK’s sparkline_scaled. #[spark=MIN/MAX] uses a fixed scale and clamps; with a single number it is a one-cell gauge:

Format Draws
#[spark]0 1536 48213 20000 3000 100 0#[nospark] ▁▁█▃▁▁▁
#[spark=0/100]12 18 23 40 55 30 20 10 95#[nospark] ▁▂▂▃▄▃▂▁▇
#[spark=0/100]85#[nospark] ▆
#[spark]#{flash.plugin.network.down_history}#[nospark] The last 20 download rates

The drawing replaces all the enclosed text, so labels and units go outside the markers. Text without a number is left as it was, so a meter over a value that is not published yet shows nothing rather than an empty bar. Numbers are ASCII decimals such as 42, -3 or 0.75; a - right after a digit is a separator, not a sign. A value split across several fragments or restyled inside the markers is read as one text, and the drawing takes the style of its first part. Only #[nometer] ends a meter and only #[nospark] a sparkline (#[default] does not); a new #[meter=…] or #[spark…] starts a new drawing. A drawing never spans a line break: each line inside the markers is drawn on its own. A malformed marker — meter=0, meter=201, meter=10/0, spark=5/5 — is ignored as a whole, like any invalid tmux style. Inside a #{?…} branch, separate style tokens with spaces (#[meter=10 fg=green]), since a comma ends the branch.

A horizontal rule needs no marker: tmux’s repeat operator draws #{R:─,#{flash.widget.columns}}, a line of ─ as wide as the widget.

Data

Everything the bar can show is available. These values suit widgets:

Value Meaning
#{flash.plugin.cpu.percent} Total CPU, integer 0–100.
#{flash.plugin.cpu.history} The last 20 CPU totals, oldest first, space-separated.
#{flash.plugin.cpu.load} One-minute load average, 3.47.
#{flash.plugin.cpu.uptime} Time since boot, sleep included, 3d 4h.
#{flash.plugin.memory.percent}, history Used memory 0–100, and its last 20 samples.
#{flash.plugin.disks.percent} Startup-volume usage 0–100.
#{flash.plugin.disks.read_bps}, write_bps Disk rates in whole bytes per second.
#{flash.plugin.disks.read}, write The same rates in binary units, 1.5 MiB/s.
#{flash.plugin.network.down_bps}, up_bps Default-route rates in whole bytes per second.
#{flash.plugin.network.down_history}, up_history Their last 20 samples.
#{flash.plugin.network.address} First IPv4 address of the default-route interface.
#{flash.plugin.power.percent} Battery charge 0–100; empty without a battery.
#{flash.plugin.power.state} charging, discharging, charged or ac.
#{flash.plugin.processes.top_cpu}, top_mem The busiest processes by CPU or resident memory, one row each.
#{flash.plugin.tmux.session}, window, pane The attached local tmux client’s session, window and pane; empty without one.
#{flash.calendar} The 75-column calendar: three months, the day’s progress, time zone, DST and moon.
#{flash.source.<name>} The output of a named source.
#{flash.history.<name>} The last N numeric outputs of a source with history = N, oldest first.
#(command) A tmux shell job, re-run every widget interval.
#{flash.widget.name} The widget’s table name.
#{flash.widget.columns} Its width in cells: columns, or max_columns when the widget sizes itself.

The plugin values are described in status plugins; an empty value means unknown, not zero, so a meter over it draws nothing. [plugin.processes] top_count = 5 sets the rows of each process table (1–20); the plugin samples processes only while a surface shows one of the tables (top processes).

A named source runs a command on its own cadence. history = N (2–512) keeps the last N outputs that parse as a number; it survives while nothing shows the source, so a widget that reappears still has its past, and changing the source’s definition starts it afresh. It cannot combine with cycle_interval.

[statusbar.sources.load]
command = ["/bin/sh", "-c", "sysctl -n vm.loadavg | awk '{print $2}'"]
interval = 10
history = 30
load #[spark]#{flash.history.load}#[nospark] #{flash.source.load}

Keys

Key Default Meaning
enabled true Show the widget.
template — The status format; required.
screen "primary" "primary" (the menu-bar display), "all", or a display number counted left to right from 1.
anchor "top_left" top_left, top_centre, top_right, centre_left, centre, centre_right, bottom_left, bottom_centre, bottom_right.
gap_x, gap_y 24 Points from the anchored edges; ignored along a centred axis.
columns 0 Width in cells; 0 fits the widest line, up to max_columns.
max_columns 120 Cap for a widget that sizes itself.
font "" An installed monospaced font; empty, unknown or proportional falls back to the system monospaced font (with a warning).
font_size 13 Points, 6–200.
line_spacing 0 Extra points between lines.
fg "#D8DEE9" Default text colour, #RRGGBB.
bg "#2E344000" Background, #RRGGBB or #RRGGBBAA; transparent by default.
border, border_size "#00000000", 0 Border colour and width in points.
corner_radius 8 Points.
padding 8 Points between the edge and the text.
interval 0 Seconds between #() re-runs; 0 follows [statusbar] interval. Time refreshes on its own boundaries.
hide_from_capture false Ask the window server to leave the widget out of screenshots, recordings and screen sharing. Best effort, as for [overlay] screen_capture, which also applies.

The whole format is written inline in template; there is no [widgets.<name>.options] table.

Refresh and cost

Widgets are evaluated by the status bar’s controller, beside the bar: one set of sources and jobs, one clock deadline on the shared PollScheduler, and no timer of their own. A source shown by the bar and a widget runs once. Each surface is re-evaluated only when a value it read changed. A job shown by several surfaces refreshes at the fastest of their intervals. Time is never polled: every surface shares one clock tick, at the next boundary of the finest unit any of them shows (see status formats), so a seconds widget ticks every second and a date-only one at midnight. Meters and sparklines are drawn while the document is parsed, with no extra evaluation.

A widget whose windows are all covered — a maximized or full-screen app over the desktop — stops refreshing: its sources, jobs and clock no longer count until a window is visible again. After 30 seconds covered, it also stops observing plugin segments, so a status plugin only it shows stops sampling; the delay keeps briefly covering the desktop from respawning plugins. macOS reports that coverage per window; a widget whose window has never been reported visible is treated as visible, so a display that does not report occlusion keeps its widgets live. The budget and how to measure it are in performance.

Coming from conky

conky Flash
own_window_type desktop, own_window_hints below Built in: widgets sit on the desktop below every window.
alignment top_right, gap_x, gap_y anchor = "top_right", gap_x, gap_y
xinerama_head screen, a display number from 1, left to right
update_interval 1 interval = 1 (plugin values update as they are published)
use_xft, font font, font_size (monospaced; one per widget)
default_color, own_window_colour, own_window_argb_value fg, bg = "#RRGGBBAA"
${color red}…${color} #[fg=red]…#[default]
${alignr}, ${alignc} #[align=right], #[align=centre]
${hr} #{R:─,#{flash.widget.columns}}
${time %H:%M} %H:%M in the template (it is strftime-expanded)
${cpu} #{flash.plugin.cpu.percent}
${cpubar} #[meter=20]#{flash.plugin.cpu.percent}#[nometer]
${cpugraph} #[spark]#{flash.plugin.cpu.history}#[nospark]
${memperc}, ${membar} #{flash.plugin.memory.percent}, #[meter=20]#{flash.plugin.memory.percent}#[nometer]
${fs_used_perc /}, ${fs_bar /} #{flash.plugin.disks.percent}, #[meter=20]#{flash.plugin.disks.percent}#[nometer]
${diskio_read}, ${diskio_write} #{flash.plugin.disks.read}, #{flash.plugin.disks.write}
${loadavg 1}, ${uptime_short} #{flash.plugin.cpu.load}, #{flash.plugin.cpu.uptime}
${downspeed}, ${downspeedgraph} #{flash.plugin.network.down_bps}, #[spark]#{flash.plugin.network.down_history}#[nospark]
${addr} #{flash.plugin.network.address}
${battery_percent}, ${battery_bar} #{flash.plugin.power.percent}, #[meter=20]#{flash.plugin.power.percent}#[nometer]
${top name 1} … #{flash.plugin.processes.top_cpu} (top_mem for ${top_mem}), [plugin.processes] top_count rows
${exec cmd} #(cmd)
${execi 60 cmd} [statusbar.sources.x] with command = [...] and interval = 60, shown as #{flash.source.x}
${execgraph …} A source with history = N, drawn with #[spark]#{flash.history.x}#[nospark]
${tail f 5} A source with command = ["tail", "-n5", "f"]
${if_…}…${else}…${endif} #{?condition,then,else}, with #{==:…}, #{>:…} and #{m:…} comparisons
template0 … ${template0 a b} No templates: write each row out in template, or print generated rows from a source script

Out of scope