The interface
fwm draws its own status strip, launcher, desktop overview and menus. There are no titlebars anywhere (windows are server-side decorated with a focus border and nothing else), and there is no panel process to start — all of it is the compositor.
Contents: the tray · the modes menu · the launcher · the desktop strip · the wallpaper picker · key hints · config problems · the visualiser · collision sound
The tray
One strip along the top of each monitor, built from flat chevron-ended
islands. Super+J hides it; a real-fullscreen window hides it automatically
(fake fullscreen deliberately keeps it).
Left to right:
- Focused window — its title, plus a live physics readout: speed, direction and mass. Each monitor reports the window on its own desktop.
- Desktop indicators — ten dots, the current one marked. The marker slides as the camera pans, so it tracks a free pan rather than jumping between desktops.
- The stats pill — what the machine is doing:
CPU 12% • RAM 7.4G • GPU 41%. Click it for the menu below. Ellipsized rather than dropped on a narrow screen: it is the only way to reach its own menu, and the switches in that menu are what would make it fit. - The modes pill — four icons (tiling, floating, gravity, visualiser), lit when that mode is on. Click for the menu below. Fixed width, and dropped rather than squeezed on a screen too narrow for it: the clock grows with the locale’s date and the desktop island is centred, so something has to give, and losing a pill that is also a keybind beats overlapping the clock.
- A ⚠ pill when the config has problems — click to read them.
- A ⌨ pill in the accent colour while a
[mode.*]submap is open, naming it — a mode is a state the keyboard is in, and nothing else says so. - The clock and date, with the keyboard layout in front of it when you have
more than one configured (
RU • 11:02 • Thu, 30/07).
Opacity is [decor] tray_opacity; the whole palette can be derived from the
wallpaper with [decor] color_source = "wallpaper".
It can stand down for an external bar, with [decor] tray_yield = true.
Then, on a monitor where a layer-shell client reserved space along the top —
waybar, quickshell, a dock — fwm’s strip hides itself and stops reserving its
own band, so the two never stack. Per monitor: a second screen with no bar keeps
its strip, and closing the bar brings it back.
Off by default. A client asking for space must not be able to take fwm’s own chrome off the screen; running a bar instead of the strip is a decision, and this is where it is made. Left off, the strip stays put and an external bar simply sits below it.
Only an exclusive zone counts even then. A panel that anchors to the top
without reserving space is asking to float over the screen rather than replace
anything, and the strip stays. Super+J (toggle_tray) still hides it by hand.
The modes menu
Click the modes pill (or bind modes_menu). Eight rows:
| Row | Control | What it is |
|---|---|---|
| Tiling | switch | BSP tiling on the desktop you are looking at |
| Floating | switch | floating mode on that desktop |
| Gravity | switch | on = the heaviest step in gravity_steps, off = zero-g |
| Mass | size / ram |
what decides how heavy a window is — see Physics |
| Sound | switch | the collision knock |
| Cava | off / visual / physical |
the audio visualiser |
| Ring | switch | close the desktop strip into a ring ([camera] wrap) |
| Breakable | switch | a hard enough collision destroys a window ([physics] hp) |
Two rows are segmented controls rather than switches because they are not on-off
things. The config’s fourth cava mode (both, bars that are drawn and push) is
what the physical segment actually selects; a mode you can only discover in the
file is better than one you can land on by clicking past it.
Breakable sits last on purpose: it is the only row that can destroy someone’s unsaved work, so it is not the one the hand lands on by accident. For the same reason it is the one row that is never remembered — every session starts with windows unbreakable, and no config key can start one otherwise.
The Mass and Sound choices are remembered across restarts, in
~/.local/state/fwm/modes. They are written the moment you click and applied over
the config on every load, so your config.toml is never rewritten to record a
click. Delete that file to go back to whatever the config says.
Layout, gravity and the ring are not remembered either: they are per-session state that a keybind changes just as often as the menu does. Breakable is not remembered for a different reason — see above.
The stats menu
Click the stats pill (or bind stats_menu). One row per sensor, each with its
live value and a switch; switching one off takes it out of the pill but keeps it sampled-ready, so
switching it back on shows a number immediately.
Three sensors are built in — cpu (load over the last second), ram (in use,
i.e. total minus MemAvailable) and gpu (busy percentage, from amdgpu or
i915; a card that does not report it is shown greyed rather than hidden).
Everything else is yours. Any key in [stats] that is not one of the
table’s own settings defines a sensor: the key is its name, the value is a shell
command, and its first line of output is what the tray shows.
[stats]
items = ["cpu", "ram", "gpu", "vol", "mic"]
interval = 2.0
vol = "pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]*%' | head -1"
mic = "pactl get-source-mute @DEFAULT_SOURCE@ | grep -q yes && echo off || echo on"
items is the selection and the order; a name in it that is neither built in
nor defined is reported as a config problem, because a misspelt sensor and a
sensor with nothing to say look identical in a tray. Commands run every
interval seconds (floor 0.5; the built-ins sample once a second regardless),
one run at a time per sensor, in their own process group, and are killed if they
have not answered in five seconds — so a slow sensor reports less often instead
of piling up processes.
The compositor never blocks on a sensor: a command is started on one frame and collected on a later one, through a pipe that is only read when it has something to say.
The launcher
Super+Space. Type to filter, ↑/↓ (or Tab) to move, Return to run,
Escape to close. It reads the same .desktop files everything else does and
draws real icons from your icon theme ([decor] icon_theme, or auto-detected from
the gtk3 setting and then hicolor).
The tiles have physics of their own — they settle as the list filters — and the launcher owns the keyboard while it is open, so no keystroke reaches a client you cannot see.
The desktop strip
Super+A (expo). Every desktop becomes a live-looking card in a row you can
pan, click into, and drag windows between. z steps out to the wider view, where
the cards become a ring in 3D you can orbit; x closes the strip into a ring
(the same toggle_wrap the config’s [camera] wrap sets).
Keys are listed in Keybindings. While the strip is up the simulation is frozen — otherwise the windows in those pictures would quietly have moved by the time you dropped one.
The wallpaper picker
Super+Shift+P. Browses [wallpaper_picker] dir (default ~/Pictures), stills
and videos alike, and applies the choice with a cross-fade. The pick is remembered
in ~/.local/state/fwm/wallpaper and re-applied after every config load, so a
reload keeps it. [wallpaper_picker] fps caps the frame rate of videos chosen
this way.
Screenshots
Print photographs the monitor the pointer is on; Super+Shift+S dims the screen
and waits for you to drag a rectangle out — Escape, or a click with no drag in
it, cancels. Either way the PNG goes on the clipboard, ready to paste into
anything that takes an image; nothing is written to disk, and there is nothing
to configure. A line at the bottom of the screen confirms the size copied.
The region shot has one flourish: the moment it is taken, a frozen copy of the
rectangle lifts off the screen, shrinks, tilts and flies down into the message
that says it was copied — the live screen carrying on underneath it the whole time.
It is worth more than decoration, since it shows exactly which pixels were
caught after the selector’s dimming has already gone. [effects] shot_fly
scales it, and 0 turns it off.
fwm serves the bytes itself — it owns the seat’s selection — so no wl-copy
and no clipboard manager has to be running. The picture stays pasteable until
the next thing you copy replaces it.
The picture is the frame the monitor actually drew, so the tray, the bars and
any panel that was up are all in it — a screenshot of fwm, not of the windows
it happens to be holding. An external grim still works too: fwm implements
wlr-screencopy-v1.
Key hints
Super+Shift+? draws the live bind list — read from your config, not from a
hard-coded table, so it is accurate for whatever you have bound.
Config problems
A broken config never costs the session: fwm carries on with defaults and puts a
⚠ pill in the tray with the count. Clicking it (or show_errors) opens the panel
listing what went wrong, line by line. Fix the file and press Super+Shift+R.
The audio visualiser
A row of spectrum bars along the bottom of the screen, fed by loopback capture of
whatever is playing — no external cava process, and fwm does its own FFT.
The point is the second half: with mode = "physical" or "both" each band is a
solid moving body along the floor, so a window sitting at the bottom of the screen
stands on the spectrum, gets thrown by the bass, and lands back down between
beats.
Configured in [cava]. It captures through PipeWire or
PulseAudio, whichever is actually running, and never starts a sound server of its
own — libpulse would happily autospawn one, and a second daemon fighting the first
for the card costs you your audio, not just your bars. If no server is running
yet, fwm keeps looking: bars appear when something starts playing, however long
after login that is.
Collision sound
With [sound] collisions = true, windows knock when they hit each other, the
floor or the walls. How hard the hit was sets the volume; how heavy the window is
sets the pitch, so with mass = "ram" a two-gigabyte browser knocks like a
wardrobe. path takes a WAV of your own; without one the click is synthesised — a
short noise burst for the contact and a low decaying sine for the weight behind
it.
Nothing is opened until the first collision and the device is handed back a few seconds after the last one, so a quiet desktop holds no audio stream. Playback needs libpulse-simple (pipewire-pulse serves it too).