# 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`](../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"` — 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 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. ### `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: ```text systemctl --user mask xdg-desktop-autostart.target ``` **Example:** ```toml 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. See docs/session.md. ### `restore_windows` Default: `false` Restore still-open windows after a restart: on quit or logout, windows that resolve to a .desktop entry are saved 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:** ```toml 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` 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:** ```toml 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) ### `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** ```toml 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 centers it, 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 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 ### `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` refresh rate (0-144) of blur under an animated wallpaper; 0 = off, freezing the frost so it stops re-sampling the wallpaper. Camera moves force a refresh regardless. ### `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:** ```toml 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** ```toml 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** ```toml 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 ` — 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 ` — run a command without loading cursor and exiting fullscreen (toggles, OSD, screenshots) - `close-window` — close the focused window - `suspend-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 it - `nudge-window ` — move focused window by nudge_step px - `pan-viewport ` — pan camera by pan_step px - `center-window` — center viewport on focused window + reset zoom - `focus-center` — focus + center on the window under the pointer + reset zoom - `center-nearest ` — navigate to nearest window in direction - `cycle-windows forward` — Alt-Tab style window cycling - `cycle-windows backward` — reverse cycle - `home-toggle` — toggle between current position and origin - `zoom-in` — step zoom in - `zoom-out` — step zoom out - `zoom-reset` — zoom to 1.0 - `go-to-bookmark ` — jump the camera to a saved bookmark - `set-bookmark ` — save the current camera center as a bookmark (create or overwrite) - `move-to-bookmark ` — move the focused window's center to a bookmark point - `zoom-to-fit` — fit all windows in viewport - `zoom-to-fit-snapped` — fit only the focused window's snap cluster - `toggle-fullscreen` — toggle focused window fullscreen - `fit-window` — toggle maximize: centers + resets zoom + fills viewport; restore only resizes back - `fit-window-snapped` — fit-window for the focused window's whole snap cluster - `fill-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 restore - `toggle-pin-to-screen` — pin/unpin the focused window to the screen (ignores pan/zoom, floats above) - `reload-config` — hot-reload config file - `toggle-cursor-pan` — toggle cursor edge-pan (see [navigation.edge_pan]) - `quit` — exit the compositor - `send-to-output ` — move focused window to adjacent output - `send-cursor-to-output ` — move the cursor to adjacent output - `switch-layout ` — cycle keyboard layout: next, prev (or previous), or a 0-based index - `none` — 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** ```toml "ctrl+Print" = "spawn driftwm msg screenshot window -o - | wl-copy" ``` **Example: tap binding — bare modifier chord, fires on release with no key on top** ```toml "alt+shift" = "switch-layout next" ``` **Example: fill-window (unbound by default)** ```toml "mod+g" = "fill-window" ``` **Example: suspend-window (unbound by default)** ```toml "mod+s" = "suspend-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. 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. 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 window - `move-snapped-windows` — drag the window plus every window connected via snap adjacency - `resize-window` — drag-resize the focused window - `resize-window-snapped` — drag-resize, propagating to the snap cluster - `pan-viewport` — drag or scroll to pan the camera - `zoom` — drag or scroll to zoom - `center-nearest` — navigate toward the drag direction Any action from the [keybindings] Actions list also works on button and wheel-up/wheel-down triggers: `exec `, 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-" = "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 direction - `3-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 only - `N-finger-hold` — threshold only (fires on release) Continuous actions, locked to their triggers: - `pan-viewport` — swipe - `zoom` — pinch - `move-window` — swipe or doubletap-swipe - `move-snapped-windows` — swipe or doubletap-swipe - `resize-window` — swipe or doubletap-swipe - `resize-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)** ```toml "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-" = "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 direction - `N-finger-pinch` — continuous only (use pinch-in/out for discrete) - `N-finger-pinch-in/out` — threshold only - `N-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: ```text 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. On the winit backend only `position` applies — 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"). `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). - `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; an exact entry replaces the wildcard wholesale (fields don't merge), so 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:** ```toml [[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" [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 (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 mode - `focus_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). `position` becomes output-relative; movable unless widget = true. Toggle live with `toggle-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 shutdown 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 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 `position` resolved against it; dragging or send-to-output reassigns it afterward. Find names under `outputs.*` in `driftwm msg state`. (default: unset) - `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: ```text `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** ```toml [[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)** ```toml [[window_rules]] app_id = "factorio" pass_keys = ["ctrl+q"] ``` **Example: Compose rules, blur from first rule, opacity from second (both apply)** ```toml [[window_rules]] app_id = "Alacritty" blur = true [[window_rules]] app_id = "Alacritty" opacity = 0.9 ```