Skip to content

Customization

Tessa aims to be highly customizable without embedding a JavaScript/TypeScript runtime or a native plugin VM. Customization has three layers:

  1. Declarative configuration — key bindings, window rules, layout tuning, colors, includes, and live reload.
  2. A process extension surface — hooks, structured events, status entries, the launcher, and the Unix-socket IPC used by tessa-ctl.
  3. Protocols for external policy engines — the long-lived layout-helper and effect-helper protocols are available now, including per-window tint and animation overrides. The design goal is that a policy engine can live in any language without loading code into the compositor.

The flat key = value format is intentional: it keeps the parser small, supports positional includes, and makes effective values and parsed rules inspectable with tessa-ctl config.

A window rule is one line. Match clauses and actions are separated by ;:

window-rule = app-id=pavucontrol; float=true
window-rule = title=Picture-in-Picture; float=true; position=40,40
window-rule = app-id=firefox; workspace=2
window-rule = title=GIMP*; float=false
window-rule = match=app ~ "^firefox$" && title !~ "Private|Incognito"; workspace=2
window-rule = title=Chat*; border-active=#4cc9f0; border-inactive=#1b2a41
window-rule = app-id=mpv; opacity=0.9; border-width=0; corner-radius=0
window-rule = title=Picture-in-Picture; opacity=0.85; corner-radius=14; blur=true

Match clauses:

Key Meaning
app-id, app_id, class xdg application id
title window title
output, monitor output name
match, expr boolean expression using POSIX extended regexes

Expression fields are app / app-id / class, title, and output / monitor. Operators are ==, !=, ~, !~, && / and, || / or, ! / not, and parentheses. Values may be quoted to include spaces. Simple match keys and match are combined with AND.

Patterns support shell-style * and ? wildcards and are case-sensitive. * matches any sequence, including an empty one:

window-rule = title=*Picture-in-Picture*; float=true
window-rule = app-id=org.example.*; workspace=3

Action clauses:

Key Meaning
float=true/false open floating or tiled
tile=true/false inverse of float
fullscreen=true/false open fullscreen
workspace=1..9 assign the window to a workspace
size=WxH floating size, for example size=900x600
width=N, height=N floating size, one axis at a time
position=X,Y floating position
border-active=#RRGGBB per-window focused border
border-inactive=#RRGGBB per-window unfocused border
opacity=0.0..1.0 window and decoration opacity
corner-radius=0..64 per-window rounding (rounding alias)
blur=true/false per-window backdrop blur
shadow=true/false per-window layered shadow
border-width=0..16 per-window border thickness; 0 hides it
tint=#RRGGBBAA / tint=none composited tint overlay (blocks direct scanout while visible)
tearing=true/false per-window tearing preference
scratchpad=true/false place the window on the scratchpad
special=NAME / workspace=special:NAME place the window on a special workspace
animation=true/false enable or suppress open/workspace/close motion
animation-ms, animation-open-ms, animation-workspace-ms, animation-close-ms per-window durations (-1 inherits)
animation-curve, animation-open-curve, animation-workspace-curve, animation-close-curve per-window curves

size and position imply floating unless the same rule says float=false. Invalid expressions or regular expressions are config errors with file/line diagnostics. Border overrides, opacity, rounding, border width, and shadow state are also used by close snapshots. corner-radius and blur require a scenefx build (the default; disabled with -Dscenefx=false); the other effects work everywhere.

Rules are evaluated in file order; a later matching rule can override an earlier one. Structural actions (float, workspace, size, position, fullscreen) run when a window maps and are re-evaluated for existing windows on Super+R / tessa-ctl dispatch reload. Decorative actions (border-*, opacity, corner-radius, blur, shadow, border-width) also react immediately when the title or app-id changes.

Values may be quoted when they contain spaces. A ; inside a value is not supported in the flat format.

Layer-shell clients such as bars, notification daemons, and wallpaper providers are configured with layer-rule:

layer-rule = namespace=tessa-bar; opacity=0.94; corner-radius=12; blur=true
layer-rule = namespace=tessa-notifications; layer=overlay; opacity=1; blur=false
layer-rule = namespace=my-wallpaper; layer=background; opacity=1; blur=false
Clause Meaning
namespace= / ns= layer namespace pattern
output= / monitor= output name pattern
layer= background, bottom, top, overlay, or 0..3
opacity=0.0..1.0 layer surface opacity
corner-radius=0..64 layer surface rounding
blur=true/false backdrop blur override

Later matches win. Without a rule, opacity is 1, corners are square, and blur follows the global setting for top/overlay layers only. Layer rules apply on map and on config reload. Effective values are visible in tessa-ctl state / state --json; parsed rules are listed by tessa-ctl config / config --json. Opacity works in every build; rounding and blur require a scenefx build (the default; disabled with -Dscenefx=false).

Key Default Meaning
gaps 10 gap between tiles
outer_gaps 16 gap around the tiling area
master_ratio 0.58 master-column width ratio
master_count 1 windows stacked in the master column
mosaic_ratio 0.62 slice ratio for the mosaic spiral
mosaic_min_size 160 minimum tile before mosaic falls back to grid
grid_columns 0 fixed grid columns; 0 uses ceil(sqrt(n))
layout master default layout (master, mosaic, grid, columns, scrolling, monocle, stacking, or custom)
layout_1 … layout_9 inherited initial layout per workspace

Runtime layout choices are stored per output and workspace in outputs.conf. A config reload updates workspaces that have no runtime override. layout_N only seeds workspaces without a saved or runtime choice.

workspace-rule overrides layout and spacing per workspace:

workspace-rule = ws=2; layout=grid; gaps=4; outer-gaps=8; master-ratio=0.65
Clause Meaning
ws=1..9 target workspace (required)
layout=master/mosaic/grid/columns/scrolling/monocle/stacking/custom initial layout seed
gaps=N gap between tiles on this workspace
outer-gaps=N outer gap on this workspace
master-ratio=0.15..0.9 master-column width on this workspace

output-rule overrides layout tuning for one monitor, identified by connector name or EDID metadata, optionally narrowed to a single workspace:

output-rule = output=HDMI-A-1; layout=grid; gaps=8; outer-gaps=12
output-rule = output=HDMI-A-1; ws=2; layout=master; master-ratio=0.62
output-rule = make=Dell Inc.; model=DELL*; adaptive-sync=on
output-rule = serial=ABC123; adaptive-sync=off
Clause Meaning
output= / monitor= connector name pattern
make= / vendor= EDID manufacturer pattern
model= EDID model pattern
serial= / edid-serial= EDID serial pattern
ws=1..9 optional workspace; omitted means every workspace
layout=master/mosaic/grid/columns/scrolling/monocle/stacking/custom layout seed
gaps=N, outer-gaps=N tile and outer spacing
master-ratio=0.15..0.9, master-count=1..8 master tuning
mosaic-ratio=0.25..0.85, mosaic-min-size=N mosaic tuning
grid-columns=0..8 fixed grid columns; 0 is automatic
profile=NAME apply a named tuning profile first
adaptive-sync=true/false / vrr= request VRR for this output

At least one match clause (output, make, model, or serial) is required. Any number may be combined; every field that is present must match. Patterns support * and ?, which is useful when a connector name changes between docks or boots.

Output rules also accept theme keys: accent, bg, border-active, border-inactive, panel-bg, panel-fg, bg-top, bg-bottom, bar-height, bar-top-margin, and bar-side-margin. The panel applies the rule matching its own output, so per-monitor colors and dimensions stay in sync. Use hyphens or underscores; both are accepted.

adaptive-sync is applied only to rules without ws=. Tessa checks whether the output advertises adaptive sync and logs the commit result. Removing the rule restores the state the output had before Tessa applied a policy. Some monitors need a VRR-capable mode and connector; check the Tessa log if enabling VRR has no effect.

Rules are applied in file order. Precedence is global defaults < output rule (all workspaces) < workspace-rule < output rule with ws=. A runtime layout choice still wins over the seed and is persisted.

Named profiles group layout/tuning values:

profile = compact; gaps=0; outer-gaps=0; master-ratio=0.5; master-count=2
profile = grid3; layout=grid; grid-columns=3; gaps=6; outer-gaps=2
output-rule = output=DP-1; profile=compact; gaps=4
workspace-rule = ws=2; profile=grid3

Profiles may be declared before or after the rules that reference them. Explicit rule keys override profile values. Profiles cover layout/spacing, not theme colors. Unknown profile references reject the reload. Workspace profiles support layout, gaps, outer-gaps, master-ratio, master-count, mosaic-ratio, mosaic-min-size, and grid-columns.

The Super+Ctrl+H/L ratio binding adjusts the active output/workspace ratio for the current session; config reload restores configured ratio values.

Workspaces can be bound to monitors so the shortcut always brings that monitor forward:

workspace_pin = eDP-1 1
workspace_pin = HDMI-A-1 3

Keyboard, gesture, IPC, and bar switches to a pinned workspace redirect to its output. Super+Shift+<n> moves the focused window to that workspace, changing its output when the workspace is pinned elsewhere. A missing output releases the pin until that output appears. tessa-ctl workspace <n> <output> still names the requested monitor, but a pin takes precedence.

Workspaces can also have names for the bar and for commands:

workspace-name = 2 Code
workspace-name = 3 Chat
bind = SUPER+2 workspace Code
bind = SUPER+SHIFT+2 move-to-workspace Code
window-rule = app-id=org.mozilla.firefox; workspace=Chat

Names are global, unique, single tokens, and cannot be numeric. They resolve after the full config is parsed, so declaration order is flexible. The bar pill shows the name (long names are shortened), while tessa-ctl workspace Code and tessa-ctl dispatch move-workspace Code use the same alias. Windows still store a numeric workspace internally; modes, pins, gestures, and persistence keep working by number. Window-rule workspace= accepts either form.

Each output has a hidden floating scratchpad:

window-rule = app-id=org.example.notes; scratchpad=true
# window-rule = app-id=org.example.notes; workspace=special
bind = SUPER+grave toggle-scratchpad
bind = SUPER+SHIFT+grave move-to-scratchpad

toggle-scratchpad shows or hides the focused output’s scratchpad and focuses its first window when opening. move-to-scratchpad sends the focused window into the scratchpad, or returns a scratchpad window to normal tiling. Switching workspaces hides the scratchpad. The scratchpad is an overlay of floating windows, not an extra numbered workspace, so it does not affect workspace occupancy or Super+1..9.

Window groups turn the tiled windows on a workspace into one tabbed tile:

bind = SUPER+SHIFT+g toggle-group
bind = SUPER+ALT+g group-add
bind = SUPER+ALT+SHIFT+g group-remove

toggle-group groups all tiled windows on the active workspace, or ungroups the whole group when a member is focused. While grouped, only the active member is visible and Super+Tab / Super+Shift+Tab cycle tabs instead of moving to another tile. group-add joins the focused window to a group on the same workspace; group-remove detaches it. Groups are runtime state, are not persisted, and are dropped when a member moves to another workspace or the scratchpad. IPC commands mirror the actions: toggle-group, group-next, group-prev, group-add, and group-remove.

Mosaic is deliberately bounded: when the configured minimum tile size cannot be maintained, the whole workspace uses the grid layout instead of overlapping or producing unusable slivers.

Custom tiling lives in long-lived helper processes, one per output. Select it with layout = custom, point layout_helper at a command, and bind it if you want it outside the Super+T cycle:

layout = custom
layout_helper = python3 ~/.config/tessa/layout.py
layout_helper_timeout = 100
bind = SUPER+SHIFT+T set-layout custom

Tessa sends HELLO<TAB>2 once, then asynchronous requests describing the usable area and the tiled windows. It never blocks the compositor waiting for the reply. After a helper announces VERSION<TAB>2, requests look like this ( means a literal tab):

HELLO 2
LAYOUT 2
OUTPUT HEADLESS-1
AREA 0 0 1920 1080
OUTER_GAP 16
GAP 10
BORDER 2
SCALE 1.000
TRANSFORM 0
COUNT 2
WINDOW 7 foot notes 1 0
WINDOW 8 kitty shell 0 1
END

The last two WINDOW fields are focused and urgent (1 or 0). Urgency can be set from a script with tessa-ctl dispatch urgent <id> on (or off); focusing a window clears it. State JSON exposes it as "urgent".

The helper replies with one rectangle per requested window:

VERSION 2
RECT 7 16 16 940 1048
RECT 8 966 16 938 1048
END

VERSION 2 is optional. Without it the helper is treated as protocol 1; old helpers that ignore unknown lines keep working. The first request uses the protocol-1 three-field WINDOW form; focused/urgent fields are added once the helper has announced VERSION 2. Coordinates are absolute logical compositor coordinates. Rectangle width and height are frame sizes including the border. Floating and fullscreen windows are not sent. A response must contain exactly one rectangle for every request window and may not contain unknown ids. Return ERR instead of rectangles to reject the frame.

Each output gets its own process and rectangle cache. Requests are sent when the layout, focus, or urgency changes. While a reply is outstanding, the previous rectangles stay in place and further changes coalesce into the next request. Until the first successful reply, Tessa uses the built-in layout. A missing command, timeout, crash, malformed response, or rectangle mismatch falls back to the master layout for that output. The helper is retried lazily on the next request after one second; there is no dedicated retry timer. Restarting or changing layout_helper restarts the helpers; a config reload with the same command keeps them alive.

The request is limited to 64 tiled windows. The default timeout is 100 ms and the allowed range is 10–1000 ms. stdout is reserved for the protocol; write diagnostics to stderr. The command runs as your user through /bin/sh, like a hook. See examples/layout-cascade.py for a minimal working helper.

Modes group bindings and are entered with the enter-mode action:

mode = resize
bind = h resize-width -30
bind = l resize-width 30
bind = Escape enter-mode default
mode = default
bind = SUPER+SHIFT+r enter-mode resize

mode = <name> changes the parse context for following bind lines until the next mode line. Names may contain letters, digits, _, and -. Includes restore the caller’s mode after returning.

Mode bindings are matched even when no modifiers are held; global (default) bindings still work, and keys with no match reach the focused client. This makes modal resize, navigation, or launcher-like keymaps possible without a separate compositor. tessa-ctl mode <name> switches from a script, and the active mode is exposed as MODE <name> in tessa-ctl state, "mode" in state JSON, "modes" in config JSON, and the mode-change event.

Leader keys and per-mode timeouts extend named modes:

leader = SUPER+SPACE
leader_timeout = 1000
mode = leader
bind = f spawn firefox
bind = t spawn kitty
bind = Escape enter-mode default
mode = resize
mode_timeout = 300
bind = h resize-width -30
bind = l resize-width 30

Press the leader in the default mode to enter leader; a matching binding runs one action and returns to default unless it enters another mode. Pressing the leader again, Escape, or leader_timeout exits. leader_timeout = 0 keeps leader mode active. mode_timeout applies to the mode section it appears in and returns to default; 0 disables it.

Event rules filter lifecycle events with the same expression language and run a shell command:

event-rule = window-open; match=app ~ "^firefox$" && ws == "2"; run=notify-send "Firefox" "Opened on 2"
event-rule = window-open; match=id ~ "^[0-9]+$" && title == "Mail"; run=touch ~/.local/state/mail
event-rule = workspace-change; match=ws == "3"; run=~/bin/on-workspace-3

The first clause is the event name (or use event=). match/expr is optional; run/command is required. Expressions additionally expose event, ws, and id, compared as strings. Commands receive the hook environment (TESSA_EVENT, TESSA_WINDOW_ID, TESSA_WORKSPACE, TESSA_TITLE, TESSA_OUTPUT, TESSA_ARG, …). Matching event rules run in config order before hook commands. Like hooks, they run through /bin/sh -c with the session user’s privileges.

Configuration is parsed with file and line tracking. Unknown keys, invalid numbers/booleans/colors, malformed rules, and missing includes are reported instead of being silently ignored:

Terminal window
tessa-ctl config-check

Super+R / tessa-ctl dispatch reload rejects a configuration with errors and keeps the previously active one. tessa-ctl config shows the effective values plus diagnostic error <path>:<line>: <message> lines. config-check exits non-zero when errors remain.

For tools, both snapshots have a schema version and are available as JSON:

Terminal window
tessa-ctl state --json
tessa-ctl config --json

State JSON contains schema: 1, outputs, occupancy, focused id, statuses, and window geometry. Config JSON contains scalar settings, per-workspace layouts and tuning, rules, hooks, event rules, profiles, theme roles, and diagnostics. The full wire format is in EXTENDING.md.

The same socket supports JSON-RPC 2.0 for read access and event subscriptions:

Terminal window
tessa-ctl rpc '{"jsonrpc":"2.0","id":1,"method":"hello"}'
tessa-ctl rpc '{"jsonrpc":"2.0","id":7,"method":"state"}'
tessa-ctl rpc '{"jsonrpc":"2.0","id":"cfg","method":"config"}'

hello negotiates capabilities (state, config, events, subscriptions, request-ids, extensions, effects2, protocol version 1) and reports tessa, extension_api, layout_api, and effect_api. subscribe streams lifecycle events as JSON notifications with the request id in params.subscription. Mutation still uses the line commands.

libinput settings are configured with repeatable input (or input-rule) lines. Match by device type and/or a wildcard device name; matching rules are merged in file order:

input = type=touchpad; tap=true; tap-drag=true; natural-scroll=true
input = type=touchpad; name=*Precision*; accel-profile=flat; accel-speed=-0.3
input = type=mouse; name=*Mouse*; natural-scroll=false; scroll-method=button; scroll-button=275
input = type=touchpad; dwt=false; click-method=clickfinger
input = type=pointer; left-handed=true
Clause Meaning
type=any/keyboard/pointer/touchpad/mouse/touch/tablet/tablet-pad/switch device class; default any
name= / device= libinput device-name pattern; * and ? supported
tap=, tap-drag=, tap-drag-lock= touchpad tap and tap-drag behavior
natural-scroll=, left-handed=, middle-emulation= pointer scroll/direction/emulation
dwt= / disable-while-typing= disable the touchpad while typing
accel-profile=adaptive/flat, accel-speed=-1.0..1.0 pointer acceleration
scroll-method=none/two-finger/edge/button/circular pointer/touchpad scrolling
scroll-button=1..767 evdev button used by scroll-method=button
click-method=none/button-areas/clickfinger touchpad click behavior
tap-button-map=lrm/lmr, clickfinger-button-map=lrm/lmr button order

A pointer that supports tap gestures is classified as a touchpad; other pointers are mice. Touchpad-only clauses are ignored on a mouse, and pointer clauses that the device does not support are skipped. Every device’s original libinput state is captured before Tessa applies a rule, so config reload can remove an override and return to the original state. Device names are shown by libinput list-devices.

Cursor appearance uses two global keys:

cursor_theme = Adwaita
cursor_size = 24

cursor_theme is an Xcursor theme name; an empty value uses the system default. cursor_size is clamped to 16–96 logical pixels. Reload applies both immediately. If the named theme fails to load, Tessa logs a warning and keeps the previous cursor.

Key Meaning
gestures enable compositor swipe gestures (default true)
gesture_fingers finger count for shell swipes, 2–5 (default 3)
gesture_workspace_threshold horizontal distance needed for a workspace switch (default 400)
gesture_launcher_threshold upward distance needed for the launcher (default 300)
touch accept touch input devices (default true)
tablet enable tablet-v2 tools and pads (default true)
pointer_constraints enable pointer-constraints-v1 (default true)
xim export XMODIFIERS for X11 clients (default true)
xim_server XIM server name; empty disables the variable (default fcitx)
xim_start optional XIM server command started after the X display appears

A swipe with exactly gesture_fingers fingers is handled by the shell when no keyboard-focused layer surface is open. Swiping left goes to the next workspace, swiping right to the previous one, and swiping up opens the launcher. Set gestures = false to forward every gesture to clients. Gestures with other finger counts, plus pinch and hold, are always sent to the focused client through pointer-gestures-v1. Tune the thresholds in 20–1000; smaller values react sooner.

Touch devices are attached to the shared cursor and routed through scene hit-testing. Tapping a client focuses it; the panel accepts touch directly. Tablet-v2 tools receive proximity, tip, pressure, distance, tilt, rotation, slider, wheel, and button events, and fall back to pointer clicks on surfaces that do not support tablet-v2. Tablet pads forward buttons, rings, strips, and mode changes to the keyboard-focused surface.

Pointer constraints are activated when their surface receives keyboard focus and deactivated on a focus change. Locked pointers stop cursor movement while relative motion continues to reach the client; confined pointers are clipped to the constraint region. Set pointer_constraints = false to ignore constraint requests.

For X11 input methods, Tessa exports XMODIFIERS=@im=<xim_server> once XWayland is running. xim_start is optional and is started only after the X socket exists, which avoids starting Fcitx before its display is ready:

xim = true
xim_server = fcitx
xim_start = fcitx5 -d

If Fcitx or IBus already starts from startup, leave xim_start empty. XIM settings are applied at compositor startup; restart Tessa after changing them. Use xim = false when another input method stack manages XMODIFIERS.

Key Meaning
accent accent used by borders, panel, and launcher
bg wallpaper/background color
border_active focused-window border color; empty means use accent
border_inactive unfocused border color
border_width border thickness in logical pixels
font panel/launcher family
bar_height bar height in logical pixels (32–96)
bar_top_margin distance from the top edge (0–64)
bar_side_margin left/right margin (0–128)
panel_bg bar background color
panel_fg bar primary text color
bg_top top color of the wallpaper gradient; unset derives it from bg
bg_bottom bottom color of the wallpaper gradient; unset derives it from bg
wallpaper_art draw the decorative wallpaper art (default true)
wallpaper_image PNG wallpaper path (absolute or ~/...); replaces gradient/art
wallpaper_fit cover (default), contain, or stretch
animations animate window open, close, and workspace transitions (default true)
animation_ms default motion duration in milliseconds, 0–1000 (default 160)
animation_open_ms, animation_workspace_ms, animation_close_ms per-kind duration; -1 inherits (default -1)
reduce_motion disable movement animations (default false)
reduce_motion_animations none or fade for a close-only fade (default none)
reduce_motion_fade_ms reduced-motion close fade duration (default 120)
shadows draw layered window drop shadows (default true)
corner_radius rounded window radius in logical pixels, 0–64 (default 10; scenefx builds)
blur backdrop blur for translucent clients and shell layers (default true; scenefx builds)
blur_radius blur radius, 1–32 (default 6)
blur_passes blur quality passes, 1–5 (default 3)
power_lock command for the power menu’s Lock action (default loginctl lock-session)
power_suspend command for Suspend (default systemctl suspend)
power_reboot command for Reboot (default systemctl reboot)
power_off command for Power Off (default systemctl poweroff)
screenshot_dir screenshot PNG directory (default ~/Pictures/Screenshots)
volume_status optional status command (prints a percentage); empty auto-detects wpctl/pactl/amixer
volume_up optional volume-up command override
volume_down optional volume-down command override
volume_mute optional mute-toggle command override

The panel rereads the palette, launcher entries, bar geometry, and power commands when the compositor emits config-reload. A geometry change recreates the bar layer surface, so the reserved area follows immediately. bg_top, bg_bottom, wallpaper_art, and wallpaper_image repaint the panel background without a restart. Theme packages are loaded through theme; see below.

bar_layout selects and orders the right-hand cluster (the first name is closest to the right edge). The logo, workspace pills, and window chips keep their anchored positions. tessa-panel --list-modules prints the registry, including whether a module exposes compositor context.

bar_layout = logo, workspaces, chips, layout, groups, mode, effects, clock, tray, status

When the right cluster does not fit, the panel hides modules from the lowest bar_priority upward and draws a +N chip. Clicking the chip lists the hidden modules and their priorities. Defaults put the clock, status widgets, and workspaces first, and stats/media/tray last:

# Higher values stay visible longer. Names match bar_layout.
bar_priority = clock=100, status=80, layout=60, groups=55, mode=65, effects=30
bar_priority = stats=20, media=40, tray=35, battery=50, network=50, volume=60
# bar_overflow = false # disable priority hiding and the +N chip

The planner uses approximate module widths and also reserves room for the +N chip; those hard minimum widths (for example stats below 1180 px and media below 1250 px) still apply. To preview decisions for a given bar width without opening a session at that size:

Terminal window
# width, then the left-side space reserved for the logo/chips
tessa-panel --bar-plan 850 200

The command prints visible, hidden, and skip ... reason=minimum-width lines with each module’s priority and estimated cost. External status widgets have their own --priority and +N overflow inside the status module; the bar planner treats the whole module as one unit.

wallpaper_image is loaded with Cairo’s PNG support. cover fills the output and crops the excess, contain letterboxes onto bg, and stretch ignores the aspect ratio. A missing or invalid image logs an error and falls back to the gradient/art.

Semantic roles are available for colors shared by the compositor and panel:

theme-role = accent = #e2684f
theme-role = window.border.active = #8fd3ff
theme-role = window.border.inactive = #243040
theme-role = window.background = #0a0f1a
theme-role = panel.background = #0e1320
theme-role = panel.foreground = #eef2f8
theme-role = wallpaper.top = #162038
theme-role = wallpaper.bottom = #070a12

Roles are applied after direct keys and override them; a matching per-output theme rule still wins for that output. The panel reads the same roles.

A theme package is a normal include, addressed with the theme alias:

theme = ~/.config/tessa/themes/vermilion-ink

theme behaves exactly like include; the compositor and panel share the same file. A directory package loads <dir>/theme.conf (other files are ignored for forward compatibility). The config watcher reloads the active package when it changes, follows a theme change to a different package, and Super+R reloads it manually. See examples/themes/vermilion-ink/ for a complete palette, and tessa-panel --theme-json to inspect the resolved tokens.

Windows slide in from a configurable offset when mapped, and workspace switches slide the incoming workspace horizontally. Both use named cubic-bezier or spring curves (built-ins, bezier = name, x1, y1, x2, y2, or spring = name, damping, frequency) over their per-kind duration (animation_open_ms / animation_workspace_ms, falling back to animation_ms). A window closed through the compositor (a key binding, bar chip, IPC, or another client) captures its surface and scales/fades that snapshot with animation_close_curve over animation_close_ms. A client that closes itself disappears immediately because its buffer is already gone. reduce_motion = true disables open/workspace movement while leaving other settings intact; animations = false disables everything. animation_ms = 0 sets the fallback duration to zero, but explicit per-kind or per-window durations still apply. With reduced motion, reduce_motion_animations = fade keeps only a close fade without scale or slide, and none disables close effects. Popover animation is controlled separately by the panel.

Window rules can opt individual windows in or out and override the duration or curve:

spring = bounce, 0.35, 1.4
window-rule = app-id=kitty; animation=false
window-rule = title=Picture-in-Picture; animation-ms=260; animation-curve=spring-soft; animation-close-ms=180; animation-close-curve=bounce

animation-curve covers open and workspace transitions; close uses animation-close-curve. A spring’s damping is 0.05–2 and its frequency is 0.25–4 oscillations over the animation; lower damping overshoots more, and the curve always ends at 1.

Shadows are three translucent scene rectangles behind each client frame. They follow the border geometry and are disabled with shadows = false.

In a scenefx build (the default), corner_radius rounds client content, the border, and shadow layers; fullscreen windows stay square. blur enables backdrop blur on translucent windows and on the top/overlay/popup layer surfaces, using blur_radius and blur_passes. Both apply immediately on config reload. These keys are accepted but have no visual effect in the default wlroots-scene build.

The wallpaper is a vertical gradient. Set bg_top and bg_bottom to override the derived default; either key can be omitted independently. Decorative shapes (the accent wash, ink blots, sun, and tessellation) are drawn only when wallpaper_art = true.

Click the clock to open the month calendar. Left/Right, the arrow buttons, or the scroll wheel change the month; Home returns to today. The power menu opens by right-clicking the TESSA mark. It offers Lock, Suspend, Reboot, Power Off, and Quit Tessa; the first four run the power_* commands above. Up/Down and Enter work from the keyboard, and clicking outside closes either popover.

Both popovers can also be opened or toggled by an extension:

Terminal window
tessa-ctl panel calendar
tessa-ctl panel power
tessa-ctl panel power DP-1

Opening a popover emits the panel event with arg=calendar or arg=power and the output name, so hooks can observe or drive the shell as well.

Super+V opens the clipboard history, and tessa-ctl panel clipboard can open it from an extension or another binding. The panel uses the compositor’s wlr-data-control-v1 global to observe selections without requiring keyboard focus. History keeps the latest eight distinct text entries (up to 16 KiB each) in memory only; it is never written to disk. Selecting an entry promotes it to the top and makes the panel the new selection source, so any application can paste it with its normal paste command.

clipboard is also available as a key-binding action:

bind = SUPER+v clipboard
bind = SUPER+SHIFT+v clipboard

Super+Shift+S (or tessa-ctl panel screenshot) captures the focused output with wlr-screencopy. Drag to select a region, click for the full output, or press Enter for the full output; Esc or right-click cancels. The panel draws the selection on an overlay, punches the selected area out of the dim layer, and shows the physical pixel size while dragging.

PNGs are written to screenshot_dir:

screenshot_dir = ~/Pictures/Screenshots

That is the default. Subdirectories are created as needed. Files are named tessa-YYYYMMDD-HHMMSS-<ms>.png, where the millisecond field is unpadded (for example tessa-20261103-020304-42.png). The capture is taken before the overlay is shown, so the selection UI and dim layer are not included. The bar is part of the output and is included unless you select a region below it. screenshot is also a key-binding action:

bind = SUPER+SHIFT+s screenshot

When a battery is present under /sys/class/power_supply, the bar shows its charge and colors the fill while charging. The network indicator reads the default route from /proc/net/route and the interface state from /sys/class/net; it shows Wi-Fi when the interface has a wireless attribute and LAN otherwise. Both refresh every five seconds and need no configuration.

Tessa watches MPRIS on the session bus. The first player discovered contributes a bar chip with the current title and play/pause state. Click the chip, press Super+M, or run tessa-ctl panel media to open the controls: previous, play/pause, and next. In the popover, Space toggles playback and Left/Right skip. The MPRIS client is read-only for metadata; album art is not displayed. Use media as a key-binding action, for example:

bind = SUPER+M media
bind = SUPER+SHIFT+m media

The panel owns org.kde.StatusNotifierWatcher and hosts registered tray items. Icons from IconPixmap or IconName appear in the bar. Left-click activates an item; right-click or Super+Shift+T opens a popover with the item titles and statuses. Up/Down selects, Enter activates, and Esc closes. Items can also be opened with:

Terminal window
tessa-ctl panel tray

tray is available as a key-binding action:

bind = SUPER+SHIFT+t tray

DBusMenu context menus and secondary activation are not implemented yet. If a different watcher already owns the D-Bus name, the tray service logs the failure and the rest of the panel keeps running.

When wpctl, pactl, or amixer is available, the panel shows the default output volume in the bar. Click it or press Super+Shift+A to open a compact volume bar just below the volume indicator without dimming the desktop. Up/Down changes the volume, m toggles mute, and Esc closes. tessa-ctl panel audio opens the same bar, and audio is a key-binding action:

bind = SUPER+SHIFT+a audio

The default commands are:

  • wpctl get-volume @DEFAULT_AUDIO_SINK@, wpctl set-volume ... 5%+/-, and wpctl set-mute ... toggle for PipeWire
  • pactl get-sink-volume, pactl set-sink-volume, and pactl set-sink-mute for PulseAudio
  • amixer get Master and amixer sset Master ... for ALSA

Override any part with panel settings. A custom status command should print a percentage and may contain MUTED/[off]; custom commands run through /bin/sh -c:

volume_status = ~/.local/bin/my-volume-status
volume_up = ~/.local/bin/my-volume up
volume_down = ~/.local/bin/my-volume down
volume_mute = ~/.local/bin/my-volume mute

The compositor exposes the protocols xdg-desktop-portal-wlr needs: zwlr_screencopy_manager_v1, linux-dmabuf when the renderer supports it, ext-output-image-capture-source-manager-v1, ext-image-copy-capture-v1, and both foreign-toplevel list protocols. The repository ships packaging/portal/tessa.portal; system packages install it under /usr/share/xdg-desktop-portal/portals/. It selects the wlr backend for XDG_CURRENT_DESKTOP=Tessa.

Install xdg-desktop-portal, xdg-desktop-portal-wlr, and a running PipeWire server. If xdpw’s automatic chooser is inconvenient, select an output directly:

~/.config/xdg-desktop-portal-wlr/config
[screencast]
chooser_type = none
output_name = HDMI-A-1
max_fps = 30

Monitor screencasting is covered end to end by tests/portal.sh. Window sources depend on the portal backend’s support for the image-capture-source protocols; Tessa currently advertises output capture sources.

The panel implements the org.freedesktop.Notifications D-Bus service on the session bus, so applications can use it without a separate daemon. Up to four toasts stack below the bar. They support replacement by notification id, timeout (0 keeps a notification until dismissed), CloseNotification, and click-to-dismiss. Notifications marked with an expiry time remove themselves and emit the corresponding signal.

Advertised capabilities are actions, body, body-markup, icon-static, and persistence:

  • Actions. Non-default action pairs become buttons at the bottom of the toast. Clicking one emits ActionInvoked and closes the notification. A default action is triggered by clicking the card body. Notification actions can also be invoked from an extension with tessa-ctl notify-action <id> <key>, which is useful for scripted tests and keyboard-driven automation.
  • Icons. image-data/image_data, image-path/image_path, the icon-name hint, and the app_icon argument are supported. PNG theme icons are searched under the usual XDG icon directories; otherwise a colored monogram is shown.
  • Markup. The body is rendered with Pango markup when it parses cleanly (bold, italic, underline, spans, and links). Invalid markup falls back to plain text.
  • Routing. New toasts are created on the currently focused output. When focus moves while toasts are visible, the stack moves to the new output. Actions still work because a single panel owns the D-Bus name and renders the surface on the chosen output.

If another notification daemon is already running, Tessa cannot own the bus name and leaves notifications to that daemon. Notification sounds and HTML beyond Pango’s supported subset are not implemented.

You can exercise the service with:

Terminal window
gdbus call --session --dest org.freedesktop.Notifications \
--object-path /org/freedesktop/Notifications \
--method org.freedesktop.Notifications.Notify \
test-app 0 "" "Build finished" "All targets are up to date" "[]" "{}" 5000

See ../README.md for the defaults. Additional bindings use XKB keysym names:

bind = SUPER+Return terminal
bind = SUPER+g set-layout grid
bind = SUPER+ALT+Left resize-width -30
bind = SUPER+ALT+Right resize-width 30

Available actions include spawn, terminal, close, quit, workspace, move-to-workspace, focus, move, toggle-float, toggle-fullscreen, focus-next, focus-prev, launcher, clipboard, screenshot, media, tray, audio, reload, ratio, toggle-layout, set-layout, resize-width, and resize-height.

tessa-ctl config prints the effective configuration, including parsed rules and tuning values:

Terminal window
tessa-ctl config
tessa-ctl state
tessa-ctl events window-open

Hooks receive the same structured fields as the event stream. Status entries, custom launcher entries, runtime layout changes, and window resizing are all available through IPC. See EXTENDING.md for the protocol.

The 0.7 customization-parity items are implemented:

  • effect_helper receives per-window state from a long-lived process and returns opacity, corner radius, blur/shadow, and border-width overrides; see EXTENDING.md. There is no in-process plugin VM.
  • tessa-ctl config-check FILE, config-diff FILE, and config-edit provide candidate validation and effective-config diff previews.
  • Shell v2 adds selectable/ordered bar modules, per-surface fonts and foreground/background colors, PNG or PPM-shader wallpapers, and layer rules for the bar, popovers, and toasts.
  • The bar module registry is exposed by tessa-panel --list-modules (add --json for tools). Built-in context modules include layout, groups, mode, and effects; they read compositor state rather than spawning a separate shell process. tessa-panel --theme-json exports resolved semantic color, font, geometry, and effect tokens for external shells.
  • tessa-ctl shell reports which shell roles Tessa Panel owns and lists external layer-shell clients such as Waybar or Quickshell.
  • tessa-ctl perf and frame_time_budget expose render-commit timings; the headless benchmark is tests/performance.sh.
  • The built-in panel is supervised with pidfd exit detection and bounded exponential restart backoff; tests/panel-recovery.sh covers crash recovery and config-driven enable/disable.
  • tests/fuzz-parsers.sh fuzzes config and IPC parsing, and tests/fuzz-asan.sh repeats it with the ASan runtime preloaded. Helper timeout contracts are covered by tests/helper-timeouts.sh.

Remaining validation is hardware-specific (DRM/libinput, physical touchpads, VRR, and a target-laptop frame-time budget). CI-built signed packages and release artifacts are still on the 0.1.x roadmap. The roadmap is in ROADMAP.md. A TypeScript/TSX configuration runtime remains a non-goal; Tessa’s answer to ShojiWM-style composition is a process protocol plus declarative rules, not an embedded JS VM.