Configuration
driftwm reads its configuration from ~/.config/driftwm/config.toml (respecting
XDG_CONFIG_HOME). Every field is optional — anything you omit uses the built-in
default shown below. Copy config.reference.toml to
get started, then uncomment and edit only the lines you want to change. Validate a
config with driftwm --check-config.
General
mod_key
Default: "super"
Window manager modifier key: “super” (default), “alt”, or “mod3”. Nothing occupies mod3 by default — remap a key onto it in your keymap first.
focus_follows_mouse
Default: false
Sloppy focus: keyboard focus follows the pointer to windows. Moving to empty canvas keeps focus; click empty canvas to unfocus.
window_placement
Default: "center"
Where new windows spawn when no window rule positions them:
"center"— under the focus_placement point (the viewport center unless you changed it); camera animates to the new window."cursor"— centered on the cursor (clamped to the active output’s usable area); camera stays put unless zoomed out and zoom.reset_on_new_window is true."auto"— snap-place adjacent to a cluster already in view: try the anchor’s edges (clockwise from the one nearest the viewport center), then BFS to neighbors. The anchor is the focused window while it’s visible enough, otherwise the nearest element in view (suspended stand-ins included). Falls back to “center” when nothing suitable is in view, when you clicked empty canvas to clear focus, or when no valid placement was found.
focus_placement
Default: "center"
Where a centering navigation parks the focused window in the viewport. One of “center” (default), “top”, “bottom”, “left”, “right”, “top-left”, “top-right”, “bottom-left”, “bottom-right”. Handy on a widescreen, where centering wastes both flanks, or a vertical panel, where it wastes both ends.
An edge or corner puts the window’s visual frame a snap.gap in from the usable area — the same inset fill-window stops at, so a placed window and a filled one share an edge. That gap is canvas px, so it shrinks as you zoom out: 12.0 renders as 4.8 screen px at zoom 0.4.
A window too big to fit on an axis with its two gutters centers on that axis rather than hanging off screen; the other axis still places, so a tall window under “top-left” still goes left.
It applies wherever the camera actually moves to a window — center-window, focus-center, center-nearest, Alt-Tab, click-to-navigate, activation, a newly mapped window, the window focus follows when one closes (see auto_navigate_on_close) and driftwm msg focus. Focusing a window that is already fully on screen doesn’t pan at all, so this places windows on centering navigations rather than holding one against the edge continuously. Fullscreen, pinned and fit windows lock or fill the viewport and ignore it, as does a filled window returning to the view it was filled in.
Because the spawn point follows it too (see window_placement above), this also shifts where a new window lands on the canvas — the position msg move reports and the session file stores. Under window_placement = “auto” that only applies to the fallback: a spawn that finds a slot next to the anchor still goes there.
Overridable per monitor — see the Outputs section.
autostart
Default: []
Commands to run at startup (after WAYLAND_DISPLAY is set). Each entry is passed to sh -c, so full shell syntax works (pipes, &&, env vars).
Running driftwm as a systemd session (via driftwm-session / a display manager) also launches XDG autostart entries (~/.config/autostart, /etc/xdg/autostart), in addition to the autostart list above. To opt out:
systemctl --user mask xdg-desktop-autostart.target
Example:
autostart = ["waybar", "swaync"]
[session]
Persist your canvas across restarts. Suspended windows are always saved to ~/.local/state/driftwm/session.json; these flags control the rest.
suspend_on_close
Default: false
Suspend instead of close when a window is closed by the client (titlebar X, in-app quit). close-window bindings, msg close, and taskbar closes still close for real. Per-window overridable via a suspend_on_close window rule. Whether windows that were still open at a logout come back is governed by [session].restore_windows, not by this flag. See docs/session.md.
restore_windows
Default: false
Restore still-open windows after a restart: windows that resolve to a .desktop entry are saved as you go and come back as suspended windows on the next launch (nothing auto-launches). Per-window overridable via a restore_windows window rule. See docs/session.md.
restore_camera
Default: false
Restore each output’s camera position and zoom from the saved session on the next launch; without it a fresh start centers every output. Read at launch: a mid-session change applies on the next launch.
restore_bookmarks
Default: false
Restore the bookmark registry from the saved session on the next launch, overlaying saved bookmarks on the [navigation.bookmarks] config seeds; without it runtime set-bookmark / msg bookmark edits don’t survive a restart. Read at launch: a mid-session change applies on the next launch.
[env]
Environment variables set before any clients launch. Child processes (autostart, exec bindings) inherit these. These override the compositor’s built-in toolkit defaults (MOZ_ENABLE_WAYLAND, QT_QPA_PLATFORM, SDL_VIDEODRIVER, GDK_BACKEND, ELECTRON_OZONE_PLATFORM_HINT).
Example:
QT_WAYLAND_DISABLE_WINDOWDECORATION = "1"
MOZ_ENABLE_WAYLAND = "1"
[input.keyboard]
layout
Default: "us"
XKB layout (e.g., “us,ru” for multi-layout)
remember_layout_per_window
Default: false
for multi-layout setups
variant
Default: ""
XKB variant (e.g., “dvorak”, or “,” for two defaults)
options
Default: ""
XKB options (e.g., “grp:win_space_toggle” for Super+Space layout switch) grp:alt_shift_toggle/grp:ctrl_shift_toggle break those chord bindings — use a tap binding (see [keybindings]) instead
model
Default: ""
XKB model (e.g., “pc105”)
repeat_rate
Default: 25
keys/sec
repeat_delay
Default: 200
ms before repeat starts
layout_independent
Default: true
match bindings by physical key position across layouts
num_lock
Default: true
num lock state on startup
caps_lock
Default: false
caps lock state on startup
[input.trackpad]
tap_to_click
Default: true
enable tap-to-click
natural_scroll
Default: true
reverse scroll direction (content follows fingers)
tap_and_drag
Default: true
double-tap-hold to drag
accel_speed
Default: 0.0
pointer acceleration (-1.0 to 1.0)
accel_profile
Default: "adaptive"
“flat” or “adaptive”
click_method
Default: "none"
none = device default; clickfinger = finger count (1=left, 2=right, 3=middle); button_areas = position on trackpad
disable_while_typing
Default: true
ignore trackpad input shortly after a key press (palm rejection)
enable
Default: true
false turns the trackpad off entirely. A set-trackpad press overrides this until the compositor restarts, or until a saved config changes the resulting on/off state.
disable_on_external_mouse
Default: false
turn the trackpad off while an external mouse is connected
[input.mouse]
accel_speed
Default: 0.0
pointer acceleration (-1.0 to 1.0)
accel_profile
Default: "flat"
“flat” or “adaptive”
natural_scroll
Default: false
reverse scroll direction
left_handed
Default: false
swap left/right mouse buttons (skipped on devices without left-handed support)
[input.touch]
enable
Default: true
Touchscreen hardware. Gesture thresholds and bindings live in [touch].
enable touchscreen support
map_to_output
Default: "none"
pin touch to an output by connector name; “none” (default) auto-detects the touchscreen by physical size + connector. Only set this to tell apart two identically-sized touchscreens.
Example:
map_to_output = "DP-2"
[cursor]
theme
Default: "none"
XCURSOR_THEME; “none” = inherit from environment (e.g. “Adwaita”)
size
Default: 0
XCURSOR_SIZE; 0 = inherit from environment (e.g. 24)
inactive_opacity
Default: 0.5
cursor opacity on non-active outputs (0.0–1.0)
[navigation]
trackpad_speed
Default: 1.5
trackpad (scroll/gestures) pan multiplier (1.0 = direct, 0 = off)
mouse_speed
Default: 1.0
mouse (drag) pan multiplier (1.0 = direct, 0 = off)
touch_speed
Default: 1.0
touchscreen gesture pan multiplier (1.0 = direct, 0 = off)
drift
Default: 0.5
momentum coast: 0 = off, 0.5 = default, 1 = floatiest
camera_speed
Default: 0.3
camera lerp factor, 0-1 (higher = faster, 1 = instant)
auto_navigate_on_close
Default: true
on close, pan to the newly focused window if off-screen false = camera stays put; focus only moves to a visible window
auto_navigate_on_click
Default: false
completed click on a partially off-screen window also pans it in fully visible → focus only
nudge_step
Default: 20
px per nudge-window action (mod-shift-arrow by default)
resize_step
Default: 20
px per grow-window/shrink-window action (both unbound by default; does not affect drag-resize)
pan_step
Default: 100.0
px per pan-viewport action (mod-ctrl-arrow by default)
anchors
Default: []
Anchors: canvas points discoverable by center-nearest (4-finger swipe / Mod+Arrow) even when no window is there. Uses Y-up coordinate system. Empty by default.
Example: origin + 4 corners
anchors = [[0, 0], [-1750, 1750], [1750, 1750], [1750, -1750], [-1750, -1750]]
[navigation.bookmarks]
Named canvas points for the go-to-bookmark / set-bookmark / move-to-bookmark actions and driftwm msg bookmark. Uses Y-up coordinates (same convention as window rules). This table only SEEDS the runtime registry at startup — set-bookmark and the IPC verb update it live. An explicitly empty table (just the header, no keys) disables the default seeds. Runtime edits persist across restarts only with the [session] restore_bookmarks flag. A bookmark stores a canvas position, not a zoom level.
| Name | Position |
|---|---|
1 | [-1750, 1750] |
2 | [1750, 1750] |
3 | [1750, -1750] |
4 | [-1750, -1750] |
[navigation.edge_pan]
zone
Default: 100.0
activation zone width (px from viewport edge)
speed_min
Default: 4.0
px/frame at zone boundary
speed_max
Default: 10.0
px/frame at viewport edge
cursor_pan
Default: false
pan when the bare cursor touches a screen edge (not just while dragging). Toggle: “toggle-cursor-pan”.
cursor_zone
Default: 20.0
cursor edge-pan activation zone (px). Pans at a constant speed_max within the zone (steady, push-speed independent); speed_min is unused here.
latency_ms
Default: 120
delay before pan starts at an edge bordering another monitor (ms). Outer edges remain immediate. 0 disables.
[zoom]
step
Default: 1.1
multiplier per keypress (1.1 = 10% per press)
trackpad_speed
Default: 1.0
trackpad pinch-zoom multiplier (1.0 = direct, 0 = off)
mouse_speed
Default: 1.0
mouse-wheel zoom multiplier (1.0 = direct, 0 = off)
touch_speed
Default: 1.0
touchscreen gesture zoom multiplier (1.0 = direct, 0 = off)
fit_padding
Default: 80.0
viewport px padding for zoom-to-fit (screen space)
reset_on_new_window
Default: true
animate zoom to 1.0 when a new window is mapped (false = keep current zoom, pan only)
reset_on_activation
Default: true
animate zoom to 1.0 when an off-screen window requests focus (false = keep current zoom, pan only)
interact_min
Default: 0.0
zoom (1.0 = 100%) below which a window is too small to touch: left click navigates to it per focus_placement (a filled window returns to its fill view instead, if that view is itself above the threshold), drag anywhere moves it, pointer input is suppressed. 0 disables. Note the reachable minimum zoom is dynamic (fit * 0.5), so with few windows a low threshold can be unreachable and thus inert.
[snap]
enabled
Default: true
magnetic edge snapping during window drag
gap
Default: 12.0
gap left between windows and around the usable area, in canvas px — snapping, fill-window, fit-window, auto placement and focus_placement all measure it
distance
Default: 24.0
activation threshold (screen px from edge)
break_force
Default: 32.0
screen px past snap to break free
corners
Default: false
also align corners (parallel edges line up: top-to-top, left-to-left)
centers
Default: false
also align centers (midpoints line up along the moved axis)
[decorations]
bg_color
Default: "#303030"
title bar background
fg_color
Default: "#FFFFFF"
title text + close button × color
corner_radius
Default: 10
clip window corners to this radius
shadow
Default: true
drop shadow under window chrome
title_bar_height
Default: 25
SSD title bar text. The font is resolved via fontconfig — install the adwaita-fonts package for the default look; otherwise a generic sans is substituted.
SSD title bar height in px
font
Default: "Adwaita Sans"
title text font family
font_size
Default: 11
title text size in points
font_weight
Default: "medium"
thin/extralight/light/regular/medium/ semibold/bold/extrabold/black
title_align
Default: "center"
“left” or “center”. “center” centers short titles and left-aligns + ellipsizes long ones
default_mode
Default: "client"
Decoration mode for windows without a rule:
"client"— CSD: client draws its own titlebar (default)"minimal"— SSD: no titlebar; shadow, corners, and border still apply via [decorations] + per-window rules"none"— bare client surface: compositor adds zero chrome; per-window border/corner/shadow rules are ignored
“server” (driftwm titlebar) is available per-window via [[window_rules]], not as a global default: many toolkits (GTK, Electron) ignore xdg-decoration and draw CSD anyway, giving a double titlebar.
border_width
Default: 0
Borders apply to “client”, “server”, and “minimal” modes. “none” mode has no border unless one is set per-app in [[window_rules]].
px; 0 disables the border
border_color
Default: "#303030"
unfocused border
border_color_focused
Default: "#303030"
focused border. Same default as above — set this to a different color to get a focus indicator.
[effects]
blur_radius
Default: 2
number of Kawase down+up passes
blur_strength
Default: 1.1
per-pass texel spread
animate_blur_fps
Default: 20
cap (0-144) on how often the frost re-samples a background that moves on its own — an animated (u_time) shader, or a wallpaper daemon’s surface. 0 = never re-sample that motion, so the frost keeps whatever the background looked like when it was captured. Only that motion is capped: panning, zooming, and swapping the background itself (a new wallpaper, a restarted daemon) always refresh, 0 included.
animation_speed
Default: 0.5
window open/close/move/resize lerp factor (higher = faster; 1 = instant)
animation_scale
Default: 0.95
open/close grow/shrink amplitude (1 = fade only)
[background]
type
Default: "default"
Five types: “default” (built-in dot-grid — the default), “shader”, “tile”, “wallpaper”, “none”. For shader/tile/wallpaper, path is the source.
Example:
type = "shader" # procedural GLSL (scrolls with canvas)
path = "/usr/local/share/driftwm/wallpapers/animated/fast_smoke.glsl"
type = "tile" # image tiled across the canvas
path = "~/Pictures/Wallpapers/tile.png"
type = "wallpaper" # single image fixed to viewport (does not scroll/zoom)
path = "~/Pictures/Wallpapers/wallpaper.jpg"
type = "none" # render no built-in background (path ignored), so a
# wlr-layer-shell wallpaper daemon (swaybg, swww, mpvpaper for live video)
# is the wallpaper. Launch it yourself; see docs/shaders.md.
A “shader” can also sample an image via texture (bound to the shader’s tex sampler) — a procedural effect on your image. See docs/shaders.md.
Example: shader sampling an image
type = "shader"
path = "/usr/local/share/driftwm/wallpapers/textured/ripple.glsl"
texture = "~/Pictures/Wallpapers/photo.jpg"
mirror_tile
Default: false
tile mode only: mirror-fold the image (2×2 reflected block) so a non-seamless image’s edges always meet a reflection — no visible tile seams. Also gives a kaleidoscope/symmetry look. No-op for other background types. Single images only; gigapixel pyramidal TIFFs aren’t mirrored (pre-mirror the source).
cache_shader
Default: false
Bake a heavy static shader to a texture once and pan that, instead of recomputing it every frame. Only for shaders that slide rigidly with the camera; animated (u_time) and zoom-dependent (u_zoom) ones always render live.
transparent_shader
Default: false
Honor a shader’s output alpha so transparent pixels reveal whatever sits below the background — e.g. a wlr-layer-shell Background surface from an external wallpaper engine. Forces the shader onto the live path, so cache_shader has no effect while this is on. Image backgrounds ignore it: tile/wallpaper PNGs with an alpha channel become see-through automatically. See docs/shaders.md.
cache_budget_mb
Default: 128
Memory ceiling (MB) shared by cache_shader and gigapixel-TIFF wallpapers, with LRU eviction. Raise it for sharper revisits on large / HiDPI displays; lower it on memory-constrained machines (too low just keeps the background blurrier).
animate_fps
Default: 0
Frame-rate cap (0-1000) for animated (u_time) shader backgrounds. 0 = every output frame. Slow-moving shaders look identical well below the refresh rate; between ticks the compositor reuses the composited result instead of re-evaluating the shader, so this directly scales the background’s GPU cost.
[bindings]
disable_defaults
Default: []
Opt out of built-in default bindings by category, for a clean slate. Normally your [keybindings]/[mouse]/[gestures]/[touch] entries merge with the built-ins (use = "none" to drop a single default). Listing a category here removes ALL of that category’s defaults, leaving only your own entries. Categories: “keys”, “mouse”, “gestures”, “touch”.
Example: bring your own keyboard scheme, keep mouse + gesture + touch defaults
disable_defaults = ["keys"]
[keybindings]
Keyboard bindings: “Modifier+…+Keysym” = “action [arg]” Merges with defaults. Use “none” to unbind a default binding. “mod” expands to mod_key. Literal modifiers: alt, super (alias logo), ctrl (alias control), shift, mod3. Keysyms are XKB names (case-insensitive): return, tab, up, a, equal, etc. A bare modifier combo (e.g. “alt+shift”) is a tap binding (fires on chord release; see [input.keyboard] options).
Actions:
exec <cmd>— launch an app (shows loading cursor until window appears, exits fullscreen)exec-terminal— launch the auto-detected terminal (see [keybindings] below; override with $TERMINAL)exec-launcher— launch the auto-detected app launcher (see [keybindings] below; override with $LAUNCHER)spawn <cmd>— run a command without loading cursor and exiting fullscreen (toggles, OSD, screenshots)close-window— close the focused windowsuspend-window— close the focused window but leave a suspended window in its place (Enter/click relaunches; needs a .desktop entry); on an already-suspended window, dismisses itnudge-window <dir>— move focused window by nudge_step pxgrow-window <dir>— grow focused window by resize_step px, moving its<dir>edge outward; the opposite edge stays putshrink-window <dir>— shrink focused window by resize_step px, pulling its<dir>edge inward; the opposite edge stays putpan-viewport <dir>— pan camera by pan_step pxcenter-window— bring the focused window to the focus_placement point + reset zoom; a window still filled returns to the camera + zoom it was filled in insteadfocus-center— focus the window under the pointer and bring it there too + reset zoom; same fill exceptioncenter-nearest <dir>— navigate to nearest window in direction; a window target lands on the focus_placement point, a navigation.anchors target still centerscycle-windows forward— Alt-Tab style window cyclingcycle-windows backward— reverse cyclehome-toggle— toggle between current position and originzoom-in— step zoom inzoom-out— step zoom outzoom-reset— zoom to 1.0go-to-bookmark <name>— jump the camera to a saved bookmarkset-bookmark <name>— save the current camera center as a bookmark (create or overwrite)move-to-bookmark <name>— move the focused window’s center to a bookmark pointzoom-to-fit— fit all windows in viewportzoom-to-fit-snapped— fit only the focused window’s snap clustertoggle-fullscreen— toggle focused window fullscreenfit-window— toggle maximize: centers + resets zoom + fills viewport; restore only resizes backfit-window-snapped— fit-window for the focused window’s whole snap clusterfill-window— grow in place to fill free space; edges outside the usable area or overlapping another window pull back to a gap; clears maximize; press again to restoretoggle-pin-to-screen— pin/unpin the focused window to the screen (ignores pan/zoom, floats above)reload-config— hot-reload config filetoggle-cursor-pan— toggle cursor edge-pan (see [navigation.edge_pan])set-trackpad on|off|toggle— enable/disable every trackpad for the rest of the session (see [input.trackpad] enable)quit— exit the compositorsend-to-output <dir>— move focused window to adjacent outputsend-cursor-to-output <dir>— move the cursor to adjacent outputswitch-layout <target>— cycle keyboard layout: next, prev (or previous), or a 0-based indexnone— unbind this key combo
Directions: up, down, left, right, up-left, up-right, down-left, down-right
| Binding | Action | Notes |
|---|---|---|
"mod+return" | exec-terminal | $TERMINAL, else first of: foot, alacritty, ptyxis, kitty, wezterm, gnome-terminal, konsole |
"mod+d" | exec-launcher | $LAUNCHER, else first of: fuzzel, wofi, rofi, bemenu-run, wmenu-run, tofi-drun, mew-run |
"mod+q" | close-window | |
"mod+e" | toggle-cursor-pan | toggle cursor edge-pan (see [navigation.edge_pan]) |
"mod+f" | toggle-fullscreen | |
"mod+m" | fit-window | |
"mod+shift+m" | fit-window-snapped | |
"mod+t" | toggle-pin-to-screen | |
"mod+c" | center-window | |
"mod+x" | focus-center | |
"mod+a" | home-toggle | |
"mod+up" | center-nearest up | |
"mod+down" | center-nearest down | |
"mod+left" | center-nearest left | |
"mod+right" | center-nearest right | |
"mod+shift+up" | nudge-window up | |
"mod+shift+down" | nudge-window down | |
"mod+shift+left" | nudge-window left | |
"mod+shift+right" | nudge-window right | |
"mod+ctrl+up" | pan-viewport up | |
"mod+ctrl+down" | pan-viewport down | |
"mod+ctrl+left" | pan-viewport left | |
"mod+ctrl+right" | pan-viewport right | |
"alt+tab" | cycle-windows forward | |
"alt+shift+tab" | cycle-windows backward | |
"mod+equal" | zoom-in | |
"mod+minus" | zoom-out | |
"mod+0" | zoom-reset | |
"mod+z" | zoom-reset | |
"mod+w" | zoom-to-fit | |
"mod+shift+w" | zoom-to-fit-snapped | |
"mod+1" | go-to-bookmark 1 | jump to bookmark 1 (top-left corner by default) |
"mod+2" | go-to-bookmark 2 | jump to bookmark 2 (top-right corner) |
"mod+3" | go-to-bookmark 3 | jump to bookmark 3 (bottom-right corner) |
"mod+4" | go-to-bookmark 4 | jump to bookmark 4 (bottom-left corner) |
"mod+shift+1" | set-bookmark 1 | overwrite bookmark 1 at the current view |
"mod+shift+2" | set-bookmark 2 | |
"mod+shift+3" | set-bookmark 3 | |
"mod+shift+4" | set-bookmark 4 | |
"mod+alt+up" | send-to-output up | move window to output above |
"mod+alt+down" | send-to-output down | |
"mod+alt+left" | send-to-output left | |
"mod+alt+right" | send-to-output right | |
"mod+l" | spawn swaylock -f -c 000000 -kl | |
"mod+ctrl+shift+q" | quit | |
"XF86AudioRaiseVolume" | spawn wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ | |
"XF86AudioLowerVolume" | spawn wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- | |
"XF86AudioMute" | spawn wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle | |
"XF86MonBrightnessUp" | spawn brightnessctl set +5% | |
"XF86MonBrightnessDown" | spawn brightnessctl set 5%- | |
"XF86AudioPlay" | spawn playerctl play-pause | |
"XF86AudioPause" | spawn playerctl play-pause | |
"XF86AudioNext" | spawn playerctl next | |
"XF86AudioPrev" | spawn playerctl previous | |
"XF86AudioStop" | spawn playerctl stop | |
"Print" | spawn grim - | wl-copy | |
"shift+Print" | spawn grim -g \"$(slurp -d)\" - | wl-copy |
Example: built-in window capture (isolated window + shadow) to clipboard
"ctrl+Print" = "spawn driftwm msg screenshot window -o - | wl-copy"
Example: tap binding — bare modifier chord, fires on release with no key on top
"alt+shift" = "switch-layout next"
Example: fill-window (unbound by default)
"mod+g" = "fill-window"
Example: suspend-window (unbound by default)
"mod+s" = "suspend-window"
Example: set-trackpad (unbound by default)
"mod+f10" = "set-trackpad toggle" # flip the trackpad between on and off
"mod+shift+f10" = "set-trackpad off" # turn it off whatever the current state
Example: keyboard resize (unbound by default) — both drive the right edge, one pushing it out and one pulling it in, by resize_step px
"mod+ctrl+shift+right" = "grow-window right"
"mod+ctrl+shift+left" = "shrink-window right"
[mouse]
resize_on_border
Default: true
When true (default), dragging a window’s edge or corner resizes it via the invisible resize border (SSD frame or CSD margin). Set false to make that border inert — resize only through explicit bindings (e.g. alt+right) or gestures. The band stays part of the window either way: pointer focus and on-window bindings still apply over it, so it never reads as empty canvas.
decoration_resize_snapped
Default: false
When true, resizing a window by dragging its edge (SSD or CSD border) propagates to every window connected to it via snap adjacency. Only that drag: the grow-window/shrink-window keybindings and the resize gesture always size the one window. Bind resize-window-snapped to a mouse button, gesture, or touch trigger for a cluster-aware drag of your own.
decoration_fit_snapped
Default: false
When true, maximize/unmaximize initiated via window decoration (CSD maximize button, SSD title-bar double-click, or xdg/foreign-toplevel set_maximized) propagates to every window connected via snap adjacency. Keybinding/gesture fit is unaffected — bind fit-window-snapped explicitly if you want cluster-aware fit there too.
Mouse bindings: “Modifier+…+Trigger” = “action” Context-aware: on-window, on-canvas, anywhere. Specific context checked first, then “anywhere” as fallback. Click-to-focus and SSD decoration clicks are always hardcoded. Triggers: left, right, middle (buttons), trackpad-scroll, wheel-scroll, wheel-up, wheel-down. The wheel-up/wheel-down triggers fire once per discrete wheel notch and can run any action, e.g. volume on mod+shift+scroll. Merges with defaults. Use “none” to unbind.
Mouse actions:
move-window— drag the focused windowmove-snapped-windows— drag the window plus every window connected via snap adjacencyresize-window— drag-resize the focused windowresize-window-snapped— drag-resize, propagating to the snap clusterpan-viewport— drag or scroll to pan the camerazoom— drag or scroll to zoomcenter-nearest— navigate toward the drag direction
Any action from the [keybindings] Actions list also works on button and wheel-up/wheel-down triggers: exec <cmd>, close-window, toggle-fullscreen, etc.
[mouse.on-window]
| Binding | Action | Notes |
|---|---|---|
"alt+left" | move-window | |
"alt+shift+left" | move-snapped-windows | |
"alt+right" | resize-window | |
"alt+shift+right" | resize-window-snapped | |
"alt+middle" | fit-window | |
"alt+shift+middle" | fit-window-snapped | |
"mod+middle" | toggle-fullscreen |
[mouse.on-canvas]
| Binding | Action | Notes |
|---|---|---|
"left" | pan-viewport | unmodified left-click on empty canvas → pan |
"trackpad-scroll" | pan-viewport | trackpad scroll on empty canvas → pan |
"wheel-scroll" | zoom | mouse wheel on empty canvas → zoom |
[mouse.anywhere]
| Binding | Action | Notes |
|---|---|---|
"mod+left" | pan-viewport | |
"mod+ctrl+left" | center-nearest | direction from drag delta |
"mod+trackpad-scroll" | pan-viewport | |
"mod+wheel-scroll" | zoom |
[gestures]
swipe_threshold
Default: 12.0
Trackpad gesture thresholds — tune for your touchpad size. These three keys also exist under [touch] for the touchscreen, where swipe_threshold is measured in millimetres — the values are not interchangeable between the two sections.
px cumulative distance before directional swipe fires
pinch_in_threshold
Default: 0.85
scale below which pinch-in fires (1.0 = no pinch)
pinch_out_threshold
Default: 1.15
scale above which pinch-out fires (1.0 = no pinch)
Gesture bindings: "Modifier+N-finger-<type>" = "action" Context-aware: on-window, on-canvas, anywhere. Unbound gestures are forwarded to the focused app. “none” unbinds.
Gesture types (2–5 fingers):
N-finger-swipe— continuous OR threshold (action determines behavior)N-finger-swipe-up/down/left/right— threshold only, physical finger direction3-finger-doubletap-swipe— continuous only (window grabs; 3-finger tap then swipe)N-finger-pinch— continuous only (use pinch-in/out for discrete)N-finger-pinch-in/out— threshold onlyN-finger-hold— threshold only (fires on release)
Continuous actions, locked to their triggers:
pan-viewport— swipezoom— pinchmove-window— swipe or doubletap-swipemove-snapped-windows— swipe or doubletap-swiperesize-window— swipe or doubletap-swiperesize-window-snapped— swipe or doubletap-swipe
Threshold actions: any action from the [keybindings] Actions list. center-nearest may also be bound bare — its direction is the pan direction of the swipe (fingers left looks right), unlike the named per-direction triggers above which are physical (swipe triggers only).
[gestures.on-window]
| Binding | Action | Notes |
|---|---|---|
"alt+3-finger-swipe" | resize-window | |
"alt+shift+3-finger-swipe" | resize-window-snapped | |
"3-finger-doubletap-swipe" | move-window | |
"alt+2-finger-pinch-in" | fit-window | |
"alt+2-finger-pinch-out" | fit-window | |
"alt+shift+2-finger-pinch-in" | fit-window-snapped | |
"alt+shift+2-finger-pinch-out" | fit-window-snapped | |
"alt+3-finger-pinch-in" | toggle-fullscreen | |
"alt+3-finger-pinch-out" | toggle-fullscreen |
[gestures.on-canvas]
| Binding | Action | Notes |
|---|---|---|
"2-finger-pinch" | zoom |
[gestures.anywhere]
| Binding | Action | Notes |
|---|---|---|
"3-finger-swipe" | pan-viewport | continuous (per-frame dx/dy) |
"4-finger-swipe" | center-nearest | threshold (accumulate, detect direction, fire once) |
"mod+3-finger-swipe" | center-nearest | mod makes 3-finger swipe navigate too |
Example: per-direction overrides (threshold only)
"4-finger-swipe-up" = "exec brightnessctl set +5%"
"4-finger-swipe-down" = "exec brightnessctl set 5%-"
"4-finger-swipe-right" = "cycle-windows forward"
"4-finger-swipe-left" = "cycle-windows backward"
| Binding | Action | Notes |
|---|---|---|
"mod+2-finger-pinch" | zoom | mod overrides app forwarding |
"3-finger-pinch" | zoom | continuous |
"4-finger-pinch-in" | zoom-to-fit | threshold |
"mod+4-finger-pinch-in" | zoom-to-fit-snapped | |
"4-finger-pinch-out" | home-toggle | threshold |
"mod+3-finger-pinch-in" | zoom-to-fit | |
"mod+3-finger-pinch-out" | home-toggle | |
"4-finger-hold" | center-window | fires on release |
"mod+3-finger-hold" | center-window |
[touch]
swipe_threshold
Default: 15.0
Touchscreen gesture thresholds. Touchscreen hardware (enable, output mapping) is configured under [input.touch] instead.
These three keys share their names with [gestures], but the values are not interchangeable: swipe_threshold is in millimetres here and in px there. A touchscreen is a display, so swipe travel is physical — the millimetre value is converted to px through the panel’s pixel density.
swipe_threshold must be positive; 0 or less falls back to the default with a warning.
mm cumulative distance before directional swipe fires
pinch_in_threshold
Default: 0.85
scale below which pinch-in fires (1.0 = no pinch)
pinch_out_threshold
Default: 1.15
scale above which pinch-out fires (1.0 = no pinch)
tap_time
Default: 250
Tap and hold timings. Raise them if gestures need a deliberate, unhurried touch; lower them if the panel feels sluggish to respond. Every single tap waits out double_tap_time before firing — lower it for snappier taps, at the cost of needing faster double-taps.
ms a tap may last; lift later and no tap fires
double_tap_time
Default: 300
ms window for a second tap to pair into a double-tap — and the delay a single tap’s action waits out before it fires
hold_time
Default: 350
ms of dwell before a drag commits as a hold gesture (hold-swipe, doubletap-hold-swipe) instead of a plain swipe
tap_travel
Default: 2.0
mm a contact may drift and still count as a tap; past this it becomes a pan or drag
Bindings: "N-finger-<type>" = "action" (touch has no keyboard modifiers) Context-aware: on-window, on-canvas, anywhere. Unbound gestures are forwarded to the focused app. “none” removes a binding in its context. A fully unbound gesture forwards to the app.
Touch gesture types (1–5 fingers):
N-finger-swipe— continuous OR threshold (action determines behavior)N-finger-swipe-up/down/left/right— threshold only, physical finger directionN-finger-pinch— continuous only (use pinch-in/out for discrete)N-finger-pinch-in/out— threshold onlyN-finger-tap— threshold only (quick touch, no movement)N-finger-doubletap— threshold only (two quick taps)N-finger-doubletap-swipe— continuous only (tap then drag)N-finger-doubletap-hold-swipe— continuous only (tap, hold, then drag)N-finger-hold-swipe— continuous only (dwell then drag)
Continuous and threshold actions: the same sets and rules as under [gestures].
[touch.on-window]
1–2 fingers starting on a window forward to the app, and the 3+ finger pan/zoom/navigate gestures are bound “anywhere” so they apply over windows too. The window-targeted gestures live here: they act on the window the fingers land on (a gesture starting on empty canvas just pans).
| Binding | Action | Notes |
|---|---|---|
"3-finger-doubletap" | fit-window | |
"3-finger-doubletap-swipe" | move-window | continuous |
"3-finger-hold-swipe" | resize-window | continuous (dwell then drag) |
"3-finger-doubletap-hold-swipe" | move-snapped-windows | continuous (tap, hold, then drag — moves the whole snap cluster) |
[touch.on-canvas]
| Binding | Action | Notes |
|---|---|---|
"1-finger-swipe" | pan-viewport | |
"2-finger-swipe" | pan-viewport | |
"2-finger-pinch" | zoom |
[touch.anywhere]
| Binding | Action | Notes |
|---|---|---|
"3-finger-swipe" | pan-viewport | continuous |
"3-finger-pinch" | zoom | continuous |
"4-finger-swipe" | center-nearest | threshold (direction from drag) |
"4-finger-pinch-in" | zoom-to-fit | threshold |
"4-finger-pinch-out" | home-toggle | threshold |
"5-finger-swipe" | center-nearest | threshold (direction from drag) |
"5-finger-pinch-in" | zoom-to-fit | threshold |
"5-finger-pinch-out" | home-toggle | threshold |
"3-finger-tap" | center-window |
[xwayland]
enabled
Default: true
X11 support via xwayland-satellite. driftwm spawns satellite eagerly at startup, exports DISPLAY=:N, and X11 apps connect transparently. If the binary isn’t found, X11 support is disabled with a warning; everything else still runs.
spawn xwayland-satellite at startup
path
Default: "xwayland-satellite"
path to xwayland-satellite binary ($PATH lookup works)
[backend]
wait_for_frame_completion
Default: false
Hardware stability quirks. All default to false (opt-in). Enable these if you experience flickering, crashes, or rendering issues. Particularly useful on NVIDIA GPUs with proprietary drivers. Note: These flags must be set before launching driftwm. Changing them requires a restart. For additional NVIDIA-specific settings, set these environment variables in your session wrapper script or shell profile before starting driftwm:
export SMITHAY_USE_LEGACY=1 # Use legacy DRM API instead of atomic modesetting
export __GL_GSYNC_ALLOWED=0
export __GL_VRR_ALLOWED=0
export __GL_MaxFramesAllowed=1
export NVD_BACKEND=direct
Force GPU-fence wait before every page flip (already done automatically when smithay reports needs_sync — typical case on NVIDIA. Set true only if you still see flicker after defaults.)
disable_direct_scanout
Default: false
Force EGL composition (disable direct scanout)
disable_hardware_cursor
Default: false
Composite the cursor into the frame instead of using the KMS cursor plane, while keeping direct scanout for fullscreen apps. Fixes a stuttering / tearing hardware cursor on discrete NVIDIA GPUs.
max_capture_fps
Default: 0
Cap FPS delivered to continuous screen-capture clients (recorders / casts). 0 = unlimited. Each captured frame forces a full-scene re-composite that competes with a fullscreen app behind it; a cap (e.g. 30 or 60) trims that overhead while gaming + streaming. One-shot screenshots (grim) are never throttled.
[output.outline]
color
Default: "#ffffff"
outline color for other monitors’ viewports
thickness
Default: 1
pixels (0 to disable)
opacity
Default: 0.5
0.0–1.0
Outputs
Per-output configuration — the monitor’s hardware setup, plus the handful of behaviors that are worth varying between a widescreen and a portrait panel. Each [[outputs]] entry matches by connector name. Find connector names with wlr-randr or check driftwm logs at startup. Outputs without a matching entry default to scale 1.0. On the winit backend only position and focus_placement apply — mode, scale and transform belong to the host window.
name = "*" is a wildcard entry: it applies to any connected output that has no exact-name entry (exact entries always win). A fixed position makes no sense on the wildcard — it’s ignored (falls back to “auto”).
An exact-name entry replaces the wildcard entry wholesale — fields do not merge. So if name = "*" sets focus_placement = “right” and an exact name = "DP-1" entry sets only scale, DP-1 takes neither the wildcard’s focus_placement nor its hot corners: every field it doesn’t name falls back to the top-level key or the built-in default, never to the wildcard.
mode accepts “preferred”, “max”, “WxH”, or “WxH@Hz”. “preferred” (the default, and the safe choice) uses the monitor’s advertised preferred mode. “max” picks the highest resolution, then highest refresh. A bare “WxH” only selects a mode the monitor already advertises — if none matches, it keeps the preferred mode (logged as a warning, not an error). “WxH@Hz” forces that exact mode, synthesizing a CVT modeline when the monitor doesn’t advertise it (intended for CRTs or forcing non-standard modes; may be rejected by some panels).
Supported fields:
name— connector name, or “*” for the fallback entry (see above); required.scale— fractional scale factor (default: 1.0).transform— normal, 90, 180, 270, flipped, flipped-90, flipped-180, or flipped-270 (default: normal).position— “auto” (left-to-right placement) or [x, y] in layout coords.mode— “preferred”, “max”, “WxH”, or “WxH@Hz” (see above; default: preferred).focus_placement— where a centering navigation parks the focused window on THIS monitor; the same nine values as the top-level key (see General), which it overrides.hot_corners— per-output screen-corner action bindings (see below).
Hot-corners bind any keyboard action to a screen corner. The action fires when the cursor enters that corner of THIS output (per-monitor setting), and re-arms when the cursor leaves. The whole [[outputs]] block can be omitted if you don’t need any hot corners on a particular monitor. A name = "*" entry’s hot corners apply to any output without an exact-name [[outputs]] entry; per the wholesale-replacement rule above, an exact entry with no hot_corners table gets none.
Corner actions: any action from the [keybindings] Actions list. Pass “none” to leave a corner unbound.
Example:
[[outputs]]
name = "eDP-1" # connector name, or "*" for a fallback entry (required)
scale = 1.5 # fractional scale (default: 1.0)
transform = "normal" # normal, 90, 180, 270, flipped, flipped-90, flipped-180, flipped-270
position = "auto" # "auto" (left-to-right) or [x, y] in layout coords
mode = "preferred" # "preferred", "max", "1920x1080", or "2560x1440@144"
[[outputs]]
name = "HDMI-A-1"
scale = 1.0
mode = "1920x1080@60"
focus_placement = "right" # park the active window against this ultrawide's right edge
[outputs.hot_corners]
threshold = 4 # corner zone size in logical px (default: 4)
top_left = "zoom-to-fit"
top_right = "none"
bottom_left = "spawn grim - | wl-copy"
bottom_right = "exec-launcher"
disable_when_fullscreen = true # suppress hot-corners while a fullscreen window is on this output (default: true)
disable_while_dragging = true # suppress hot-corners while any mouse button is held, or a pointer grab (move/resize/pan) is active (default: true)
Window rules
Window rules: match windows and apply per-window overrides. ALL matching rules are merged in config order (later rules override earlier ones for scalar fields; boolean flags are sticky-on). This lets you compose rules — e.g. one rule sets blur=true, a later one adds opacity=0.85.
This section is the field reference. The full recipe collection, along with matching/merge semantics and pattern-syntax details, lives in docs/window-rules.md.
Supported fields:
app_id— match: Wayland app_id. X11 apps proxied via xwayland-satellite arrive with app_id set from WM_CLASS instance (typically lowercase). At least one of app_id/title is required; all specified criteria must match.title— match: window title.position— [x, y] coordinates (visual-frame center, Y-up). Canvas coords, or output-relative (origin = output center) when pinned_to_screen.size— [width, height] initial dimensions of the visual frame — the app’s content plus any title bar and border driftwm draws, so the same numbers give the same on-screen rectangle whatever the decoration mode (one-shot; user/app can resize afterwards, so pair with widget = true to lock it)fullscreen— true: force this window to open in fullscreen modefocus_on_open— false: map the window without focusing it or moving the camera to it. Omit to keep the default focus-on-map behavior. Pairs well with pinned_to_screen for unobtrusive overlays; the window still takes focus later through normal interaction (hover or click). (default: true)widget— true: pinned (immovable), below normal windows, excluded from navigation and alt-tab (default: false)pinned_to_screen— true: lock the window to the output’s screen space — ignores pan/zoom, floats above normal windows (PiP, toolbars).positionbecomes output-relative; movable unless widget = true. Toggle live withtoggle-pin-to-screen(Mod+T). (default: false)suspend_on_close— override [session].suspend_on_close for matched windows (true / false). Escape hatch for terminals and scratchpads that should always really close (or always suspend). (default: inherit)restore_windows— override [session].restore_windows for matched windows (true / false). false keeps an app out of the session save, so it doesn’t come back as a suspended window on the next launch; true restores one app while the section key stays off. Key the rule on app_id, since a rule matching on title alone governs saving only. Independent of suspend_on_close, which governs closes: set both false for an app that should never leave a stand-in behind. A window you suspended explicitly still comes back. (default: inherit)preserve_aspect_ratio— true: keep the window’s aspect ratio during interactive resizes; the ratio is taken at the start of each resize. (default: false)decoration— overrides [decorations] default_mode for matched windows. Omit to inherit default_mode. Values:- “client”: CSD — client’s own titlebar
- “server”: SSD — driftwm’s titlebar
- “minimal”: SSD — no titlebar, just shadow + corners + border (this is the mode for chrome-on-borderless widgets; border_width / corner_radius / shadow rules apply)
- “none”: bare client surface — compositor adds zero chrome, and per-window border_width / corner_radius / shadow rules are ignored. Use “minimal” if you want chrome without a titlebar.
blur— true: blur background behind this window (default: false). Real GPU/VRAM cost, but it tracks the window’s size on screen, so zooming out makes a blurred window cheaper, not dearer. Once several blurred windows cover most of the viewport between them the compositor switches to one shared full-screen blur they all sample, so a screenful of frost doesn’t multiply the work. Results are cached, and recomputed when the view moves or the content behind the window changes.opacity— 0.0–1.0: window transparency (default: 1.0, fully opaque). Below 1.0 a fullscreen window shows the canvas behind it instead of black: this [background] (dot grid, shader or image) and any canvas layers keep drawing under it, while every layer surface stays hidden along with the other windows. A wlr-layer wallpaper daemon (swaybg, swww) is a layer surface, so it does not show through; set the wallpaper in [background] if you want it visible. Costs that output its direct scan-out for as long as the window is fullscreen, so leave games opaque.border_width— per-window border width override (px). Set to 0 to disable border on a window even when [decorations] border_width > 0. Ignored for decoration = “none”.border_color— per-window unfocused border color, “#rrggbb” or “#rrggbbaa” (optional alpha byte), e.g. “#5c5c5c”.border_color_focused— per-window focused border color; same “#rrggbb[aa]” form, with an optional alpha byte.corner_radius— per-window corner radius override (px). Affects content clip, border shape, and shadow. Ignored for decoration = “none”.shadow— per-window shadow toggle. Overrides [decorations] shadow. Ignored for decoration = “none”.output— output name (e.g. “DP-1”) for this window’s fullscreen and initial screen-pin placement. Fullscreen: the rule wins; otherwise the output the client requested; otherwise the active output. pinned_to_screen: the rule; otherwise the- active output — with
positionresolved against it; dragging or send-to-output reassigns it afterward. Find names underoutputs.*indriftwm msg state. (default: unset)
- active output — with
pass_keys— controls which compositor keybindings are forwarded to the app:- pass_keys = true — forward ALL keys (game-friendly)
- pass_keys = [“mod+q”, “ctrl+q”] — forward ONLY these combos; all other compositor shortcuts stay active
- pass_keys = false / omit — compositor handles everything (default)
- VT switching (Ctrl+Alt+F1–F12) — always stays in the compositor
layer_order— stacking among layer surfaces sharing the same wlr-layer (higher = on top; ties stack by map order, newest on top). The protocol has no z-index within a layer, so two overlay clients (e.g. an on-screen keyboard and a touch visualizer) otherwise stack by launch order. Also orders canvas-positioned layers among themselves. Ignored for regular windows.
Pattern syntax (applies to all match fields):
- Plain string
"kitty"— exact match - Glob
"steam_app_*"—*wildcard - Regex
"/^steam_app_\\d+$/"— wrap in/…/
To find a window’s identifiers, run while the window is open:
`driftwm msg state`
Layer-shell surfaces (panels, notifications, bars like waybar): matched by their namespace against app_id. decoration is ignored — layers have no decoration mode. Chrome (border_width, corner_radius, shadow) is field-by-field opt-in on the rule and does NOT inherit from [decorations]. Without explicit values on the rule, a layer surface has no border, no shadow, and no corner clip.
A few representative rules follow; docs/window-rules.md collects the rest.
Example: Picture-in-Picture, pinned to the screen, stays put while you pan/zoom
[[window_rules]]
title = "Picture-in-Picture"
pinned_to_screen = true
position = [0, -300] # output-relative: 300px below center; movable, drop to center on the output
Example: Game, only let ctrl+q through, keep everything else (mod+q still closes)
[[window_rules]]
app_id = "factorio"
pass_keys = ["ctrl+q"]
Example: Compose rules, blur from first rule, opacity from second (both apply)
[[window_rules]]
app_id = "Alacritty"
blur = true
[[window_rules]]
app_id = "Alacritty"
opacity = 0.9