Customising it
Window Effects
Glass, blur, drop shadows, rounded corners and the CRT post-process pass. All of
it is synuirc configuration, and the shipped default is conservative: as of
pkgrel 183 a fresh install boots with window effects off and the SYNAPSE
wallpaper on, because effects on a machine that cannot afford them is a bad first
impression that looks like a broken system.
A default change is invisible on an existing machine. Your
synuircis yours; new defaults only reach the ISO and fresh installs.
Rendering is scenefx 0.5 on wlroots 0.20. scenefx is a fork of wlroots' scene graph, which matters — see the HDR note below.
The Super+E panel
You do not have to edit a config file to tune any of this. Super+E opens a
live panel with two pages, and every change applies as you make it.
Tab switches pages (Shift+Tab goes back). The status line always tells
you which page you are on and what Tab will do next:
| Page | Holds |
|---|---|
| CRT filters | The post-process pass — scanlines, curvature, aberration, glitch, phosphor, bloom |
| Window effects | Corners, shadow, blur, halo and translucency — the 12 rows below |
Same keys on both pages:
| Key | Does |
|---|---|
↑ / ↓ — or k / j |
Move between rows |
← / → — or h / l |
Adjust the selected row |
Space |
Toggle (see below — it means different things per page) |
s |
Save the current page |
Esc · q · Return |
Close |
The panel is modal: while it is open it swallows unmodified keys, so stray letters cannot leak into the window underneath.
Super+… combos still reach the global bind table, so the rest of the desktop keeps working.
Space is page-sensitive
- On the CRT page,
Spaceis the master on/off for the whole effects pass, from any row — you never have to navigate to the top row to kill it. - On the window page,
Spacetoggles the switch governing the row you are on. On any shadow row it toggles Drop shadow; on any blur row (including Glass halo) it toggles Backdrop blur; on Window opacity it toggles Translucency. So pressing it on a greyed-out Shadow size row lights the shadow, rather than doing nothing because that row is not itself a switch. - Rounded corners has no governing switch — 0 px is off.
Spacethere says so rather than silently ignoring you.
Saving is per page
s saves the page you are on, and the two pages persist to separate
files — filters.state for CRT, uifx.state for window effects. That is
deliberate: a shared file would mean saving a shadow you had just tuned also
wrote back whatever the CRT page happened to be holding.
Row values are stored under their synuirc key names, so a value you tune
in the panel can be pasted straight into a config file — the panel and
synuirc are one vocabulary. The exception is the translucency pair, which
lives in theme.state because the theme owns it; storing a second copy would
give startup two answers.
The window-effects rows
| Row | Range | Step | synuirc key |
|---|---|---|---|
| Rounded corners | 0–40 px | 2 | corner_radius |
| Drop shadow | on/off | — | shadow |
| Shadow size | 0–80 px | 2 | shadow_blur_sigma |
| Shadow spread | 0–64 px | 1 | shadow_spread |
| Shadow drop | −32–32 px | 1 | shadow_offset_y |
| Shadow opacity | 0–100% | 5% | shadow_opacity |
| Backdrop blur | on/off | — | blur |
| Blur radius | 1–20 | 1 | blur_radius |
| Blur passes | 1–5 | 1 | blur_passes |
| Glass halo | 0–40 px | 2 | glass_halo |
| Translucency | on/off | — | transparency |
| Window opacity | 50–100% | 2% | active_opacity |
Window opacity floors at 50% on purpose: a window that can go fully invisible is a window you cannot find again.
The knobs
All of these go in ~/.config/synui/synuirc (or /etc/synui/synuirc), and are
the same values the panel above writes.
Turning all of it off
If you do not want any of this, there is one press rather than a hunt:
Control panel (Super+C) ▸ Appearance ▸ Make it all solid — or
synctl dispatch solid, or bind a key to the solid action.
It switches window translucency off, sets Glass to Off, releases every glass pin and makes the bar and the dock opaque.
The pins are the part worth knowing about even if you never use this row. Glass is a master slider and the five surfaces under it follow it — until you adjust one by hand, which pins that row so it keeps its own value. Setting Glass to Off after that leaves a pinned bar or dock exactly as see-through as it was, on a desktop whose glass control now reads Off. Switching Sync all glass off and on again releases them all; this row does it for you.
⚠ Glass at Off does not by itself make the bar and dock fully opaque — the
curve bottoms out at 0.95, so 5% of the wallpaper still shows through. That is
why this row sets bar_opacity and dock_opacity outright, which pins both at
1.00. Delete on either row hands it back to the slider.
It is one-way: there is no button that puts your old numbers back. The rows
themselves do, and Delete on a row restores the shipped default.
Transparency and glass
| Key | Default | Range | What it does |
|---|---|---|---|
transparency |
off |
on/off | Master switch for translucent windows. It also decides whether the shell's menus are glass: with it off — or with blur off — the start menu and the bar's panels draw solid, whatever bar_opacity says, because there is no frost behind them to make a 5% surface read as one. The bar itself keeps its own alpha; a clear bar picks its ink off the wallpaper and works without blur |
active_opacity |
— | 0–1 | Focused window opacity |
inactive_opacity |
— | 0–1 | Unfocused window opacity |
transparency_exclude |
mpv vlc |
app_ids | Windows the two opacities above skip entirely. Matched case-insensitively as a substring of the app_id, so mpv also covers mpv.something. Replaces the shipped list rather than adding to it — an empty value exempts nothing. Applies whether or not the window has focus, so an exempt player does not turn glassy when you click away |
foot_alpha |
— | 0–1 | The terminal's own alpha, decoupled from the slider above. Named for foot; applies to kitty too |
corner_radius |
12 |
0–48 | Rounded window corners |
Blur
| Key | Default | Range | What it does |
|---|---|---|---|
blur |
on |
on/off | Backdrop blur behind translucent windows |
blur_passes |
3 |
1–5 | Dual-kawase passes |
blur_radius |
5 |
1–20 | Per-pass radius |
blur_noise |
0.02 |
0–1 | Dither, to break up banding |
blur_brightness |
0.90 |
Post-blur brightness | |
blur_contrast |
1.00 |
Post-blur contrast | |
blur_saturation |
1.15 |
Post-blur saturation | |
glass_halo |
0 |
0–64 px | Let the blur reach past the window as a ring |
Shadows
| Key | Default | Range | What it does |
|---|---|---|---|
shadow |
on |
on/off | Drop shadow |
shadow_blur_sigma |
18 |
0–80 | Gaussian sigma |
shadow_spread |
0 |
0–64 px | Solid ring outside the window before the tail |
shadow_offset_x / _y |
0 / 6 |
px | Offset |
shadow_opacity |
0.45 |
0–1 | |
shadow_color |
black | ||
clip_csd_margin |
on |
on/off | Crop clients to their declared xdg geometry |
CRT filters (Super+E)
| Key | Default | What it does |
|---|---|---|
effects |
off |
Master switch for the post-process pass |
effect_scanline |
0.35 |
|
effect_curvature |
0.25 |
|
effect_aberration |
0.40 |
Chromatic aberration |
effect_glitch |
0.60 |
|
effect_phosphor |
off |
Monochrome tint: amber / green / white |
effect_mono |
0.90 |
How strongly the phosphor tint desaturates |
effect_bloom |
0.55 |
Phosphor glow |
Persisted to filters.state, adjustable live with a slider per effect — this is
the CRT page of the Super+E panel, and Space on any
row is the master switch for the whole pass.
Night light
night_light / night_light_temp, or Super+Shift+B.
Reading a shadow: whose is it?
This is the single most useful skill for debugging "the shadow looks wrong on this app", and it took a measurement pass to establish.
synui's shadow is a gaussian. No hard edge, reach = sigma + spread, and
shadow_offset_y makes the top band differ from the sides. The shader clips at
2·σ_eff, where coverage is about 2% — never a visible step.
A hard cliff back to the wallpaper is a client margin. GTK crops its own shadow to a rectangular margin that differs per side, so the tell is a hard edge at an asymmetric distance — tens of pixels on one side, a handful on another.
So an app whose shadow looks "overgrown and square-cornered" next to everything else is drawing its own. It was never synui's shadow.
Why Firefox is different
Firefox never binds xdg-decoration. synui declares SERVER_SIDE, but a
client only obeys if it binds the protocol, so Firefox keeps its GTK frame and an
invisible CSD shadow margin outside the window geometry it declares — and
paints a heavy shadow into it, on top of synui's own.
Proven by logging geometry against wlr_surface_get_extents:
firefox geo = 26,23 896x490 inside ext 948x542
foot geo == ext exactly
Measured, that margin is a ~36 px near-black slab at ~85% darkening with a hard step, against a server-decorated app's 12 px soft ring.
clip_csd_margin (default on) clips the client tree to
wlr_xdg_surface.geometry, so every window's ring is the one synui draws — same
width, same rounded corners. It also takes the margin's input region off the grab
ring, which is the documented cause of "Firefox can't be resized by its edges".
Three things that are easy to get wrong there:
- Clip the client tree, not
view->scene_tree.wlr_scene_subsurface_tree_set_clip()recurses into every subsurface tree below the node it is given, and xdg popups are parented beside the client tree — so clipping the parent crops the menus. synui capturesview->client_treeright afterwlr_scene_xdg_surface_create(), when the tree has exactly one child. - Skip the no-op case. A clip that covers everything is not free: it puts a source box and dest size on the scene buffer, and that alone shifted the blurred backdrop around a terminal by ~7/255. Guarded on extents ⊄ geometry, so every SSD client keeps its exact pixels.
wlr_box_equalis NULL/empty-safe, soset_clip(NULL)on an unclipped tree early-returns — cheap enough for the per-commit path.
shadow_spread: the 50% ceiling
box_shadow.frag insets the solid rectangle by sigma, so only the gaussian
tail lands outside the window. Peak darkening at the border is therefore about
half of shadow_opacity — 50% is a hard ceiling even at opacity 1.0,
against Firefox's measured ~85%.
shadow_spread lifts that ceiling without patching scenefx: grow the node by
sigma + S instead of sigma, and the shader's own inset now lands S px
outside the window. Full alpha for S px, then the tail. Node corners grow
with it; a square window stays square; S = 0 is byte-identical to the old
geometry, which is why it is the default.
Measured: the 12 px band outside a window's border goes from 60.0 to 46.1
luminance at shadow_spread = 8.
A maximized or fullscreen window has no shadow, and the Win95 theme's
SYN_CHROME_BEVELchrome returns a shadow of 0. Both are deliberate.
glass_halo: letting the blur past the window
glass_halo = 14 puts a blurred ring outside the window — the backdrop blur
extends past the frame instead of stopping at it.
The history is instructive, because the first two attempts both looked like config problems and were not.
pkgrel 181 — it only affected app-native-glass windows. The blur companion
node was built only for a translucent view, and transparency defaults to
off. So on a desktop with no transparency line in its config, the only window
with a blur node at all was Firefox, and no value of glass_halo could give an
ordinary app a ring.
If your config has no
transparencyline, most window-effect settings will appear to do nothing on most windows. Turn transparency on first.
pkgrel 182 then widened the gate to (translucent || halo > 0), so
server-decorated apps got the ring for the first time.
pkgrel 184 — the ring had a square top corner. The cause was the per-buffer construction. A decorated window is two stacked buffers, and their corner radii carry the seam rule: the titlebar rounds the top, the content rounds the bottom, and the join between them is straight. Growing those boxes outward inherits the seam — so the content buffer's ring covered the whole left edge with square top corners, and the titlebar contributed no ring at all. The seam rule is right inside the window and wrong outside it.
view_halo_update() now owns it: one wlr_scene_blur node on view->frame,
grown glass_halo px on every side, with corner_radii_all(radius + halo) and
the window's own rounded rect cut out via clipped_region so it is a pure ring.
Lowered under the shadow, with the same maximize/fullscreen suppression the
shadow has.
Two things fell out of moving it to the frame: the per-buffer path got its transparency mask back, and an opaque window no longer runs a full-window blur pass just to show a 14 px ring.
The halo is drawn above the shadow, so it blurs the spread into a smooth ramp rather than compounding it.
glass_halo = 14alone actually brightens the band (60 → 80.7) by covering the shadow; withshadow_spread = 8it lands back at 61.6, but smooth.
Firefox glass
synui-firefox-glass toggles it. Firefox is treated as app-native glass: it
needs browser.tabs.allow_transparent_browser (FF 128+), which the helper sets.
Two rules:
offmust revert the prefs. Leaving them set means Firefox stays translucent with no glass behind it.- Page content is opaque by design. Only the chrome goes glass; a transparent web page would be unreadable.
KDE blur
synui implements org_kde_kwin_blur, so KDE apps can go glass at all — but note
that Dolphin never binds it. If you are testing KDE blur, confirm with
WAYLAND_DEBUG that the client actually binds the interface before concluding
anything about the compositor.
HDR and 10-bit
They are two different things and the panel keeps them apart. 10-bit colour
(d) is how many shades the link carries; HDR10 (Shift+D) is a
different signal entirely. Every 10-bit plane used to be labelled "HDR-ready",
which is wrong: plenty of 10-bit panels are not HDR at all. What a monitor is
comes from its EDID; whether it can be driven is asked of the connector.
HDR10 output ships — see Screens for the controls. It is the signal, not the rendering: the display is told it is receiving HDR10 and the desktop is mapped into it, while the compositor still draws in 8-bit sRGB. An HDR video does not gain highlights it did not have.
That last part is what is still blocked on scenefx, and scenefx is a fork of
wlroots' scene graph — so the work is not "wait for wlroots", it is "port or
patch the fork". wp_color_management_v1 is exported as of the 0.20 port.
Three things about output colour, which anything touching it runs into:
- Colour goes on the output state. Handed to the scene it is ignored while window effects are on — scenefx's renderer declares that it does not do output colour transforms, and then silently drops the field.
- A NULL transform must be committed, or it never turns off. The witness
is
modetest'sGAMMA_LUTproperty, not the compositor's own log. The same is true of the HDR image description: a screen put into HDR has to be explicitly told it is back to sRGB. - There is one colour slot per screen. Night light and HDR cannot both hold it, so in HDR the warmth is folded into the HDR curve rather than applied beside it — which is why a screen in HDR still goes warm at night, and by the same amount as the screen next to it.
Verifying an effect actually changed something
Effects are easy to talk yourself into seeing. Two things make a comparison honest:
- Change one knob and restart the compositor between arms. Reloading config in place leaves other state moving, so a whole-image comparison tells you nothing.
- Compare a measurement, not an impression — a column of pixels walking outward from the window border, rather than two screenshots side by side.
The headless rig used for this, and its traps, are in Development Notes.
See also: The Desktop, synui, Troubleshooting.