fwmctl and the IPC
fwm listens on a unix socket and speaks one line of text per request, replying
with one line of JSON. fwmctl is a thin client for it that links nothing at all,
so it keeps working whatever happens to the compositor’s dependencies.
$XDG_RUNTIME_DIR/fwm-$WAYLAND_DISPLAY.sock # e.g. /run/user/1000/fwm-wayland-0.sock
fwmctl finds it through $FWM_SOCKET if set, else from $WAYLAND_DISPLAY. fwm
exports FWM_SOCKET to its children, so a script started from inside the session
needs no configuration. A nested run is a different socket: point FWM_SOCKET
at it explicitly or you will be talking to the outer session.
Commands
| Command | Does |
|---|---|
fwmctl state |
compositor state: desktop, camera, per-monitor desktops, gravity, per-desktop modes, focused title |
fwmctl windows |
every window: id, title, app_id, geometry, desktop, focus, pinned, nocollide, xwayland |
fwmctl outputs |
monitors, their current mode and every mode they offer |
fwmctl memory |
fwm’s own memory, split into heap, mapped libraries and client buffers |
fwmctl output <name> k=v … |
change one monitor |
fwmctl config |
every settable option with its value, range and one-line help |
fwmctl get <name> |
read one option |
fwmctl set <name> <value> |
change one option, this session only |
fwmctl set <a>=<v> <b>=<v> … |
several at once, applied together or not at all |
fwmctl save <name> [value] |
…and remember it across reloads and restarts |
fwmctl save --all |
remember everything this session has changed |
fwmctl unsave <name> / --all |
forget it, and put the configured value back now |
fwmctl saved |
what is remembered, and what each of those is worth right now |
fwmctl window <id> k=v … |
change one window: desktop, position, pin, collision, focus, close |
fwmctl dispatch <action> |
run any keybind action |
fwmctl reload |
reload config.toml, discarding every set |
fwmctl version |
the running fwm’s release, which binary is answering (path, mtime, pid) and the IPC version |
fwmctl subscribe [events] |
stream events as JSON lines until killed |
fwmctl -h prints the same list with the output keys.
Reading state
$ fwmctl state
{"ok":true,"desktop":2,"camera_x":3840,"windows":4,"screen_width":1920,
"screen_height":1080,"outputs":[{"name":"eDP-1","desktop":2,...}],
"gravity":1.000,"locked":false,"mode":"physics",
"modes":["physics","tiling",...],"focused":"~/fwm"}
state is the only place that answers “which desktop am I on” for every monitor
at once — with independent screens the question has more than one answer — and the
only place that reports each desktop’s mode.
What fwm itself is using
$ fwmctl memory
{"ok":true,"rss_mb":141.5,"anon_mb":31.6,"file_mb":83.9,"shmem_mb":22.7}
top shows fwm as one number — RES, well past a hundred megabytes — and it reads
like a compositor with a leak. The split says otherwise, and it is the reason this
command exists:
anon_mbis fwm’s own heap and stacks. This is the compositor’s real footprint, and the only figure a bug here can inflate.file_mbis mapped executables and libraries — mesa, ffmpeg, pango, cairo. Those pages are shared with every other process that maps them, so the same memory is counted again inside every one of their totals.shmem_mbis client buffers mapped for compositing. Charged to fwm and to the client that owns them; it tracks how many windows are open and how big they are, not anything fwm holds onto.
Only rss_mb is the sum, and it is the one number that answers no question on its
own. Watch anon_mb across a session — that is where a leak in fwm would show.
Changing settings live
$ fwmctl get physics.gravity
{"ok":true,"name":"physics.gravity","value":"981.000"}
$ fwmctl set physics.gravity 300
{"ok":true,"name":"physics.gravity","value":"300.000"}
$ fwmctl config | jq -r '.options[] | "\(.name) = \(.value)"' | head -3
physics.friction = 0.985
physics.mass = 0
physics.mass_ram_ref = 300.000
Everything scalar is settable: the [physics], [tiling], [camera],
[decor], [effects], [input], [gestures], [cava] and [sound] numbers.
fwmctl config is generated from the same table the setter uses, so it documents
itself rather than needing this page kept in sync.
Not settable, on purpose:
- Arrays —
[binds],[[wallpaper]],[[rule]],[mode.*]. They are not scalars; reloading is the right way to change them. physics.tick_rate— the tick timer is armed once at startup, so accepting a new value would report success and change nothing.cava.bars— it rebuilds every scene rect and every kinematic body.sound.path— the sample is loaded once; a reload picks up a new one.- Strings (
icon_theme, thekbd_*keys) — re-read only by a full reload.
set is runtime-only. config.toml is never rewritten; fwmctl reload (or
Super+Shift+R) puts everything back to what the file says. Enumerated options
are numbers here because the table is typed: physics.mass is 0 for size and 1
for ram, cava.mode is 0–3.
Several settings in one command are checked before any of them is applied, so a typo in the third pair leaves the first two alone — and they land in a single re-apply, which is what keeps a script changing three related knobs from producing a frame of the half-changed world:
$ fwmctl set sun.azimuth=120 sun.elevation=25 sun.length=40
{"ok":true,"set":[{"name":"sun.azimuth","value":"120.000"}, …]}
$ fwmctl set sun.blur=4 sun.nonsense=1
{"ok":false,"error":"unknown option \"sun.nonsense\" (try: config)"} # blur unchanged
Keeping what you found — save
set is the right default and, on its own, a dead end: everything found by
trying it is lost at the next reload unless you go and edit the file by hand.
save writes it down instead — not into config.toml, which stays yours, but
into an overlay fwm owns and applies over the config after every load:
~/.local/state/fwm/settings # one `name = value` per line
$ fwmctl set sun.blur 18 # try it
$ fwmctl save sun.blur # keep it — a bare name saves what it is worth now
{"ok":true,"name":"sun.blur","value":"18.000","saved":[…]}
$ fwmctl save sun.opacity 0.6 # or set and keep in one go
$ fwmctl save --all # everything this session has changed
{"ok":true,"count":3}
$ fwmctl saved
{"ok":true,"saved":[{"name":"sun.blur","value":"18.000","live":"4.000"}]}
$ fwmctl unsave sun.blur # forget it, and the configured value is back now
$ fwmctl unsave --all
saved reports both what is written down and what the option is worth now;
they differ whenever a later set has moved one, which is exactly the state
somebody asking the question is trying to see. unsave does not need a reload —
a reload would also discard every other set the session is standing on, which
is a heavy price for taking back one line.
Three things worth knowing:
- Editing
config.tomlstill works and still wins for anything the overlay does not name. The overlay is a diff, not a copy. - The modes menu’s two switches (
physics.mass,sound.collisions) are applied after the overlay, so the menu keeps the last word on them. Saving one is legal and a later click will overrule it — the pill on screen shows what the menu chose, and a saved value that quietly beat it would make the pill a liar. - A name this fwm does not have is skipped in silence, so a file written by a newer build never stops an older one from starting. A name it does have with a value it will not accept is reported through the tray’s ⚠ pill, like any other config problem.
One window
dispatch acts on whatever has the focus, because that is what a keybind means.
A script has an id out of windows and something it wants done to that window:
$ fwmctl window 7 desktop=4 pin=on
{"ok":true,"window":{"id":7,"title":"~/fwm", …,"desktop":4,"pinned":true}}
| Key | Does |
|---|---|
desktop=0-9 |
send it there |
x=, y= |
put it down, in the same coordinates windows reports |
pin=on|off|toggle |
hold it still |
nocollide=on|off|toggle |
let everything pass through it |
focus=on |
give it the keyboard |
close=on |
ask it to close — it may decline and put up a save dialog |
Parsed in full before anything is applied, like output, and answered with the
window as it now stands. A window is dropped where you put it rather than
thrown: it arrives with no velocity, whatever it had before. Position is refused
while the window’s desktop is tiling, because there the geometry belongs to the
layout, which would put it straight back — saying so beats appearing to work for
one frame.
Monitors
$ fwmctl outputs | jq -r '.outputs[].name'
eDP-1
HDMI-A-1
$ fwmctl output HDMI-A-1 mode=2560x1440@144 scale=1.25 position=0,0 desktop=3
Keys: mode=WxH[@Hz], scale=, transform=normal|90|180|270|flipped|flipped-90|…,
position=X,Y, desktop=0-9, enabled=on|off. Every token is parsed before any
of them is applied — a typo in the third setting must not leave a screen halfway
through the other two, which matters most here because a monitor mid-change may be
showing nothing readable. The last lit screen cannot be turned off.
Like set, this is for the session: [[output]] in the config file has the last
word on reload.
Events
$ fwmctl subscribe
{"event":"window_open","id":7,"title":"~/fwm","app_id":"foot","desktop":2}
{"event":"window_focus","id":7,"title":"~/fwm","app_id":"foot","desktop":2}
{"event":"window_focus","id":null}
{"event":"desktop","desktop":3}
{"event":"mode","desktop":3,"mode":"tiling"}
{"event":"gravity","gravity":1.000}
{"event":"config_reload"}
{"event":"setting","name":"sun.blur","value":"18.000","saved":true}
Subscribe to everything, or a comma-separated subset:
window_open, window_close, window_focus, window_title, desktop, mode,
gravity, config_reload, setting. The reply names what was actually subscribed, so a
client can log it rather than assume its request was understood. Subscribing twice
widens the set rather than replacing it.
setting fires whichever hand moved the knob — the socket, a keybind, the modes
menu — because a subscriber cannot tell them apart and should not have to. It is
emitted by comparing what the options are worth against what was last announced,
rather than from each place that changes one, so a route added later is covered
by construction. saved says whether the value is also in the overlay, which is
the difference between a bar redrawing itself and one that can expect the change
to outlive a reload.
"id":null on a focus event is not “window 0”: focus genuinely goes nowhere when
the last window on a desktop closes, and a subscriber has to be able to tell the
two apart.
Nothing in the IPC can stall the compositor: writes go through an outbound queue, and a subscriber that stops reading is dropped rather than allowed to hold compositor state hostage.
Scripting
subscribe plus dispatch is the whole plugin story — no shared address space,
so a script that crashes takes nothing with it:
# Turn gravity off while a video player is open, back on when it closes.
fwmctl subscribe window_open,window_close | while read -r ev; do
case "$ev" in
*window_open*mpv*) fwmctl set physics.gravity 0 ;;
*window_close*mpv*) fwmctl set physics.gravity 981 ;;
esac
done
# A waybar/eww module: the focused window and its speed.
fwmctl state | jq -r '"\(.focused) — desktop \(.desktop)"'
# Heavier windows while a browser is up, from a keybind:
# [binds] "super+m" = "spawn:fwmctl set physics.mass 1"