fwm/docs/fwmctl

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_mb is fwm’s own heap and stacks. This is the compositor’s real footprint, and the only figure a bug here can inflate.
  • file_mb is 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_mb is 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, the kbd_* 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.toml still 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"