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
| Component | What it is |
|---|---|
atrium | The compositor. A fork of niri with a fixed workspace model and five tiling algorithms. |
atrium-shell | The bar and panels, as one quickshell configuration. |
atrium-network | Wifi, wired, and VPN management on wpa_supplicant, dhcpcd, and openvpn. |
atrium-portal | The xdg-desktop-portal backend for screen sharing. |
atrium-setup | System 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:
XDG_RUNTIME_DIRfallback. 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.- D-Bus session bus. If
DBUS_SESSION_BUS_ADDRESSis missing (a raw TTY login), it re-executes itself underdbus-run-session. - Activation environment.
dbus-update-activation-environment --allpushes the base environment to D-Bus before the compositor starts; the compositor pushesWAYLAND_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:
| Variable | Source |
|---|---|
XDG_CURRENT_DESKTOP=atrium, XDG_SESSION_TYPE=wayland | fixed |
WAYLAND_DISPLAY, ATRIUM_SOCKET, DISPLAY | after socket creation |
GBM_BACKEND=nvidia-drm, __NV_PRIME_RENDER_OFFLOAD=1, __GLX_VENDOR_LIBRARY_NAME=nvidia | auto-detected when the NVIDIA driver is bound |
XCURSOR_THEME, XCURSOR_SIZE | from 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 toatrium.log.1throughatrium.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 used | Instead |
|---|---|
| elogind daemon | seatd + pam_rundir + doas |
| polkit | group membership + doas |
| NetworkManager | wpa_supplicant + dhcpcd + openvpn |
| sudo | doas |
| gvfs, udisks2 | udevil |
| ntfs-3g | kernel ntfs3 module |
| systemd, dinit | runit |
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+[.
| Algorithm | Arrangement |
|---|---|
| master-stack (default) | Master area on the left, stack on the right. Adjustable master count, ratio, stack columns, and flip. |
| centered-master | Master window centered, stack columns flanking both sides. |
| fibonacci | Windows spiral inward, each taking half the remaining space. |
| grid | Equal cells in rows and columns. |
| monocle | One 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:
| Parameter | Default | Keys |
|---|---|---|
master-ratio | 0.55 | Mod+L grow, Mod+H shrink |
master-count | 1 | Mod+, add, Mod+. remove |
stack-count (stack columns) | 1 | Mod+Shift+, add, Mod+Shift+. remove |
flip | off | Mod+\ |
gaps | 8 px | config 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+Ftoggles floating. Floating windows render above tiles and move or resize withMod+drag. Window rules can float an app permanently.Mod+Shift+Ffullscreen covers the whole output, bar included. Apps can request it themselves (F11 in a browser, double-click in mpv).Mod+Mmaximize fills the working area and keeps the bar visible.Mod+Iiconifies: the window stays on its workspace but leaves the layout untilMod+Shift+Irestores it. What a client’s own minimize request does is theiconify-actionsetting (ignoreby default,iconify, orsend-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
| Keys | Action |
|---|---|
Mod+Shift+E, Ctrl+Alt+Delete | Quit the compositor |
Mod+Shift+R | Reload the config file |
Mod+Escape | Toggle keyboard-shortcut inhibiting (for apps that grab keys) |
F1 | Show the hotkey overlay |
Applications
| Keys | Action |
|---|---|
Mod+Return | Terminal (alacritty in the default; set yours) |
Mod+R | Launcher (fuzzel in the default; set yours) |
No app-specific launch bindings ship. Add your own spawn binds.
Windows
| Keys | Action |
|---|---|
Mod+Q | Close window |
Mod+Shift+Q | Kill window (SIGKILL) |
Alt+Tab / Shift+Alt+Tab | Focus next / previous window |
Mod+Tab | Focus master |
Mod+U | Focus the urgent window |
Mod+/ | Swap with master |
Mod+Shift+J / Mod+Shift+K | Swap with next / previous |
Mod+F | Toggle floating |
Mod+Shift+F | Fullscreen |
Mod+M | Toggle maximize |
Mod+I / Mod+Shift+I | Iconify / restore |
Layout
| Keys | Action |
|---|---|
Mod+] | Cycle layout algorithm |
Mod+[ | Reset layout |
Mod+\ | Flip layout |
Mod+L / Mod+H | Grow / shrink master area |
Mod+, / Mod+. | Add / remove a master window |
Mod+Shift+, / Mod+Shift+. | Add / remove a stack column |
Workspaces and monitors
| Keys | Action |
|---|---|
Mod+1 … Mod+0 | Bring workspace 1 to 10 to this monitor |
Mod+Shift+1 … Mod+Shift+0 | Send window to workspace, stay |
Mod+Ctrl+1 … Mod+Ctrl+0 | Send window to workspace, follow |
Mod+Left / Mod+Right | Previous / next occupied workspace |
Mod+Up / Mod+Down | Previous / next workspace, including empty |
Mod+Shift+Left / Mod+Shift+Right | Send window to previous / next workspace |
Mod+Wheel | Cycle workspaces |
Mod+N / Mod+Shift+N | Focus next monitor / move window to next monitor |
Media and screenshots
| Keys | Action |
|---|---|
XF86AudioRaiseVolume / LowerVolume / Mute / MicMute | PipeWire volume and mute via wpctl |
XF86MonBrightnessUp / Down | brightnessctl |
Print | Screenshot: the screen freezes and dims, drag to select, release to capture (dropdowns and tooltips survive) |
Shift+Print | Screenshot the active output |
Alt+Print | Screenshot all outputs as one image |
Ctrl+Print | Screenshot the active window |
Mod+Shift+X / Mod+Ctrl+X | Power 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
| Node | What it configures |
|---|---|
input | Keyboard (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. |
layout | Default algorithm, gaps, focus ring, border, shadow, default window width, preset widths |
workspace "name" | Label, icon, open-on-output, and a per-workspace layout override |
environment | Variables for everything the compositor spawns. See The session. |
spawn-at-startup | Programs to launch with the session (pipewire, the shell, a wallpaper setter) |
cursor | Cursor theme and size |
hotkey-overlay | Whether the keybind overlay shows at startup |
screenshot-path, screenshot-annotator | Where captures go, or which tool receives them on stdin |
iconify-action | What a client minimize request does |
animations | Off, or a slowdown factor |
window-rule | See Window rules |
binds | See Keybindings |
debug | Backend 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
Pipewireservice. - Network:
atrium-network barglyph, hover status, click launches the atrium-network window. - Bandwidth: up and down from
/proc/net/dev; the popup lists top processes viabandwhich. - 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 throughmpc 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.AtriumIpcis the compositor spine (runsatrium 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
| Invocation | What |
|---|---|
atrium-network bar | Print the bar glyph and exit |
atrium-network bar --watch | Print the glyph on every state change |
atrium-network status | Multi-line status text for a tooltip |
atrium-network | The GTK4 window, centered and floating |
atrium-network --bar-edge top --bar-margin 22 | The 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 readsOPTSfrom/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 asdoas openvpn --config <file>, so/etc/doas.confneedspermit 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 castslists 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
| State | CPU | GPU |
|---|---|---|
| Idle desktop | 0% (epoll sleep) | none |
| Typing in a terminal | minimal | active output, damage-tracked |
| Workspace switch | brief | active output, ~12 frames |
| Ratio adjust | per keystroke | active 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 aLayoutElementwith 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 aTilingSpaceand aFloatingSpace.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_opsandverify_invariantsexercise 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.