Customising it

Window Effects

Edit on GitHub · last edited

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 synuirc is 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, Space is 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, Space toggles 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. Space there 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 filesfilters.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:

  1. 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 captures view->client_tree right after wlr_scene_xdg_surface_create(), when the tree has exactly one child.
  2. 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.
  3. wlr_box_equal is NULL/empty-safe, so set_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_opacity50% 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_BEVEL chrome 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 transparency line, 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 = 14 alone actually brightens the band (60 → 80.7) by covering the shadow; with shadow_spread = 8 it 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:

  • off must 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's GAMMA_LUT property, 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.