Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Atrium is a workspace-driven tiling Wayland compositor, and the desktop built around it, for Void Linux.

An atrium is the central courtyard of a Roman house. Every room opens onto it, each with a purpose, a fixed location, things that belong there. You stand in the atrium and go to any room. You don’t scroll through rooms. You don’t tag them. You go where you need to be.

Workspaces are rooms. A window belongs to exactly one workspace. Bringing a workspace to a monitor swaps it with whatever was there. The work of managing windows lives in spatial memory, not in your head.

What is in the box

ComponentWhat it is
atriumThe compositor. A fork of niri with a fixed workspace model and five tiling algorithms.
atrium-shellThe bar and panels, as one quickshell configuration.
atrium-networkWifi, wired, and VPN management on wpa_supplicant, dhcpcd, and openvpn.
atrium-portalThe xdg-desktop-portal backend for screen sharing.
atrium-setupSystem configuration for a Void install: seatd, doas, pam_rundir, greetd, theming, logging.

Principles

  • Do one thing well. The compositor manages windows. Bars, notifications, launchers, lock screens are separate programs speaking standard Wayland protocols and the compositor’s IPC.
  • Workspaces, not tags. No multi-membership, no bitmasks, no ambiguity about where a window is.
  • Keyboard-first. Every action has a binding, handled by the compositor itself. No hotkey daemon.
  • Minimal runtime. No systemd, no elogind, no polkit, no D-Bus activation of session services. seatd, doas, and pam_rundir do the job.
  • Understand every piece. The desktop is a sum of small parts you can read and replace.

Status

Pre-release. The compositor and shell bar are the author’s daily desktop; nothing is packaged yet. Getting started builds it from source on an existing Void install. The status page tracks the line to packages and an ISO.

Where things are

Source and issue trackers: git.efpsoftware.org/atrium. Security reports: security@atrium-project.org, policy in Security policy.

Build and install

Atrium is built from source on Void Linux. These steps produce the compositor, the session wrapper, and the greeter entry. The shell and the other components each have their own short section afterwards.

Build dependencies

doas xbps-install -S rust cargo pkg-config \
    libdrm-devel libinput-devel libseat-devel libxkbcommon-devel \
    mesa-devel MesaLib-devel wayland-devel wayland-protocols \
    pango-devel cairo-devel pipewire-devel libdisplay-info-devel

Rust stable, MSRV 1.85.

Runtime dependencies

doas xbps-install -S libdrm libinput libseat libxkbcommon mesa wayland \
    pango cairo pipewire libdisplay-info \
    seatd dbus xdg-desktop-portal xdg-desktop-portal-gtk \
    xwayland-satellite

Seat management is seatd: enable the service (ln -s /etc/sv/seatd /var/service/) and put your user in the _seatd group. libseat auto-detects it. Atrium does not use elogind; The system underneath covers the rest of the base.

Build

git clone https://git.efpsoftware.org/atrium/atrium.git
cd atrium
make build

make build runs cargo build --release --no-default-features --features dbus,screencast: no systemd feature, D-Bus kept for portals, screencasting on.

make test runs the same gate the pre-commit hook enforces: cargo fmt --check, clippy with -D warnings across the workspace, and the tests.

Try it nested

cargo run

Opens a compositor window inside your current Wayland session through the winit backend, reading your real config. Do not pass --session here; that forces the TTY/DRM backend.

Install

doas make install

Installs to /usr/local/bin (atrium, atrium-session, atrium-session-debug), the wayland-session entries for greetd or any display manager (atrium.desktop, atrium-debug.desktop), the portal configuration, the default config, and the bundled Heroicons. The install target does not rebuild; run make build first.

First-time setup

mkdir -p ~/.config/atrium
cp /usr/share/atrium/default-config.kdl ~/.config/atrium/config.kdl

Then edit ~/.config/atrium/config.kdl. The two bindings to set first are the terminal and launcher under binds (Mod+Return, Mod+R). The file hot-reloads on save. See Configuration.

The shell

git clone https://git.efpsoftware.org/atrium/atrium-shell.git
cd atrium-shell
make install          # to ~/.local

Requires quickshell (Atrium builds its own lean package without polkit; see The system underneath). Add spawn-at-startup "~/.local/bin/atrium-shell" to config.kdl. atrium-shell restart restarts a running instance.

Network app and portal

cd atrium-network && make build && doas make install
cd atrium-portal  && make build && doas make install

atrium-network needs gtk4, gtk4-layer-shell, and a running wpa_supplicant with its D-Bus interface enabled (-u). atrium-portal is D-Bus activated by xdg-desktop-portal when an app asks to share the screen; see atrium-portal for the portals.conf entry.

The session

How a login becomes a running compositor, and where environment variables come from.

Login chain

runit → greetd (VT7) → tuigreet → PAM → atrium-session → atrium --session

greetd starts as a runit service and shows tuigreet. After PAM authenticates, pam_rundir creates /run/user/$UID, and greetd runs the session command from /usr/share/wayland-sessions/atrium.desktop, which is atrium-session.

atrium-session is a short #!/bin/sh wrapper with three jobs:

  1. XDG_RUNTIME_DIR fallback. pam_rundir exports the variable only for the first session of a user. A second login (a TTY already open, or a re-login after the compositor exited) inherits nothing, and the Wayland socket bind fails. The wrapper sets /run/user/$(id -u) when the variable is empty.
  2. D-Bus session bus. If DBUS_SESSION_BUS_ADDRESS is missing (a raw TTY login), it re-executes itself under dbus-run-session.
  3. Activation environment. dbus-update-activation-environment --all pushes the base environment to D-Bus before the compositor starts; the compositor pushes WAYLAND_DISPLAY, DISPLAY, and the GPU variables after the socket exists, so portals and D-Bus-activated services find the session.

Then exec atrium --session. There is no login-shell re-exec: the compositor and the programs it spawns do not get your shell’s PATH, so spawn-at-startup entries use full paths for anything in ~/.local/bin.

atrium-session-debug (the “Atrium (debug)” greeter entry) is the same script with -d, raising the log level.

What the compositor sets

With --session, the compositor exports these itself:

VariableSource
XDG_CURRENT_DESKTOP=atrium, XDG_SESSION_TYPE=waylandfixed
WAYLAND_DISPLAY, ATRIUM_SOCKET, DISPLAYafter socket creation
GBM_BACKEND=nvidia-drm, __NV_PRIME_RENDER_OFFLOAD=1, __GLX_VENDOR_LIBRARY_NAME=nvidiaauto-detected when the NVIDIA driver is bound
XCURSOR_THEME, XCURSOR_SIZEfrom the cursor config

What you set: the environment block

Anything a compositor-spawned program needs goes in config.kdl:

environment {
    GTK_THEME "Arc-Dark"
    QT_QPA_PLATFORMTHEME "gtk3"
    _JAVA_AWT_WM_NONREPARENTING "1"
}

The block applies to every process the compositor launches: spawn-at-startup entries, the shell, the launcher, and anything the launcher spawns. It takes effect at the next login; a live reload only reaches processes spawned after it.

Shell rc files (.zshenv, .bashrc) do not reach these programs. A variable set only there appears to work from a terminal and is silently absent for the same program started from the bar. The usual tell is Java menus mispositioned when a Swing or JavaFX app is launched from the launcher.

Logs

  • ~/.local/state/atrium/atrium.log: the compositor log, always written at info level; the previous five sessions rotate to atrium.log.1 through atrium.log.5. Panics land here with a backtrace.
  • ~/.local/state/atrium/atrium-portal.log: the portal’s log.
  • Compositor stderr goes to the greeter’s VT and is effectively lost; the file is the durable record.

The system underneath

Atrium runs on a deliberately small base. This page lists what it expects from the system and what it does not use. atrium-setup/setup.sh applies all of it to a stock Void install; read it as the executable version of this page.

Session foundation

Kernel (DRM, input, udev)
  ↑
seatd          device broker, runit service; user in group _seatd
  ↑
libseat        auto-detects seatd
  ↑
atrium

PAM            greetd, doas, lock screen
  ↑
pam_rundir     creates /run/user/$UID at login

doas           privilege escalation (/etc/doas.conf)
udevil         removable media, setuid, no daemon
D-Bus          transport only: wpa_supplicant, PipeWire, portals

No runtime authorization daemon. Access is group membership at login: wheel (doas, wpa_supplicant control), _seatd, video, audio, plugdev, storage, network.

/etc/pam.d/system-login carries -session optional pam_rundir.so in place of pam_elogind.so.

Network

wpa_supplicant and dhcpcd as runit services, up before login; openvpn and wireguard on demand through doas. wpa_supplicant runs with -u so atrium-network can talk to it over D-Bus, and the wheel-group D-Bus policy lives in a drop-in file (/etc/dbus-1/system.d/wpa_supplicant-wheel.conf), never in the package’s own conf, which xbps overwrites on update.

Audio

PipeWire with WirePlumber. spawn-at-startup "pipewire" in config.kdl is the only launch line: PipeWire starts WirePlumber through its own config drop-in.

Logging

Void ships no syslog. Atrium’s setup installs socklog-void and enables nanoklogd (kernel ring buffer to disk) and socklog-unix. Logs are under /var/log/socklog/, root-owned. Without this a GPU fault leaves no record.

Patched packages

Three Void packages are rebuilt from templates in atrium/xbps-templates:

  • quickshell, built without polkit, UPower, Hyprland, and i3 integration.
  • xwayland-satellite, carrying an upstream pull request so X11 self-minimize iconifies instead of vanishing.
  • nvidia, the closed kernel module with GSP firmware disabled on Turing, which keeps the box alive through a GPU fault.

Plus seatd built without the elogind option (a stock build flag, no template change). Until the binary package repository exists these are built with xbps-src in a void-packages checkout; the templates repo’s bin/sync-to-tree copies them in.

Not used

Not usedInstead
elogind daemonseatd + pam_rundir + doas
polkitgroup membership + doas
NetworkManagerwpa_supplicant + dhcpcd + openvpn
sudodoas
gvfs, udisks2udevil
ntfs-3gkernel ntfs3 module
systemd, dinitrunit

GNOME libraries are not a dependency of anything Atrium ships.

Workspaces and monitors

Workspaces

Ten fixed workspaces by default, defined by workspace blocks in the config. They always exist; nothing spawns or destroys them. Each holds an ordered list of windows, its own layout algorithm, and its own layout parameters (master ratio, master count, stack columns, flip, gaps).

A window belongs to exactly one workspace. There is no multi-membership and no tagging.

Workspaces have no monitor affinity. Any workspace can appear on any monitor.

Monitors

Each connected monitor shows exactly one workspace, and no two monitors show the same one. Bringing a workspace to a monitor (Mod+1 through Mod+0) does one of two things:

  • if the workspace is on another monitor, the two monitors swap workspaces;
  • if the workspace is unattached, it replaces the one on this monitor, which becomes unattached.

An unattached workspace keeps its windows and layout state. Nothing is lost; it is simply not on screen.

Monitors are identified by EDID (manufacturer, model, serial) for persistent configuration, falling back to the connector name.

Hotplug

When a monitor connects, it gets the workspace last shown on it, or the lowest-numbered unattached workspace. When a monitor disconnects, its workspace becomes unattached with everything preserved, and can be brought to any remaining monitor.

Focus

Focus follows keyboard commands, not the pointer. Clicking a window focuses it; moving the pointer over it does not. focus-follows-mouse in the input block turns pointer focus on.

Mod+N moves focus to the next monitor; Mod+Shift+N moves the focused window there.

Where new windows open

On the active workspace of the active monitor, unless a window rule says otherwise (open-on-workspace). Dialogs float and open on their parent’s monitor. Fixed-size windows float.

Sending windows

  • Mod+Shift+<n>: send the focused window to workspace n and stay.
  • Mod+Ctrl+<n>: send it and follow.
  • Mod+Shift+Left/Right: send it to the previous or next workspace.

Labels and icons

A workspace can carry a display label and an SVG icon for bars:

workspace "1" {
    label "Trading"
    icon "icons/heroicons/o-chart-bar.svg"
}

Relative icon paths resolve under $XDG_DATA_HOME/atrium/ then /usr/share/atrium/, where the compositor installs 325 Heroicons. Both fields are exposed over IPC and rendered by atrium-shell.

Layouts

The layout engine is one function: layout(windows, rect, params) → geometries. Every algorithm implements the TilingLayout trait and is stateless; the state lives in the workspace’s LayoutParams.

The five algorithms

Cycle per workspace with Mod+]; reset with Mod+[.

AlgorithmArrangement
master-stack (default)Master area on the left, stack on the right. Adjustable master count, ratio, stack columns, and flip.
centered-masterMaster window centered, stack columns flanking both sides.
fibonacciWindows spiral inward, each taking half the remaining space.
gridEqual cells in rows and columns.
monocleOne window fills the working area; the bar stays visible.

Set the default globally or per workspace:

layout {
    default-algorithm "master-stack"
}

workspace "media" {
    layout {
        default-algorithm "monocle"
        gaps 0
    }
}

Parameters

Per workspace, adjusted live from the keyboard, remembered independently:

ParameterDefaultKeys
master-ratio0.55Mod+L grow, Mod+H shrink
master-count1Mod+, add, Mod+. remove
stack-count (stack columns)1Mod+Shift+, add, Mod+Shift+. remove
flipoffMod+\
gaps8 pxconfig only

Moving windows within a layout

  • Mod+/: swap the focused window with master.
  • Mod+Shift+J / Mod+Shift+K: swap with the next or previous window.
  • Mod+Tab: focus master. Alt+Tab / Shift+Alt+Tab: focus next or previous window (plain cycling; every window is already visible in a tiling layout, so there is no most-recently-used switcher).

Floating, fullscreen, maximize, iconify

  • Mod+F toggles floating. Floating windows render above tiles and move or resize with Mod+drag. Window rules can float an app permanently.
  • Mod+Shift+F fullscreen covers the whole output, bar included. Apps can request it themselves (F11 in a browser, double-click in mpv).
  • Mod+M maximize fills the working area and keeps the bar visible.
  • Mod+I iconifies: the window stays on its workspace but leaves the layout until Mod+Shift+I restores it. What a client’s own minimize request does is the iconify-action setting (ignore by default, iconify, or send-to-workspace <n>).

Adding an algorithm

Implement TilingLayout in src/layout/tiling/ and register it in tiling/mod.rs. No other file changes. The golden geometry snapshots in src/layout/tests/tiling_geometry.rs pin every algorithm’s exact rectangles for one to six windows; a new algorithm adds its own.

Keybindings

The compositor handles every key event before clients see it. If a binding matches, the event is consumed. There is no hotkey daemon.

Mod is Super on a real session and Alt when running nested under winit. Press F1 at any time for the on-screen overlay of the current bindings.

Defaults

The shipped config binds these. Everything is in the binds block of the default config and can be changed.

Session

KeysAction
Mod+Shift+E, Ctrl+Alt+DeleteQuit the compositor
Mod+Shift+RReload the config file
Mod+EscapeToggle keyboard-shortcut inhibiting (for apps that grab keys)
F1Show the hotkey overlay

Applications

KeysAction
Mod+ReturnTerminal (alacritty in the default; set yours)
Mod+RLauncher (fuzzel in the default; set yours)

No app-specific launch bindings ship. Add your own spawn binds.

Windows

KeysAction
Mod+QClose window
Mod+Shift+QKill window (SIGKILL)
Alt+Tab / Shift+Alt+TabFocus next / previous window
Mod+TabFocus master
Mod+UFocus the urgent window
Mod+/Swap with master
Mod+Shift+J / Mod+Shift+KSwap with next / previous
Mod+FToggle floating
Mod+Shift+FFullscreen
Mod+MToggle maximize
Mod+I / Mod+Shift+IIconify / restore

Layout

KeysAction
Mod+]Cycle layout algorithm
Mod+[Reset layout
Mod+\Flip layout
Mod+L / Mod+HGrow / shrink master area
Mod+, / Mod+.Add / remove a master window
Mod+Shift+, / Mod+Shift+.Add / remove a stack column

Workspaces and monitors

KeysAction
Mod+1Mod+0Bring workspace 1 to 10 to this monitor
Mod+Shift+1Mod+Shift+0Send window to workspace, stay
Mod+Ctrl+1Mod+Ctrl+0Send window to workspace, follow
Mod+Left / Mod+RightPrevious / next occupied workspace
Mod+Up / Mod+DownPrevious / next workspace, including empty
Mod+Shift+Left / Mod+Shift+RightSend window to previous / next workspace
Mod+WheelCycle workspaces
Mod+N / Mod+Shift+NFocus next monitor / move window to next monitor

Media and screenshots

KeysAction
XF86AudioRaiseVolume / LowerVolume / Mute / MicMutePipeWire volume and mute via wpctl
XF86MonBrightnessUp / Downbrightnessctl
PrintScreenshot: the screen freezes and dims, drag to select, release to capture (dropdowns and tooltips survive)
Shift+PrintScreenshot the active output
Alt+PrintScreenshot all outputs as one image
Ctrl+PrintScreenshot the active window
Mod+Shift+X / Mod+Ctrl+XPower monitors off / on

Screenshots save to screenshot-path or pipe into screenshot-annotator (satty, for example) when that is set.

Writing a binding

binds {
    Mod+Return { spawn "ghostty"; }
    Mod+Shift+S { spawn-sh "grim - | wl-copy"; }
    Mod+Q repeat=false { close-window; }
    XF86AudioMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; }
    Mod+WheelScrollDown cooldown-ms=150 { ws-next-all; }
}

Modifiers joined by +, then an XKB key name. spawn takes an argument list; spawn-sh runs a shell line. Any action from atrium msg action --help can be bound. Options: repeat=false, allow-when-locked=true, allow-inhibiting=false, cooldown-ms.

Window rules

Rules match windows by app-id (Wayland) or class (XWayland), with optional title, and are evaluated in order; every matching rule’s properties merge.

window-rule {
    match app-id="signal"
    open-on-workspace "10"
}

window-rule {
    match app-id="pavucontrol"
    open-floating true
}

window-rule {
    match app-id=r#"firefox$"# title="^Picture-in-Picture$"
    open-floating true
}

window-rule {
    match app-id=r#"^org\.wezfurlong\.wezterm$"#
    default-window-width {}
}

Match values are regular expressions. open-on-workspace takes a workspace name or index. open-floating, default-window-width, and iconify-action are the rule properties Atrium adds to what niri already supports; the niri rule properties (opacity, borders, block-out-from screencast, and so on) apply as documented upstream.

Find an app’s id with atrium msg pick-window or atrium msg windows.

Configuration

One file, ~/.config/atrium/config.kdl, in KDL. It hot-reloads: save and the change applies. Mod+Shift+R reloads by hand. A parse error keeps the previous config running and shows an error overlay.

The installed copy at /usr/share/atrium/default-config.kdl is the annotated reference; it is reproduced in full under Default configuration.

Sections

NodeWhat it configures
inputKeyboard (xkb layout, numlock), touchpad, mouse, trackpoint, focus-follows-mouse, warp-mouse-to-focus
output "name"Mode, scale, transform, position per monitor. atrium msg outputs lists names and modes.
layoutDefault algorithm, gaps, focus ring, border, shadow, default window width, preset widths
workspace "name"Label, icon, open-on-output, and a per-workspace layout override
environmentVariables for everything the compositor spawns. See The session.
spawn-at-startupPrograms to launch with the session (pipewire, the shell, a wallpaper setter)
cursorCursor theme and size
hotkey-overlayWhether the keybind overlay shows at startup
screenshot-path, screenshot-annotatorWhere captures go, or which tool receives them on stdin
iconify-actionWhat a client minimize request does
animationsOff, or a slowdown factor
window-ruleSee Window rules
bindsSee Keybindings
debugBackend knobs, including render-drm-device for hybrid GPUs

Atrium inherits most of niri’s option surface; the niri configuration reference applies for anything not covered here, except niri’s scrolling-layout options, which Atrium does not have.

Hybrid GPU

On a laptop with an Intel iGPU and an NVIDIA dGPU driving external outputs, screen sharing needs the compositor to render on the NVIDIA node:

debug {
    render-drm-device "/dev/dri/renderD129"
}

Find the right node with ls -l /dev/dri/by-path/. See atrium-portal.

Files next to it

The shell reads its own KDL files from the same directory: theme.kdl, weather.kdl, feeds.kdl. See atrium-shell.

IPC

The compositor listens on a Unix socket, $XDG_RUNTIME_DIR/atrium.<DISPLAY>.<PID>.sock, exported as $ATRIUM_SOCKET. The protocol is JSON, one message per line: send a Request, get a Reply; request EventStream to receive Event messages for as long as the connection lives. It is niri’s protocol with Atrium’s workspace and layout state added.

atrium msg

atrium msg workspaces          # every workspace with its layout state
atrium msg windows
atrium msg outputs
atrium msg focused-window
atrium msg pick-window         # click a window, print its details
atrium msg event-stream        # follow events
atrium msg action focus-workspace 3
atrium msg action close-window
atrium msg action --help       # every bindable action
atrium msg -j workspaces       # JSON

Subcommands: outputs, workspaces, windows, layers, keyboard-layouts, focused-output, focused-window, pick-window, pick-color, action, output, event-stream, version, overview-state, casts.

Workspace fields

{
  "id": 3, "idx": 3, "name": "3", "output": "HDMI-A-1",
  "label": "Terminal", "icon": "/usr/share/atrium/icons/heroicons/o-command-line.svg",
  "show_label": true, "show_icon": true,
  "is_active": true, "is_focused": true, "is_urgent": false,
  "active_window_id": 42,
  "layout": "master-stack", "master_ratio": 0.55, "master_count": 1, "flipped": false
}

Windows carry is_iconified alongside the usual id, title, app id, workspace, and focus fields.

Events

A consumer connects, receives a full snapshot (workspaces, windows, layout), then a stream of changes: WorkspacesChanged, WorkspaceActivated, WindowOpenedOrChanged, WindowClosed, WindowFocusChanged, WindowUrgencyChanged, KeyboardLayoutsChanged, CastStartedOrChanged, and the rest. Consumers filter client-side. A layout change (algorithm, ratio, flip) emits WorkspacesChanged.

The consumer pattern

atrium-shell’s Services/AtriumIpc.qml is the reference consumer: it runs atrium msg -j event-stream as a child process, restarts it if it exits, and on any event debounces into one authoritative re-pull of atrium msg -j workspaces rather than modelling each event. Bars written for other compositors adapt the same way: a process reading the stream, a full re-query on change.

Screencast requests

Request::StartCast creates a PipeWire stream for an output or window and answers with session and stream ids; CastStartedOrChanged then carries the pw_node_id. This is how atrium-portal hands a node to xdg-desktop-portal without any Mutter-compatible D-Bus surface.

atrium-shell

The bar and panels, as one quickshell configuration. One process, one QML tree, no polkit, no GTK or Qt theme engine. It reads the compositor’s IPC event stream and shells out to ordinary CLIs for everything else.

Install and run

make install                     # ~/.local/share/atrium-shell + ~/.local/bin/atrium-shell
make PREFIX=/usr/local install   # system-wide, with doas
atrium-shell                     # start
atrium-shell restart             # kill a running instance and start again
make run                         # run from the repo; hot-reloads on save

Autostart from config.kdl: spawn-at-startup "~/.local/bin/atrium-shell". A bind such as Mod+Y { spawn "~/.local/bin/atrium-shell" "restart"; } is handy.

Adding a new QML file (a service singleton in particular) does not hot-reload reliably; restart after adding files.

What is on the bar

Per monitor, resolved by connector name in Modules/Bar/Bar.qml, so each monitor can carry a different set.

  • Workspaces: one slot per workspace with its heroicon, colored by state, click to focus. Reads the compositor’s IPC.
  • Clock, Timezones (a world-clock strip with a popup and calendar).
  • Audio and Microphone with a shared mixer popup: output and input selection, per-app streams. Scroll on the icon adjusts volume. PipeWire, via quickshell’s Pipewire service.
  • Network: atrium-network bar glyph, hover status, click launches the atrium-network window.
  • Bandwidth: up and down from /proc/net/dev; the popup lists top processes via bandwhich.
  • MPD: glyph and now-playing; a mini popup with album art, seek, and transport; and a full panel with queue, playlists, and playback modes. Driven by mpc, event-driven through mpc idle.
  • Weather (OpenWeatherMap), Btc (CoinGecko), Feed (RSS/Atom headlines with a popup that opens links).
  • Tray: status-notifier icons with activate and menu.

Configuration files

All in ~/.config/atrium/, KDL, one key "value" per line:

  • theme.kdl: colors and metrics. Keys: bg, fg, accent, muted, urgent, occupied, green, red, bar-height, font-size, icon-size, font-family, icon-font. Defaults are Arc-Dark.
  • weather.kdl: api-key, city, units.
  • feeds.kdl: feed "Label" "https://…" per line.

The bar font is Hack Nerd Font Mono; icon glyphs are nerd-font codepoints, workspace icons are SVG.

Structure

  • shell.qml: entry point.
  • Commons/Theme.qml: the one source of colors and metrics; widgets never hardcode.
  • Services/: state singletons. AtriumIpc is the compositor spine (runs atrium msg -j event-stream, restarts it if it dies, and re-pulls the full workspace list on any event); the others wrap PipeWire, mpc, /proc/net/dev, and HTTP.
  • Widgets/: the reusable pieces listed above.
  • Modules/Bar/: the per-monitor panel windows.

Next

Power menu, launcher, on-screen display, and lock screen are the modules still to build; notifications stay with an external daemon (mako).

atrium-network

A small Rust program for wifi, wired, and VPN management. It talks D-Bus to wpa_supplicant (fi.w1.wpa_supplicant1), reads addresses from dhcpcd, and drives openvpn as a child process. No NetworkManager, no libnm.

Modes

InvocationWhat
atrium-network barPrint the bar glyph and exit
atrium-network bar --watchPrint the glyph on every state change
atrium-network statusMulti-line status text for a tooltip
atrium-networkThe GTK4 window, centered and floating
atrium-network --bar-edge top --bar-margin 22The window anchored to a bar edge via layer-shell

The window shows wifi, address, wired, and connectivity status; scans and connects with password entry; lists and forgets saved networks; starts and stops VPN profiles.

Requirements

  • wpa_supplicant running with -u (D-Bus interface). Void’s runit service reads OPTS from /etc/sv/wpa_supplicant/conf.
  • Your user in wheel, and the wheel-group D-Bus policy present as a drop-in at /etc/dbus-1/system.d/wpa_supplicant-wheel.conf. Without it wifi works but the app sees only “no interface”: an access-denied error swallowed by D-Bus.
  • The wifi interface is discovered through /sys/class/net/*/wireless/; nothing is hardcoded.
  • VPN profiles are ~/.config/openvpn/*.ovpn, started as doas openvpn --config <file>, so /etc/doas.conf needs permit nopass <user> cmd openvpn.

In the bar

atrium-shell’s Network widget runs bar and status together every few seconds and launches the anchored window on click. The shell stays a thin launcher on purpose: connection management belongs in a real program.

Emergency path

Without any of this, from a TTY:

wpa_cli scan && wpa_cli scan_results
wpa_cli add_network                 # returns an id, say 0
wpa_cli set_network 0 ssid '"Name"'
wpa_cli set_network 0 psk '"password"'
wpa_cli enable_network 0
wpa_cli save_config

dhcpcd assigns an address as soon as the link is up.

atrium-portal

The xdg-desktop-portal backend that makes screen sharing work: OBS, browsers, Signal, anything that asks the portal for a screen cast.

App → xdg-desktop-portal → atrium-portal → atrium (IPC: StartCast) → PipeWire stream → App

atrium-portal implements org.freedesktop.impl.portal.ScreenCast. It is D-Bus activated when an app first asks, shows a GTK4 picker for the monitor or window, sends StartCast to the compositor over IPC, waits for the pw_node_id on the event stream, and hands that node back to the main portal. Everything else (file chooser, settings, access) is left to xdg-desktop-portal-gtk. There is no Mutter-compatible D-Bus surface anywhere.

Configuration

~/.config/xdg-desktop-portal/portals.conf:

[preferred]
default=gtk
org.freedesktop.impl.portal.ScreenCast=atrium

The compositor’s make install also installs /usr/share/xdg-desktop-portal/atrium-portals.conf, which applies when XDG_CURRENT_DESKTOP=atrium.

The picker floats by rule (match app-id="atrium-portal", in the default config).

Hybrid GPUs

On an Intel plus NVIDIA laptop, Smithay picks the Intel node as primary renderer, so cast buffers carry Intel modifiers while a consumer running with GBM_BACKEND=nvidia-drm asks for NVIDIA ones, and the PipeWire link fails with no common modifier. Fix: tell the compositor to render on the NVIDIA node.

debug {
    render-drm-device "/dev/dri/renderD129"
}

Debugging

  • Log: ~/.local/state/atrium/atrium-portal.log.
  • Capabilities should read 3/7, not 0/0:
gdbus call --session --dest=org.freedesktop.portal.Desktop \
  --object-path /org/freedesktop/portal/desktop \
  --method org.freedesktop.DBus.Properties.GetAll org.freedesktop.portal.ScreenCast
  • atrium msg casts lists the compositor’s active streams.

Architecture

Atrium is niri with the window-management layer replaced. A Wayland compositor is the display server: GPU rendering, buffer management, input routing, and forty-odd protocols. That is most of the work, and niri’s implementation of it on Smithay is proven, including on NVIDIA hybrid graphics. Where windows go is the part Atrium has different opinions about, and that part is cleanly separable.

The split

atrium
├── compositor core (niri, kept; fixes cherry-picked)
│   ├── src/backend/         DRM/KMS, libinput, GPU rendering, winit and headless backends
│   ├── src/handlers/        Wayland protocol trait implementations
│   ├── src/render_helpers/  rendering pipeline, damage tracking, shaders
│   ├── src/protocols/       layer-shell, foreign-toplevel, screencopy, ext-workspace, ...
│   └── src/utils/           spawning, scaling, signals, transactions
├── layout engine (ours)
│   ├── src/layout/mod.rs         facade: global workspace pool, bring/send/cycle
│   ├── src/layout/workspace.rs   TilingSpace + FloatingSpace per workspace
│   ├── src/layout/tiling_space.rs
│   ├── src/layout/tiling/        TilingLayout trait, five algorithms
│   ├── src/layout/floating.rs
│   └── src/layout/monitor.rs     thin Monitor: output + workspace index
├── bridge (adapted)
│   ├── src/atrium.rs         central state; calls into the layout
│   ├── src/input/            keybind dispatch to layout actions
│   └── src/ipc/              IPC server with workspace-aware commands
├── atrium-config/            KDL config model
└── atrium-ipc/               IPC types (shared with atrium-portal and the shell)

Core abstractions

Layout is a pure function. layout(windows, rect, params) → geometries. The TilingLayout trait encodes exactly that; all five algorithms are stateless and LayoutParams holds the state per workspace. A new algorithm is one file.

A workspace is a container. It owns a TilingSpace (ordered tiles, active algorithm, params), a FloatingSpace, an optional output binding, and per-workspace config overrides. It exists whether or not a monitor shows it.

A monitor is a viewport. Output hardware plus a workspace index. It owns no windows and no layout state, so disconnecting a monitor detaches a workspace with everything intact.

IPC is state plus events. A full snapshot on connect, then change events. Consumers filter client-side.

Animations are per-tile and clock-driven. A shared clock advances once per frame; when nothing animates, the output idles at zero CPU. animations { off } makes everything instant.

Extension is by trait. New algorithm: implement TilingLayout. New window behavior: implement LayoutElement. New event: add to the Event enum.

Data flow

A client creates an xdg_toplevel; the window sits unmapped until its first buffer; rules resolve on app id and title; it lands in the active workspace’s tiling or floating space; arrange() computes every tile; the window receives a configure with its size; it renders at that position. Close reverses it: remove, transfer focus, re-arrange, play the close animation.

A workspace switch snapshots the screen for a crossfade, repoints the monitor, transfers focus, emits WorkspaceActivated, and renders the new workspace over roughly 200 ms.

Hotplug: on connect, prefer the workspace last shown on that output, else the lowest unbound one, compute the working area minus layer-shell exclusive zones, arrange. On disconnect, remember the mapping and detach.

Resource model

StateCPUGPU
Idle desktop0% (epoll sleep)none
Typing in a terminalminimalactive output, damage-tracked
Workspace switchbriefactive output, ~12 frames
Ratio adjustper keystrokeactive output, one frame

Single-monitor actions redraw only the active output.

What Atrium is not

Not a desktop environment (no bar, notifications, lock screen, or launcher inside the compositor). Not a floating window manager (floating is an escape hatch). Not a scrolling window manager. Not tag-based (workspace and window are one-to-one).

GPU recovery

The compositor creates robust GL contexts (LOSE_CONTEXT_ON_RESET), polls reset status each frame, and on a detected reset settles for two seconds, then rebuilds the GPU half in place: EGL context, GBM device, renderer, modeset. Wayland connections and window state stay in memory and the dmabuf global is kept alive through the rebuild, so clients survive. recover-gpu is bindable as a manual trigger. This is why Atrium runs on a private Smithay fork pinned by revision.

The layout boundary

The interface between src/layout/ and the rest of the compositor. Someone with different layout opinions replaces the layout side and keeps everything else.

LayoutElement

The abstraction that keeps the layout ignorant of Wayland. Defined in src/layout/mod.rs:

  • identity: id(), is_wl_surface(), is_child_of()
  • geometry: size(), buf_loc(), min_size(), max_size()
  • size negotiation: request_size(), expected_size(), sizing_mode()
  • rendering: render(), render_normal(), render_popups()
  • state: set_activated(), set_floating(), set_bounds()
  • transactions: configure_intent(), send_pending_configure(), on_commit()

The layout arranges rectangles. It never touches surfaces, buffers, or protocol types.

The pieces

  • Tile<W> (src/layout/tile.rs): wraps a LayoutElement with border, focus ring, shadow, and animations.
  • FloatingSpace<W> (src/layout/floating.rs): freely positioned tiles.
  • TilingSpace<W> (src/layout/tiling_space.rs): a flat tile list with a pluggable algorithm and per-space params; also per-window maximize and the rearrange animation.
  • Workspace<W> (src/layout/workspace.rs): owns a TilingSpace and a FloatingSpace.
  • Monitor (src/layout/monitor.rs): output plus workspace index.
  • The Layout<W> facade (src/layout/mod.rs): the API the rest of the compositor calls.

The facade’s API

Tiling: focus_next_window, focus_prev_window, focus_master, swap_with_master, swap_next_window, swap_prev_window, cycle_layout, flip_layout, reset_layout, grow_master_ratio, shrink_master_ratio, inc_master_count, dec_master_count, inc_stack_count, dec_stack_count.

General: window add and remove, focus, output management, workspace switching, rendering, interactive move and resize, queries.

Callers

  • src/atrium.rs: rendering, window queries, animation.
  • src/input/mod.rs: action dispatch; the tiling actions are wired here.
  • src/handlers/: window lifecycle.

Tests at the boundary

  • Golden geometry snapshots (src/layout/tests/tiling_geometry.rs) pin the exact tile rectangles for each algorithm with one to six windows.
  • Fixture tests (src/tests/) assert what clients actually receive: configure sizes, bounds, states.
  • check_ops and verify_invariants exercise structural invariants under random operation sequences.

make test runs all of it with clippy at -D warnings.

Relationship to niri

Atrium is a permanent fork of niri, not a tracking branch. Upstream fixes are cherry-picked into the parts that are niri’s; the parts that are Atrium’s own diverge for good.

niri’s: cherry-pick fixes

src/backend/, src/handlers/, src/render_helpers/, src/protocols/, src/utils/. The compositor plumbing. Atrium modifies them only for a bug upstream has not fixed yet, and takes the upstream fix when it lands.

Atrium’s own

src/layout/ (workspace model, TilingLayout, five algorithms), src/ui/, atrium-config/, atrium-ipc/, resources/.

Adapted: touched carefully

src/atrium.rs, src/input/mod.rs, src/ipc/. The bridge between upstream code and the layout. Changes here are surgical: add the action dispatch, do not restructure.

Grafting

Each niri release is triaged commit by commit into four buckets: take (cherry-pick with -x for auditability), adapt (a bug idea that applies to Atrium’s rewritten code, ported by hand), skip (scrolling-layout features, systemd and dinit integration, packaging, CI), or batch. A ledger records the cursor and the standing skips so nothing is re-litigated. Atrium is currently synced with niri v26.04.

Smithay

Atrium pins a private Smithay fork by revision, carrying robust-context creation (EGLContext::new_robust_with_priority) for GPU recovery. A graft that bumps niri’s Smithay pin rebases the fork branch onto the new pin and updates both revisions together.

Names that stay niri’s

Shader uniforms (niri_scale, niri_progress, and the rest) are a public API for custom shaders and are not renamed. Comments citing GitHub issues on niri are historical references and stay. Everything else is atrium: the crate, the Atrium state struct, ATRIUM_* environment variables, ~/.config/atrium/.

Version string

Atrium’s own version is CalVer (YYYY.MM.MICRO, no v prefix, so a bare number is ours and v26.04 is niri’s). The niri base is reported alongside, as in atrium 2026.09.0 (niri v26.04).

Default configuration

The annotated default-config.kdl the compositor ships, verbatim. Installed at /usr/share/atrium/default-config.kdl; copy it to ~/.config/atrium/config.kdl to start.

// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Atrium inherits most config options from niri upstream:
// https://niri-wm.github.io/niri/Configuration:-Introduction

// Input device configuration.
input {
    keyboard {
        xkb {
            // You can set rules, model, layout, variant and options.
            // For more information, see xkeyboard-config(7).

            // For example:
            // layout "us,ru"
            // options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
        }

        // Enable numlock on startup, omitting this setting disables it.
        numlock
    }

    // Next sections include libinput settings.
    // Omitting settings disables them, or leaves them at their default values.
    touchpad {
        tap
        natural-scroll
    }

    mouse {
    }

    trackpoint {
    }

    // Uncomment this to make the mouse warp to the center of newly focused windows.
    // warp-mouse-to-focus

    // Focus windows and outputs automatically when moving the mouse into them.
    // Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
    // focus-follows-mouse max-scroll-amount="0%"
}

// You can configure outputs by their name, which you can find
// by running `atrium msg outputs` while inside an atrium instance.
// The built-in laptop monitor is usually called "eDP-1".
// Remember to uncomment the node by removing "/-"!
/-output "eDP-1" {
    // Uncomment this line to disable this output.
    // off

    // Resolution and, optionally, refresh rate of the output.
    // Run `atrium msg outputs` to list all outputs and their modes.
    mode "1920x1080@120.030"

    // You can use integer or fractional scale, for example use 1.5 for 150% scale.
    scale 2

    // Transform allows to rotate the output counter-clockwise, valid values are:
    // normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270.
    transform "normal"

    // Position of the output in the global coordinate space.
    // This affects directional monitor actions like "focus-monitor-left", and cursor movement.
    position x=1280 y=0
}

// Settings that influence how windows are positioned and sized.
layout {
    // Default tiling algorithm for new workspaces.
    // Options: "master-stack", "monocle", "centered-master", "grid", "fibonacci"
    // default-algorithm "master-stack"

    // Set gaps around windows in logical pixels.
    gaps 8

    // When to center a window when changing focus, options are:
    // - "never", default behavior, focusing an off-screen window will keep at the left
    //   or right edge of the screen.
    // - "always", the focused window will always be centered.
    // - "on-overflow", focusing a window will center it if it doesn't fit
    //   together with the previously focused window.
    center-focused-window "never"

    // You can customize the widths that the "switch-preset-window-width" action
    // toggles between (not bound by default).
    preset-window-widths {
        proportion 0.33333
        proportion 0.5
        proportion 0.66667
    }

    // You can change the default width of the new windows.
    default-window-width { proportion 0.5; }
    // If you leave the brackets empty, the windows themselves will decide their initial width.
    // default-window-width {}

    // You can change how the focus ring looks.
    focus-ring {
        // How many logical pixels the ring extends out from the windows.
        width 4

        // Color of the ring on the active monitor.
        active-color "#7fc8ff"

        // Color of the ring on inactive monitors.
        inactive-color "#505050"
    }

    // You can also add a border. It's similar to the focus ring, but always visible.
    border {
        // The settings are the same as for the focus ring.
        // If you enable the border, you probably want to disable the focus ring.
        off

        width 4
        active-color "#ffc87f"
        inactive-color "#505050"

        // Color of the border around windows that request your attention.
        urgent-color "#9b0000"
    }

    // You can enable drop shadows for windows.
    shadow {
        // Uncomment the next line to enable shadows.
        // on

        softness 30
        spread 5
        offset x=0 y=5
        color "#0007"
    }
}

// Workspaces (10 fixed, spectrwm-style).
// Named workspaces can set display properties and layout overrides:
//   workspace "media" {
//       label "Media"
//       icon "~/.local/share/icons/Sardi/scalable/apps/multimedia.svg"
//       show-label true
//       show-icon true
//
//       open-on-output "DP-1"
//       layout {
//           default-algorithm "monocle"
//           gaps 0
//       }
//   }
//
// label:      Display text for bars/IPC (defaults to workspace name).
// icon:       Path to an SVG icon file.
// show-label: Whether to expose the label via IPC (default: true if label/name set).
// show-icon:  Whether to expose the icon via IPC (default: true if icon set).

hotkey-overlay {
    // Show keybind help at startup. Press F1 to show again.
    // Uncomment to not show it at startup:
    // skip-at-startup
}

// Alt+Tab is plain focus cycling (the right model for tiling, where every
// window is already visible). The recent-windows MRU switcher is dormant,
// kept for a possible future floating layout; Alt+Grave / Alt+Shift+Grave
// still cycle recent windows of the same app.
// recent-windows {
//     off
// }

// Which input events wake monitors after power-off-monitors.
// Absent = all events wake (default). Empty block = nothing wakes.
// monitor-wake {
//     key-press
//     mouse-click
// }

// Uncomment this line to ask the clients to omit their client-side decorations if possible.
// After enabling or disabling this, you need to restart the apps for this to take effect.
// prefer-no-csd

// You can change the path where screenshots are saved.
// {name} expands to the capture subject: the window's app id, the output name,
// or "desktop" for all-outputs captures. The rest is strftime.
screenshot-path "~/Pictures/Screenshots/{name} %Y-%m-%d %H-%M-%S.png"

// Uncomment to pipe every screenshot into an annotation tool instead of saving it
// directly. The tool receives the PNG on stdin and owns saving/copying from there.
// screenshot-annotator "satty" "--filename" "-"

// What a client-requested minimize does (the iconify keybind always iconifies).
// An iconified window stays on its workspace but leaves the layout until restored.
//   "ignore"                 drop the request; the window stays put (default)
//   "iconify"                honor it natively
//   "send-to-workspace" 5    move the window to workspace 5 instead
// Also available per window rule: iconify-action "iconify"
// iconify-action "ignore"

// Animation settings.
animations {
    // Uncomment to turn off all animations.
    // off

    // Slow down all animations by this factor. Values below 1 speed them up instead.
    // slowdown 3.0
}

// Window rules let you adjust behavior for individual windows.

// Work around WezTerm's initial configure bug.
window-rule {
    match app-id=r#"^org\.wezfurlong\.wezterm$"#
    default-window-width {}
}

// Open the Firefox picture-in-picture player as floating by default.
window-rule {
    match app-id=r#"firefox$"# title="^Picture-in-Picture$"
    open-floating true
}

// Float the atrium-portal screen share picker.
window-rule {
    match app-id="atrium-portal"
    open-floating true
}

binds {
    // Keys consist of modifiers separated by + signs, followed by an XKB key name.
    // "Mod" is Super on a TTY, Alt when running nested in winit.

    // Help — shows keybind overlay. Press any key to dismiss.
    F1 { show-hotkey-overlay; }

    //
    // === Session ===
    //
    Mod+Shift+E { quit; }
    Ctrl+Alt+Delete { quit; }
    Mod+Shift+R { spawn "atrium" "msg" "action" "load-config-file"; }
    Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }

    //
    // === Applications ===
    //
    // Set your preferred terminal, launcher, and browser here.
    Mod+Return { spawn "alacritty"; }
    Mod+R { spawn "fuzzel"; }

    //
    // === Window management ===
    //
    Mod+Q repeat=false { close-window; }
    Mod+Shift+Q repeat=false { kill-window; }

    // Focus
    Alt+Tab       { focus-next-window; }
    Shift+Alt+Tab { focus-prev-window; }
    Mod+Tab       { focus-master; }
    Mod+U         { focus-urgent; }

    // Swap
    Mod+Slash     { swap-with-master; }
    Mod+Shift+J   { swap-next-window; }
    Mod+Shift+K   { swap-prev-window; }

    // Layout algorithm cycling
    Mod+BracketRight { cycle-layout; }
    Mod+BracketLeft  { reset-layout; }
    Mod+Backslash    { flip-layout; }

    // Master area tuning
    Mod+L     { grow-master-ratio; }
    Mod+H     { shrink-master-ratio; }
    Mod+Comma  { inc-master-count; }
    Mod+Period { dec-master-count; }
    Mod+Shift+Comma  { inc-stack-count; }
    Mod+Shift+Period { dec-stack-count; }

    // Floating / fullscreen / maximize
    Mod+F       { toggle-window-floating; }
    Mod+Shift+F { fullscreen-window; }
    Mod+M       { toggle-maximize; }
    Mod+I       { iconify-window; }
    Mod+Shift+I { uniconify-window; }

    //
    // === Workspaces ===
    //
    Mod+1 { focus-workspace 1; }
    Mod+2 { focus-workspace 2; }
    Mod+3 { focus-workspace 3; }
    Mod+4 { focus-workspace 4; }
    Mod+5 { focus-workspace 5; }
    Mod+6 { focus-workspace 6; }
    Mod+7 { focus-workspace 7; }
    Mod+8 { focus-workspace 8; }
    Mod+9 { focus-workspace 9; }
    Mod+0 { focus-workspace 10; }

    // Send window to workspace (stay on current workspace)
    Mod+Shift+1 { move-window-to-workspace 1; }
    Mod+Shift+2 { move-window-to-workspace 2; }
    Mod+Shift+3 { move-window-to-workspace 3; }
    Mod+Shift+4 { move-window-to-workspace 4; }
    Mod+Shift+5 { move-window-to-workspace 5; }
    Mod+Shift+6 { move-window-to-workspace 6; }
    Mod+Shift+7 { move-window-to-workspace 7; }
    Mod+Shift+8 { move-window-to-workspace 8; }
    Mod+Shift+9 { move-window-to-workspace 9; }
    Mod+Shift+0 { move-window-to-workspace 10; }

    // Send window to workspace and follow it
    Mod+Ctrl+1 { move-window-to-workspace 1 focus=true; }
    Mod+Ctrl+2 { move-window-to-workspace 2 focus=true; }
    Mod+Ctrl+3 { move-window-to-workspace 3 focus=true; }
    Mod+Ctrl+4 { move-window-to-workspace 4 focus=true; }
    Mod+Ctrl+5 { move-window-to-workspace 5 focus=true; }
    Mod+Ctrl+6 { move-window-to-workspace 6 focus=true; }
    Mod+Ctrl+7 { move-window-to-workspace 7 focus=true; }
    Mod+Ctrl+8 { move-window-to-workspace 8 focus=true; }
    Mod+Ctrl+9 { move-window-to-workspace 9 focus=true; }
    Mod+Ctrl+0 { move-window-to-workspace 10 focus=true; }

    // Cycle workspaces (occupied only)
    Mod+Right { ws-next; }
    Mod+Left  { ws-prev; }

    // Cycle workspaces (all, including empty)
    Mod+Up   { ws-next-all; }
    Mod+Down { ws-prev-all; }

    // Send window to next/prev workspace
    Mod+Shift+Right { ws-next-move; }
    Mod+Shift+Left  { ws-prev-move; }

    //
    // === Monitors ===
    //
    Mod+N { focus-monitor-next; }
    Mod+Shift+N { move-window-to-monitor-next; }

    // Scroll wheel workspace switching
    Mod+WheelScrollDown cooldown-ms=150 { ws-next-all; }
    Mod+WheelScrollUp   cooldown-ms=150 { ws-prev-all; }

    //
    // === Media keys ===
    //
    XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+"; }
    XF86AudioLowerVolume allow-when-locked=true { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"; }
    XF86AudioMute        allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; }
    XF86AudioMicMute     allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"; }

    XF86MonBrightnessUp   allow-when-locked=true { spawn "brightnessctl" "set" "+10%"; }
    XF86MonBrightnessDown allow-when-locked=true { spawn "brightnessctl" "set" "10%-"; }

    //
    // === Screenshots ===
    //
    Print { screenshot; }
    Shift+Print { screenshot-screen; }
    Alt+Print { screenshot-all; }
    Ctrl+Print { screenshot-window; }

    //
    // === Monitor power ===
    //
    Mod+Shift+X { power-off-monitors; }
    Mod+Ctrl+X  { power-on-monitors; }
}

Security policy

Report vulnerabilities privately to security@atrium-project.org. Do not open a public issue for anything with security impact.

Scope: every repository in the atrium organization. Vulnerabilities in upstream projects Atrium depends on (niri, Smithay, quickshell, wpa_supplicant, and so on) go to those projects; tell us as well if Atrium’s use of them makes the impact worse.

What to send: the affected component and version or commit, steps to reproduce or a proof of concept, what an attacker gains, and whether the report is already public. Plain email is fine; ask for a key if you need encryption.

What happens next: acknowledgement within 3 business days. A confirmed vulnerability gets a fix on master, a CVE requested through MITRE where one applies, and credit in the release notes unless you prefer otherwise. We ask for 90 days from acknowledgement before public disclosure, shorter by agreement if the fix ships sooner.

Safe harbour: good-faith research against your own installation is welcome. Do not test systems you do not own, and do not access other people’s data.

Machine-readable: atrium-project.org/.well-known/security.txt.