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) or “alt”
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"— viewport center; 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 the focused window’s cluster: try focused’s edges (clockwise from the one nearest the viewport center), then BFS to neighbors. Falls back to “center” when no focused window or no valid placement was found.
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"]
[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)
[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
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
mouse_speed
Default: 1.0
mouse (drag) pan multiplier (1.0 = direct)
touch_speed
Default: 1.0
touchscreen gesture pan speed multiplier
drift
Default: 0.5
momentum coast: 0 = off, 0.5 = default, 1 = floatiest
animation_speed
Default: 0.3
camera lerp factor (higher = faster)
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)
pan_step
Default: 100.0
px per pan-viewport action (mod-ctrl-arrow by default)
anchors
Default: [[0, 0]]
Anchors: canvas points discoverable by center-nearest (4-finger swipe / Mod+Arrow) even when no window is there. Uses Y-up coordinate system.
Example: 4 corners
anchors = [[0, 0], [-1750, 1750], [1750, 1750], [1750, -1750], [-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) — kept small so it doesn’t trigger by accident. 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 speed multiplier
mouse_speed
Default: 1.0
mouse-wheel zoom speed multiplier
touch_speed
Default: 1.0
touchscreen gesture zoom speed multiplier
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)
[snap]
enabled
Default: true
magnetic edge snapping during window drag
gap
Default: 12.0
gap between snapped windows (canvas px)
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 (default: dark gray)
fg_color
Default: "#FFFFFF"
title text + close button × color (default: white)
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 (default: 25)
font
Default: "Adwaita Sans"
title text font family
font_size
Default: 11
title text size in points (default: 11)
font_weight
Default: "medium"
thin/extralight/light/regular/medium/ semibold/bold/extrabold/black (default: medium)
title_align
Default: "center"
“left” or “center” (default: 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 intentionally not allowed as a global default: many toolkits (GTK, Electron) ignore xdg-decoration and keep drawing CSD, producing a misaligned double titlebar. Use it per-app via [[window_rules]] instead.
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 (default: dark gray)
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 (default: 2)
blur_strength
Default: 1.1
per-pass texel spread (default: 1.1)
animate_blur_fps
Default: 20
refresh rate of blur under an animated wallpaper (0-144, default 20; 0 = off, freezing the frost so it stops re-sampling the wallpaper). The background is blurred once into a shared full-output texture and each window slices its rect from it, so cost stays flat as windows are added; a window stacked over other windows falls back to an exact per-window blur at the same cadence. Animated wallpapers evolve slowly, so well below the output rate still looks continuous through frosted glass. Camera moves force a refresh.
[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, then pan that — so it pans as cheaply as an image instead of recomputing every frame. ONLY correct for shaders that slide rigidly with the camera: u_camera used once, at full scale, as the only camera term. In GLSL:
vec2 canvas = v_coords * size + u_camera; // pan shifts the image 1:1
Parallax (u_camera * factor) renders WRONG; animated (u_time) and zoom-dependent (u_zoom) shaders are never cached and 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 for animated (u_time) shader backgrounds. 0 = every output frame (default). 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] 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”.
Example: bring your own keyboard scheme, keep mouse + gesture 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, ctrl, shift. 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 windownudge-window <dir>— move focused window by nudge_step pxpan-viewport <dir>— pan camera by pan_step pxcenter-window— center viewport on focused window + reset zoomfocus-center— focus + center on the window under the pointer + reset zoomcenter-nearest <dir>— navigate to nearest window in directioncycle-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 <x> <y>— jump camera to canvas position (bookmarks, Y-up)zoom-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; 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])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 -1750 1750 | top-left bookmark |
"mod+2" | go-to 1750 1750 | top-right bookmark |
"mod+3" | go-to 1750 -1750 | bottom-right bookmark |
"mod+4" | go-to -1750 -1750 | bottom-left bookmark |
"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"
[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.
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. Keybinding/gesture resize is unaffected — bind resize-window-snapped explicitly if you want cluster-aware resize there too.
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
Gesture thresholds — tune for your touchpad size.
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 (prevents anywhere fallback, still forwards).
Gesture types (2–5 fingers):
N-finger-swipe— continuous OR threshold (action determines behavior)N-finger-swipe-up/down/left/right— threshold only, checked before swipe fallback3-finger-doubletap-swipe— continuous OR threshold (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: pan-viewport, zoom, move-window, move-snapped-windows, resize-window, resize-window-snapped Threshold actions: any action from the [keybindings] Actions list except nudge-window, go-to, cycle-windows, and pan-viewport <dir> — e.g. center-nearest, home-toggle, zoom-to-fit, zoom-to-fit-snapped, fit-window, fill-window, exec <cmd>. center-nearest works on swipe triggers only (direction comes from the swipe).
[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, checked before swipe fallback)
"4-finger-swipe-up" = "exec brightnessctl set +5%"
"4-finger-swipe-down" = "exec brightnessctl set 5%-"
| 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]
Touchscreen gesture BINDINGS. Distinct from [input.touch], which holds the touch device settings (enable, map_to_output) — put those there, not here (same split as [gestures] bindings vs [input.trackpad] device settings).
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 (under [touch.anywhere] it also drops the anywhere fallback). 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, checked before swipe fallbackN-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-hold-swipe— continuous only (dwell then drag)
Continuous actions: pan-viewport (swipe), zoom (pinch), and the window grabs — move-window / move-snapped-windows / resize-window / resize-window-snapped (doubletap-swipe / hold-swipe). A held move-window also extends to the snap-cluster. Threshold actions: any action from the [keybindings] Actions list except nudge-window, go-to, cycle-windows, and pan-viewport <dir> — e.g. center-nearest, center-window, home-toggle, zoom-to-fit, fit-window, fill-window, exec <cmd>. center-nearest works on swipe triggers only (direction comes from the swipe).
Note: within one physical gesture, a continuous translation (pan) and a threshold pinch on the same finger count don’t combine — either bind both axes continuous (pan + zoom) or drive discrete actions from a threshold swipe/pinch.
[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 (hold to move the cluster) |
"3-finger-hold-swipe" | resize-window | continuous (dwell then drag) |
[touch.on-canvas]
| Binding | Action | Notes |
|---|---|---|
"1-finger-swipe" | pan-viewport | |
"2-finger-swipe" | pan-viewport | |
"2-finger-pinch" | zoom |
[touch.anywhere]
5-finger navigation mirrors 4-finger by default, so a stray 5th contact doesn’t abort a navigation gesture.
| 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 |
"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.
default: true
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. 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. Winit backend ignores [[outputs]] entries.
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”).
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).
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"
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 (window center, Y-up). Canvas coords, or output-relative (origin = output center) when pinned_to_screen.size— [width, height] initial window dimensions (one-shot; user/app can resize afterwards, so pair with widget = true to lock it)fullscreen— true: force this window to open in fullscreen modewidget— 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)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 that does NOT scale down with zoom (a blurred window is processed at full resolution however far you zoom out), so prefer blur on a handful of windows over globally. Results are cached and only recomputed when the content behind the window changes.opacity— 0.0–1.0: window transparency (default: 1.0, fully 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