Config reference#
The complete annotated reference config — every knob margo exposes, with
inline commentary explaining both syntax and the why behind each
setting. This is the same file you get at
/usr/share/doc/margo-git/config.example.conf after installing the
package, and it lives in source at
margo/src/config.example.conf.
Strip what you don't need; everything has a sane built-in default, so blank entries fall back to the compiled-in value.
How to use this page
The example mixes declarative settings (look, animations,
input, layout) with rule-based matchers (window rules, layer
rules, tag rules) and bindings (keyboard, mouse, gesture).
The first half covers settings; the second half is keys + rules.
Every section header below maps 1:1 to a # ── ... ── divider in
the source file, so Ctrl+F from either side hits the same spot.
For a quick-start curated subset, see Configuration instead.
Reload + validation#
mctl reload # re-read the config in place (no logout)
mctl check-config # validate without applying — exits 1 on any error
Reloading replays every setting. Removing a key reverts to its
default; removing a bind = … line unbinds it; removing a
windowrule = … line removes the rule effect from already-mapped
windows on the next arrange.
mctl check-config runs offline (no Wayland connection needed). It
catches: unknown fields, regex compile errors, duplicate bind
detection (caught real shadowing in the maintainer's own config),
and include-resolution loops. Wire it into your editor on save or
your pre-commit hook.
Discoverability tools#
mctl actions --verbose # every dispatch action with examples
mctl actions --names # bare list (handy for shell completion)
mctl rules --appid X --title Y # which rules a hypothetical client hits
mctl status / clients / outputs # live state from the running compositor
mctl rules is the right tool for "why didn't my windowrule fire?"
— offline, runs against the same rule engine as the compositor.
The full file#
# ╔═══════════════════════════════════════════════════════════════════════════╗
# ║ margo — Wayland tiling compositor ║
# ║ Annotated reference config — copy to ~/.config/margo/config.conf ║
# ╚═══════════════════════════════════════════════════════════════════════════╝
#
# This file documents every knob margo exposes. It is a *manual*, not a
# minimal sample — every section explains both the syntax and the "why".
# Strip the parts you don't need; everything here has a sane built-in
# default, so blank entries simply fall back to the compiled-in value.
#
# ─── File format ────────────────────────────────────────────────────────────
#
# key = value — scalar setting
# key = value1, value2, ... — comma-separated list
# bind = MODIFIERS,key,action[,arg] — keyboard binding
# windowrule = field:value, ... — match-and-apply rule
#
# Whitespace around `=` and `,` is ignored. Lines starting with `#` are
# comments. Strings do not need quoting unless they contain `,`, `=` or `#`,
# in which case wrap them in `"..."`.
#
# ─── Variables ──────────────────────────────────────────────────────────────
#
# var = name, value — define, usable as $name / ${name}
#
# Reusable anywhere a value appears — binds, spawn commands, env, rules —
# in this file and in anything it `source`s (define before use). Only names
# you've actually defined this way get substituted; $HOME, $(date …), $1,
# $$ and an undefined $foo are left exactly as written, so shell commands
# in spawn/spawn_shell keep working untouched:
#
# var = term, kitty
# bind = super, Return, spawn, $term
# bind = super, e, spawn, $term -e nvim
#
# Pick distinctive names — defining e.g. `var = HOME, ...` would also
# expand $HOME inside spawn_shell commands, before the shell ever sees it.
#
# ─── Reload ─────────────────────────────────────────────────────────────────
#
# mctl reload — re-read this file in place
# Super+Ctrl+R — bound at the bottom of this file
# mctl check-config — validate without applying
#
# Reloading replays *every* setting. Removing a key reverts to its default;
# removing a `bind = ...` line unbinds it; removing a `windowrule` removes
# the rule effect from already-mapped windows on next arrange.
#
# `auto_reload_config = false` turns off the automatic part — this file
# (and everything it `source`s/`include`s) is watched by default, so a
# save already triggers the reload above on its own.
#
# ─── Discoverability ────────────────────────────────────────────────────────
#
# mctl actions --verbose — every dispatch action with examples
# mctl actions --names — bare list (handy for shell completion)
# mctl rules --appid X --title Y — show which window rules a hypothetical
# client would hit (great for debugging)
# mctl status / clients / outputs — live state from the running compositor
#
# ─── Tag model (read this once) ─────────────────────────────────────────────
#
# margo follows the dwm/dwl tradition: nine *tags* per monitor, not virtual
# desktops. A window can carry one or many tags. Viewing tag N means "show
# every window whose mask includes N". The mask is a bitfield:
#
# tag 1 → 1 tag 4 → 8 tag 7 → 64
# tag 2 → 2 tag 5 → 16 tag 8 → 128
# tag 3 → 4 tag 6 → 32 tag 9 → 256
#
# all tags → 4294967295 (u32::MAX, used by `view 0` below)
#
# Bitmasks compose: `view 6` shows tags 2 *and* 3 simultaneously
# (2 | 4 = 6). `view_current_to_back = 1` (set in section 3) makes
# pressing the same tag twice bounce you back to the previous tag — the
# classic dwm "alt-tab the workspace" feel.
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 1. Look — borders, gaps, opacity, colors, shadows, blur │
# ╰───────────────────────────────────────────────────────────────────────────╯
# Border thickness (px). 0 disables borders entirely. 2-4 is the sweet spot
# for HiDPI; anything thicker eats into client area on smaller laptops.
borderpx = 3
# Corner radius (px). Applied to every toplevel and to layer-shells that
# don't opt out via `noradius`. 0 = sharp dwm-style corners.
border_radius = 12
# When only one window is visible on a tag, hide its border / radius. This
# is the niri "single column" look — useful if you find borders distracting
# in monocle-equivalent situations.
no_border_when_single = 0
no_radius_when_single = 0
# Inner gap (between adjacent tiled windows) and outer gap (between tiles
# and the screen edge). Setting the four directly is the most flexible —
# the aliases `gaps`, `gaps_in`, `gaps_out` set all sides at once.
gappih = 12 # inner horizontal
gappiv = 12 # inner vertical
gappoh = 12 # outer horizontal
gappov = 12 # outer vertical
# Hide gaps automatically when only one tiled client is visible. Off by
# default — keeps the layout from jumping when you close the second pane.
smartgaps = 0
# monly: maximise the lone tiled window of a tag (arrange it as Monocle,
# whatever the active layout). Lets a single window fill the screen even in
# column layouts like scroller, where it would otherwise keep its column
# width. Off by default. Pairs well with smartgaps. (Port of the niri tool
# "oniri" — "only window" auto-maximise.)
monly = 0
# Per-window opacity. Focused = fully opaque, unfocused dimmed for cheap
# depth cueing. The cross-fade timing comes from `animation_curve_focus`
# below, so you can make the transition snappy or smoky to taste.
focused_opacity = 1.00
unfocused_opacity = 0.90
# Dim overlay: a translucent layer drawn over a window's content, so it can
# be shaded WITHOUT changing the app's own opacity (blur behind it stays
# intact). Colors are 0xRRGGBBAA — the last two digits are the overlay's
# transparency. It never takes input, follows border_radius and the window
# through animations, and is left out of screencasts. With just
# `dim_enable = 1` the focused window is untouched and unfocused windows get
# ~1/3 black.
# dim_enable = 1
# dim_focused_color = 0x00000000
# dim_unfocused_color = 0x00000055
# Pointer size (px, scaled per-output). 24 is the GTK/Adwaita default;
# bump to 32 on 4K displays without HiDPI scaling.
cursor_size = 24
# ── Palette (Catppuccin Mocha shown; format is 0xRRGGBBAA) ──────────────────
# Every color is a 32-bit hex with an explicit alpha byte. Keep the alpha
# at `ff` for solid colors; the shadow color uses partial alpha on purpose
# so it blends with whatever sits underneath.
rootcolor = 0x1e1e2eff # background under everything (visible if no wallpaper)
bordercolor = 0x313244ff # idle border
focuscolor = 0xcba6f7ff # focused border + tag indicator highlight
urgentcolor = 0xf38ba8ff # window requesting attention (xdg_activation, _NET_WM_DEMANDS_ATTENTION)
scratchpadcolor = 0x74c7ecff # scratchpad window border
floatingcolor = 0x94e2d5ff # focused floating window (unset = focuscolor)
globalcolor = 0xb4befeff # `setglobal` window — visible on every tag
overlaycolor = 0x89b4faff # `is_overlay:1` window border (e.g. screenshot editor)
shadowscolor = 0x11111b99 # SDF drop-shadow color; 0x99 alpha = ~60% black
# Tip: the mshell desktop shell can drive these border colors from the
# wallpaper's matugen palette. It writes ~/.config/margo/colors.conf on each
# scheme change and runs `mctl reload`. `source` that file *after* this block
# (see the bottom of this file) so the generated colors override the static
# ones above — rootcolor stays whatever you set here. A missing file is a
# silent no-op, so these values remain the fallback. Standalone (no mshell)
# setups just keep the static palette.
# ── Drop shadows / ambient glow ─────────────────────────────────────────────
# Shadows are SDF-rendered on the GPU — cheap on modern hardware. The glow
# should support elevation, not compete with the content: keep it SUBTLE
# (Material/GNOME feel the halo at ~5–10% of the surface). The matugen
# `shadowscolor` is a desaturated purple at a low ~8% alpha; size=14 /
# blur=26 / offset_y=4 keep it tight to the window with a soft falloff +
# slight downward grounding. Raising size/blur/alpha makes the halo
# dominate (the eye lands on the frame, not the content) — bump them only
# a little, and lower the alpha further if you widen.
# Larger values look great on high refresh + dark wallpapers; on integrated
# graphics consider `shadows_blur = 16` for headroom.
shadows = 1 # master switch
shadow_only_floating = 1 # tiled windows have no shadow (cleaner; matches mango default)
layer_shadows = 0 # bars/notifications/launchers: keep flat
shadows_size = 14 # outer radius of the shadow disc — hugs the window
shadows_blur = 26 # gaussian sigma in shader pixels — soft falloff
shadows_position_x = 0
shadows_position_y = 4 # subtle downward bias — light from above
# ── Blur ────────────────────────────────────────────────────────────────────
# Dual-Kawase background blur is implemented (render/blur.rs). It defaults
# OFF — set `blur = 1` to blur behind translucent windows, `blur_layer = 1`
# to blur behind layer-shell surfaces (panels/launchers). Per-window exclude
# with `windowrule = noblur:1, ...`; per-layer with `layerrule = noblur:1,
# ...`. The blur only shows where the surface is actually translucent, so
# pair it with an opacity rule for the effect to be visible. Tune the look
# with `blur_params_*` below.
#
# Note: blur forces a full-output redraw on every frame while a blurred
# surface is visible (the capture must see a freshly-drawn backdrop), so
# it costs idle power — leave it off unless you want the look. The
# `blur_optimized` key is accepted for compatibility but is a no-op: the
# damage-aware cache it briefly enabled (2026-07-19) shimmered/ghosted
# on hardware and was reverted.
blur = 0
blur_layer = 0
# ── Tabbed window groups (`togglegroup`) ────────────────────────────────────
# Hyprland-style window groups: merge windows into ONE tile that shows one
# member at a time, with a tab strip across the top. Purely opt-in — no
# windows are grouped until you bind the verbs or set a `group:1` windowrule.
#
# Dispatch verbs (bind them yourself; none are bound by default):
# togglegroup group focused window with its layout neighbour,
# or ungroup it if already grouped
# changegroupactive next|prev cycle the displayed member (wraps)
# movegroupwindow next|prev reorder focused window in the tab strip
# movewindowtogroup absorb focused window into a neighbour's group
# lockgroups on|off|toggle freeze group/ungroup (existing groups still cycle)
#
# Example binds:
# bind = super,g,togglegroup
# bind = super,Tab,changegroupactive,next
# bind = super+shift,Tab,changegroupactive,prev
# bind = super,c,movegroupwindow,next
# bind = super+shift,c,movegroupwindow,prev
# bind = super+alt,g,lockgroups,toggle
#
# The tab strip is the ONLY visual cue that a tile is a group. It defaults to
# height 0 → INVISIBLE: `togglegroup` still merges windows, but you see no bar
# and can't tell which tile is grouped. Raise `group_bar_height` to ~22 to draw
# it. Each chip shows the member's window title (app-id fallback), rendered with
# fontdue and truncated with an ellipsis to fit; the active member's chip uses
# `group_active_color`, the rest `group_inactive_color` (chip text auto-picks
# black/white for contrast). Cycle members with `changegroupactive`. Colours
# default to the focus/border palette and are matugen-overridable via colors.conf.
# (If no system font is found the labels are skipped but the chips still draw.)
group_bar_height = 22 # px; 0 = no visible strip (groups still work, just invisible)
group_bar_gap = 4 # px gap between tab chips
group_active_color = 0xcba6f7ff # active member chip (defaults to focuscolor)
group_inactive_color = 0x313244ff # inactive member chips (defaults to bordercolor)
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 2. Animations — six animation domains with independent tuning │
# ╰───────────────────────────────────────────────────────────────────────────╯
#
# margo distinguishes six animation *domains*:
#
# move — windows shifting between layout slots (tile reflow, etc.)
# open — a window appearing for the first time
# close — a window leaving (kept in `LyrFadeOut` until the curve ends)
# tag — switching the active tag set
# focus — the focus-fade cross-fade between focused/unfocused opacities
# layer — layer-shells (bars, launchers, notifications) appearing/leaving
#
# Each domain has three knobs:
#
# animation_clock_<domain> = bezier | spring
# animation_duration_<domain> = ms (bezier only / spring cap)
# animation_curve_<domain> = x1,y1,x2,y2 (bezier control points)
#
# The clock decides *how time progresses*. `bezier` plays a fixed curve
# over `duration`. `spring` is physical: it preserves velocity if you
# retarget mid-flight (drag → release → drag again). Use `spring` for
# anything where the user can interrupt the animation; use `bezier` for
# discrete snap-stop transitions.
animations = 1 # global on/off (off → instant transitions)
layer_animations = 1 # off → bars/launchers appear instantly
# ── Tag-switch geometry ─────────────────────────────────────────────────────
# 0 = horizontal slide (tag N+1 enters from the right, tag N exits left)
# 1 = vertical slide (paired with `gesturebind` 3-finger up/down below)
tag_animation_direction = 0
# Carousel tag cycling: when relative tag navigation (viewtoleft /
# viewtoright) wraps past the first/last tag, slide the animation in the
# travel direction (9→1 continues rightward) instead of a long reverse.
tag_carousel = 0
# Compact occupied tags to consecutive numbers on every arrange, closing
# gaps left by closed/moved windows — e.g. windows on tags 1, 3 and 9 move
# to 1, 2 and 3, and the current view follows. Off by default because it
# renumbers tags out from under you, which surprises anyone relying on a
# fixed "tag N is always my terminal" muscle memory.
tag_gather = 0
# ── Open / close window animation type ──────────────────────────────────────
# Values: zoom | fade | slide_in_<dir> | slide_out_<dir> | none.
# `slide` is an alias for slide_in_right / slide_out_right.
animation_type_open = zoom
animation_type_close = fade
# Layer-shell open/close: most bars and launchers feel right with a slide.
# Override per-namespace via `layerrule = animation_type_open:zoom, ...` in
# section 8.
layer_animation_type_open = slide
layer_animation_type_close = slide
# Optional alpha-fade overlaid on whatever geometric animation runs.
# `fadein_begin_opacity = 0.72` means the window pops in at 72% opaque,
# rises to 100% over the animation. Set both to 0 to drop the alpha
# component entirely.
animation_fade_in = 1
animation_fade_out = 1
fadein_begin_opacity = 0.72
fadeout_begin_opacity = 1.00
# Zoom strength — both ends tend to look best at the same value.
# 0.94 = window starts/ends at 94% scale. 0.85 = pronounced "spring" feel
# (good on desktops); 0.96 = barely perceptible (good on laptops where
# the animation is on every spawn).
zoom_initial_ratio = 0.94
zoom_end_ratio = 0.94
# ── Per-domain durations (ms) ───────────────────────────────────────────────
# These are upper bounds for spring-clocked domains and the actual playback
# length for bezier-clocked ones. Numbers below are tuned to feel close to
# niri's defaults, which most users have already calibrated their eyes to.
animation_duration_move = 220
animation_duration_open = 180 # ease-out-expo, niri default
animation_duration_close = 160 # ease-out-quad, niri default
animation_duration_tag = 280
animation_duration_focus = 120 # snappy border + opacity cross-fade
# ── Bezier control points ───────────────────────────────────────────────────
# Format is `x1,y1,x2,y2` — same as CSS `cubic-bezier()`. Useful presets:
#
# ease-out-expo : 0.16, 1.00, 0.30, 1.00 — snap-stop, sharp finish
# ease-out-quad : 0.25, 0.46, 0.45, 0.94 — gentle ease
# ease-out-cubic : 0.33, 1.00, 0.68, 1.00 — between the two
# linear : 0.00, 0.00, 1.00, 1.00 — for opacity-only fades
#
# For spring-clocked domains the curve is consulted only on the rare
# bezier fallback path (e.g. very short retargets), so it's safe to leave
# the niri values everywhere.
animation_curve_move = 0.16, 1.00, 0.30, 1.00
animation_curve_open = 0.16, 1.00, 0.30, 1.00
animation_curve_close = 0.25, 0.46, 0.45, 0.94
animation_curve_tag = 0.16, 1.00, 0.30, 1.00
animation_curve_focus = 0.33, 1.00, 0.68, 1.00
animation_curve_opafadein = 0.16, 1.00, 0.30, 1.00
animation_curve_opafadeout = 0.50, 0.50, 0.50, 0.50
# ── Clock selection per domain ──────────────────────────────────────────────
# Use spring where you want velocity preservation through interruption:
# move → drag a window between slots while the previous tween still runs
# Use bezier for discrete snap-stops:
# open / close / focus / layer — these are not meaningfully retargetable
#
# The default below mirrors niri: spring for movement, bezier for everything
# else. Note `tag` is bezier in margo even though niri uses spring there;
# margo animates each window separately on tag switches (niri animates a
# single scene block), and overlapping springs were producing visual
# wobble across many parallel windows.
animation_clock_move = spring
animation_clock_open = bezier
animation_clock_close = bezier
animation_clock_tag = bezier
animation_clock_focus = bezier
animation_clock_layer = bezier
# ── Spring tuning ───────────────────────────────────────────────────────────
# margo currently has one global spring. niri's per-domain stiffnesses
# average ~770 (move=750, resize=850, workspace=700, view=750); 800 is a
# good middle ground.
# stiffness ↑ → snappier / less overshoot
# damping_ratio = 1.0 → critically damped (no overshoot, fastest settle)
# mass = 1.0 → keep at 1.0; tweak stiffness instead
animation_spring_stiffness = 800.0
animation_spring_damping_ratio = 1.0
animation_spring_mass = 1.0
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 3. Behaviour — focus, drag, snap, scratchpad, hot corner, sync │
# ╰───────────────────────────────────────────────────────────────────────────╯
# When a client requests activation (xdg_activation_v1 token), focus
# immediately. Set 0 if you find browser tab notifications too aggressive;
# the window will still light up its `urgentcolor` border.
focus_on_activate = 1
# Should focus moves cross monitor boundaries?
# focus_cross_monitor — `focusdir` (h/j/k/l) hops to the next output
# exchange_cross_monitor — `exchange_client` does the same
# focus_cross_tag — `focusdir` walks across tag groups too
# All three default off — predictable focus is more important than reach
# for most users; cross-output movement gets explicit `focusmon` /
# `tagmon` keys further down.
focus_cross_monitor = 0
exchange_cross_monitor = 0
focus_cross_tag = 0
# `view_current_to_back = 1`: pressing Super+N for the *current* tag jumps
# back to the previously-viewed tag (the classic dwm two-tag bounce).
view_current_to_back = 1
# Sloppy focus — pointer-follows-focus. Highly recommended; pairs with
# `warpcursor` so keyboard-driven focus changes also drag the pointer.
sloppyfocus = 1
warpcursor = 1
# Hide the cursor after N seconds of no movement. 0 disables. Useful on
# media playback monitors; some games will fight this with their own
# cursor manager — increase to 10+ if it flickers.
cursor_hide_timeout = 3
# Hidden-tag warm-up: after a window first maps, keep sending frame callbacks
# for this many ms even if the window starts on a background tag. This lets
# frame-throttled Electron / CEF apps launched at login (Spotify, Webcord,
# Ferdium, Discord, etc.) finish initialising before you visit their tag.
# Set 0 for strict "only visible windows get frames" behaviour.
warmup_hidden_ms = 10000
# ── Mouse-drag rearrange ────────────────────────────────────────────────────
# Holding `super+lmb` (see `mousebind` below) drags a tiled window into
# another slot — and, on a `mosaic` tag, drags a mosaic-packed window onto
# another one to swap their order. `drag_corner` picks the grab anchor:
# 0 top-left 1 top-right 2 bottom-left 3 bottom-right 4 auto
drag_tile_to_tile = 1
drag_corner = 4
drag_warp_cursor = 1
drag_tile_refresh_interval = 0 # ms; 0 = once per frame (refresh rate)
drag_floating_refresh_interval = 0
# `mosaic` layout only: when even shrinking every window to its own minimum
# size still doesn't fit, move the most recently opened offending window to
# the first empty tag on the same monitor instead of leaving it crushed.
mosaic_auto_overflow_tag = 1
# Only consulted when mosaic_auto_overflow_tag is also on. When enabled, an
# overflowing window doesn't leave the tag — it shrinks to a small,
# always-visible "peek" in the work area's bottom-right corner instead
# (click it to bring it back into the pack). Off by default, which moves it
# to an empty tag instead.
mosaic_overflow_stack = 0
# ── Floating snap ───────────────────────────────────────────────────────────
# Snap floating windows to other windows / monitor edges within
# `snap_distance` pixels. Set 0 to disable; 30 px is comfortable.
enable_floating_snap = 1
snap_distance = 30
# ── Hot corner (overview trigger) ───────────────────────────────────────────
# A `hotarea_size`-px square in `hotarea_corner` toggles the overview when
# the pointer rests on it.
# hotarea_corner: 0 top-left 1 top-right 2 bottom-left 3 bottom-right
enable_hotarea = 1
hotarea_size = 10
hotarea_corner = 3
ov_tab_mode = 0 # legacy alias: 1 = overview_style = scroller (0 changes nothing)
overviewgappi = 12 # gap between mini-windows in overview
overviewgappo = 60 # gap from overview thumbs to monitor edge
# ── Overview (zoom-out grid) ────────────────────────────────────────────────
# Mango-ext semantics: `void overview(Monitor *m) { grid(m); }` — a single
# dynamic Grid layout over every visible client across all tags. Cell count
# = window count, NOT 9: 1 window ≈ 90% × 90% centred, 2 → halves, 4 → 2×2
# quarters, 9 → 3×3 evenly.
overview_transition_ms = 180 # open/close slide duration (ms)
overview_zoom = 1.0 # 1.0 = full work area; drop to 0.85–0.9 for
# breathing room around the grid
# Selection emphasis. While overview is open, the keyboard-cycle / pointer-
# hover "selected" thumbnail gets:
# (1) a thicker focuscolor border — `overview_selected_border_multiplier`
# multiplies the normal `borderpx` (1.0 = same, 1.5–2.0 popular,
# clamped [1.0, 4.0]).
# (2) every OTHER thumbnail's content alpha drops to `overview_dim_alpha`
# for the cinematic spotlight feel (Hypr/niri default look). Selected
# always renders at 1.0. Clamped [0.1, 1.0]; set to 1.0 to disable
# dimming.
overview_selected_border_multiplier = 1.6
overview_dim_alpha = 0.6
# Alt+Tab cycle order — three modes:
# mru → focus_history first (most-recent first), then any remaining
# visible clients in clients-vec order. Matches the muscle
# memory of i3 / sway / Hypr / niri / GNOME — cycle order
# reflects how the user actually navigates between windows.
# tag → strict tag 1 → 9 order, clients-vec sequence inside each tag.
# Spatial-memory model: tag 1's windows always come first,
# tag 9's always last, independent of focus history.
# mixed → current tag's clients in MRU order, remaining tags in strict
# tag order. The "MRU where you live, tag elsewhere" hybrid.
# Unknown / typo'd value falls back to `mru` with a warn log.
overview_cycle_order = mru
# ── MRU window switcher (Super/Alt+Tab) ──────────────────────────────────────
# A second, separate alt-tab: niri-style. Hold a modifier, tap Tab to walk
# a most-recently-used window list — each step live-focuses the pick so you
# see where you are even before the overlay lands — then release the
# modifier to commit, or Esc to cancel back to whatever was focused when
# you opened it. Bound via the `mru_next` / `mru_prev` dispatch actions
# (see "Window management" below); this section only tunes the overlay
# itself and the scope/filter a bind uses when it doesn't pass its own.
mru_scope = all # default scope: all | output | workspace
mru_filter = all # default filter: all | appid
mru_max = 8 # max thumbnails shown in the strip — the
# cycle still walks every window regardless
mru_thumb_height = 170 # thumbnail height, logical px
mru_thumb_gap = 16 # gap between thumbnails, logical px
mru_panel_padding = 22 # inner padding of the switcher panel, logical px
mru_show_labels = 1 # app-id label under each thumbnail
mru_dim_alpha = 0.35 # backdrop dim behind the overlay, 0.0-0.9 (0 = off)
mru_accent_selection = 1 # tint the selection ring + labels with
# focuscolor instead of neutral grey
# ── Scroll axis tuning ──────────────────────────────────────────────────────
# `axisbind` (see keybinds) consumes whole-tick events. The factor scales
# pointer-warp / scroll-as-input when no axis bind is engaged.
axis_bind_apply_timeout = 100 # ms before a held modifier resumes scroll
axis_scroll_factor = 1.25
# ── Sync / tearing ──────────────────────────────────────────────────────────
# `syncobj_enable = 1` enables explicit sync (DXVK / Vulkan, modern GPU
# drivers). Some older drivers regress; keep at 0 unless you actively use
# Proton / native Vulkan and have verified the stack supports it.
syncobj_enable = 0
# `allow_tearing`:
# 0 = never tear 1 = always tear (immediate flips)
# 2 = tear only when a window-rule sets `force_tearing:1` ← recommended
allow_tearing = 2
# Allow shortcut-inhibitor (e.g. Citrix, RDP, browser fullscreen captures)
# to grab modifier keys away from the compositor. If you disable, those
# clients will never see Super+Tab.
allow_shortcuts_inhibit = 1
# Should a client's idle-inhibit request count while it's off-screen (a
# different tag, minimized, hidden scratchpad)?
# 0 = no, only visible inhibitors count (matches mango's default).
# 1 = yes, e.g. a background audio player keeps the screen awake even
# when its window isn't the one you're looking at.
idleinhibit_ignore_visible = 0
# Keep idle timers inhibited while the focused window is fullscreen, even
# if it never requests idle-inhibit itself (covers fullscreen video apps
# without native idle-inhibit support). mango 0.17 port.
idle_inhibit_when_fullscreen = 0
# ── Scratchpad behaviour ────────────────────────────────────────────────────
# A scratchpad is a window flagged with `isnamedscratchpad:1` that lives
# off-screen until summoned by `toggle_named_scratchpad` (see binds).
scratchpad_cross_monitor = 1 # follow the focus monitor when toggling
single_scratchpad = 1 # only one scratchpad visible at a time
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 4. Input — keyboard layout + libinput tuning │
# ╰───────────────────────────────────────────────────────────────────────────╯
# ── Keyboard ────────────────────────────────────────────────────────────────
# These map straight to `xkbcommon`. Look at `localectl list-x11-keymap-*`
# for valid values. Multiple layouts: comma-separate, e.g. `tr, us`.
xkb_rules_layout = us
xkb_rules_variant =
xkb_rules_options = ctrl:nocaps # CapsLock acts as Ctrl. Multi: comma-list.
repeat_rate = 35 # keys/sec while held
repeat_delay = 250 # ms before repeat starts
numlockon = 0 # turn NumLock on at startup
# ── Touchpad (libinput) ─────────────────────────────────────────────────────
tap_to_click = 1
tap_and_drag = 1
drag_lock = 1
trackpad_natural_scrolling = 0
disable_while_typing = 1
left_handed = 0
middle_button_emulation = 1
click_method = 2 # 1 = button-areas, 2 = clickfinger
scroll_method = 1 # 1 = 2-finger, 2 = edge, 3 = on-button
scroll_button = 274 # only used when scroll_method = 3
send_events_mode = 0 # 0 = enabled, 1 = disabled, 2 = disabled-on-mouse
button_map = 0
# ── Mouse ───────────────────────────────────────────────────────────────────
mouse_natural_scrolling = 0
accel_profile = 2 # 0 none, 1 adaptive, 2 flat
accel_speed = 1.00 # range -1.0 .. +1.0 (libinput); higher = faster
# Minimum 3- or 4-finger swipe distance before a `gesturebind` fires.
# 30 px (mango/niri default) reads as deliberate intent. Drop to 15-20
# for small touchpads or short-flick habits.
swipe_min_threshold = 30
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 5. Environment variables │
# ╰───────────────────────────────────────────────────────────────────────────╯
#
# Set BEFORE any `exec-once` clients spawn. These are exported into every
# child process — set anything you'd otherwise put in your shell rc only
# for graphical sessions. A leading `~/` (also after `:`, `=` or a space,
# so PATH-style lists work) expands to $HOME, e.g.
# env = PATH, ~/.local/bin:/usr/bin
env = XDG_CURRENT_DESKTOP, margo:mango # `:mango` keeps mango portal rules working
env = XDG_SESSION_DESKTOP, margo
env = XDG_SESSION_TYPE, wayland
env = GDK_BACKEND, wayland,x11
env = QT_QPA_PLATFORM, wayland;xcb # Qt uses ;-separated platform list
env = SDL_VIDEODRIVER, wayland
env = CLUTTER_BACKEND, wayland
# env = MOZ_ENABLE_WAYLAND, 1 # Firefox ≤ESR 115 still needed this
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 6. Layouts & per-tag layout pinning │
# ╰───────────────────────────────────────────────────────────────────────────╯
#
# Available layouts:
#
# tile — dwm classic: master + stack
# scroller — PaperWM-style horizontal column scroller (the default)
# grid — equal-area grid
# monocle — one window fullscreens; siblings hidden
# deck — master + stacked tabs
# center_tile — master centered, stacks left + right
# dwindle — recursive split (Hyprland default)
#
# `default_layout` applies to any tag that has no `tagrule` below.
default_layout = scroller
default_mfact = 0.55 # master pane fraction (0.05 .. 0.95)
default_nmaster = 1 # how many windows live in the master area
new_is_master = 0 # 1 = new windows take the master slot
center_master_overspread = 0 # center_tile: with no stack windows the master fills the full width (0 = keep mfact width, centred)
center_when_single_stack = 1 # center_tile: when only one stack pane, center the master
# `circle_layout` defines what `switch_layout` cycles through. Order matters.
circle_layout = scroller, tile, center_tile, grid, deck, monocle, dwindle, mosaic, floating
# ── Scroller-specific ───────────────────────────────────────────────────────
# scroller is the most option-heavy layout, hence its own block.
#
# scroller_structs — internal ring-buffer size (24 fits
# any reasonable session)
# scroller_default_proportion — column width ratio for new windows
# when ≥2 columns are visible
# scroller_default_proportion_single — width when only one column shows
# scroller_ignore_proportion_single — ignore the *_single value above
# scroller_focus_center — auto-center focused column
# scroller_prefer_center — open new columns near the focused
# one rather than at the end
# scroller_prefer_overspread — let columns exceed monitor width
# edge_scroller_pointer_focus — moving the pointer to the edge
# scrolls and re-focuses
# scroller_proportion_preset — `switch_proportion_preset` cycles
# through this list (Φ = 0.618)
scroller_structs = 24
scroller_default_proportion = 0.800
scroller_default_proportion_single = 0.800
scroller_ignore_proportion_single = 0
scroller_focus_center = 1
scroller_prefer_center = 1
scroller_prefer_overspread = 0
edge_scroller_pointer_focus = 1
# Speed gate for edge_scroller_pointer_focus (per-event pointer motion
# magnitude). 0 = disabled (default). Set e.g. 40 to shift focus to the
# adjacent column when the pointer rests slowly at a scroller edge, while
# ignoring fast cross-screen sweeps. Range 0–1000.
edge_scroller_focus_allow_speed = 0
scroller_proportion_preset = 1.000, 0.800, 0.618, 0.500
# ── Per-tag pinning (tagrule) ───────────────────────────────────────────────
# `tagrule` does two things:
#
# 1. Set the layout for that tag (`layout_name:`)
# 2. Pin that tag to a "home" monitor (`monitor_name:`) — `view N`
# automatically warps focus to that monitor, and any `windowrule`
# that targets `tags:N` inherits this monitor implicitly.
#
# Comment out `monitor_name:` to leave a tag floating between monitors.
# Comment out a `tagrule` line entirely to use `default_layout` for that
# tag with no monitor pin.
#
# Below: tags 1-6 home on the primary, 7-9 home on the laptop panel — a
# common dual-monitor "communications on the side, work on the main"
# split. Adjust the connector names to your hardware (`mctl outputs`
# lists what margo currently sees).
#
# tagrule = id:1, layout_name:scroller, monitor_name:DP-3
# tagrule = id:2, layout_name:scroller, monitor_name:DP-3
# tagrule = id:3, layout_name:scroller, monitor_name:DP-3
# tagrule = id:4, layout_name:scroller, monitor_name:DP-3
# tagrule = id:5, layout_name:scroller, monitor_name:DP-3
# tagrule = id:6, layout_name:scroller, monitor_name:DP-3
# tagrule = id:7, layout_name:scroller, monitor_name:eDP-1
# tagrule = id:8, layout_name:scroller, monitor_name:eDP-1
# tagrule = id:9, layout_name:scroller, monitor_name:eDP-1
# Single-monitor fallback: every tag uses scroller, no monitor pinning.
tagrule = id:1, layout_name:scroller
tagrule = id:2, layout_name:scroller
tagrule = id:3, layout_name:scroller
tagrule = id:4, layout_name:scroller
tagrule = id:5, layout_name:scroller
tagrule = id:6, layout_name:scroller
tagrule = id:7, layout_name:scroller
tagrule = id:8, layout_name:scroller
tagrule = id:9, layout_name:scroller
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 7. Window rules │
# ╰───────────────────────────────────────────────────────────────────────────╯
#
# A window rule is a comma-separated bag of `key:value` pairs. All
# *positive* keys (appid, title, …) AND together — a window must match
# every one. Then `exclude_appid` / `exclude_title` are evaluated last and
# can disqualify the rule.
#
# `windowrule-once = ...` (same key:value syntax) applies only to the
# first client that ever matches it, then is consumed for the rest of
# the session — e.g. auto-float exactly one instance of a launcher
# script's terminal without touching every terminal after it. A
# `mctl reload` resets consumption.
#
# ─── Match keys ─────────────────────────────────────────────────────────────
#
# appid:REGEX — match xdg_toplevel `app_id` (or X11 WM_CLASS)
# title:REGEX — match the window title
# exclude_appid:REGEX — skip this rule if the appid matches
# exclude_title:REGEX — skip this rule if the title matches
#
# Regexes are anchored with `^...$` by convention — a partial match can
# accidentally pick up another window. Rust's `regex` crate is used; it
# does NOT support PCRE lookaround (`(?!...)`). Use `exclude_*` for that.
# Use `(?i)` at the start of a pattern for case-insensitive matching.
#
# ─── Effect keys ────────────────────────────────────────────────────────────
#
# Tag / monitor placement
# tags:N — open in tag bitmask (1 << (N-1))
# monitor:NAME — pin to a specific output (e.g. DP-3)
# If a tagrule pins that tag, this is implicit.
#
# Floating geometry (any width/height makes the window floating)
# isfloating:0|1
# width / height — initial size (px)
# offsetx / offsety — offset from monitor center; negative = up/left
# min_width / min_height — clamp lower bound (also affects tiling)
# max_width / max_height — clamp upper bound (e.g. PiP players)
#
# State
# isfullscreen:0|1
# open_focused:0|1 — niri's "open-focused"; `0` = no_focus on map
# isnamedscratchpad:1 — open as a hidden scratchpad slot
#
# Privacy
# block_out_from_screencast:1 — hide from wlr-screencopy / portal streams
# (password managers, incognito tabs, …)
#
# Visuals (override per-window)
# focused_opacity / unfocused_opacity (0.0 .. 1.0)
# scroller_proportion / scroller_proportion_single
# isnoborder:1 isnoshadow:1 isnoradius:1 isnoanimation:1
# noblur:1 is_overlay:1 force_tearing:1
#
# Behaviour
# isterm:1 — flag as terminal (terminal swallowing — not implemented yet)
# noswallow:1 — (terminal swallowing — not implemented yet)
# group:1 — auto-merge into a tabbed window group with other
# same-app-id windows on the same tag (see the
# "Tabbed window groups" config block above).
# Example: windowrule = group:1, appid:^kitty$
# idle_inhibit_when_focus:1 — keep idle timers inhibited while this
# client has focus, even if it never requests
# idle-inhibit itself (e.g. a DRM-less video
# player). See idle_inhibit_when_fullscreen
# below for the fullscreen-wide equivalent.
# confine_pointer:1 — while focused + visible, force the cursor to
# stay inside this window. For X11 (Proton/Wine)
# games whose own pointer-confine can get
# dropped by Xwayland, letting the cursor
# escape to another monitor. Doesn't need the
# client to use the pointer-constraints
# protocol; yields to a real protocol-level
# lock/confine if one is active.
# Example: windowrule = confine_pointer:1, appid:steam_app_526870
# focus_on_xdg_activate:0 — deny focus-steal from xdg-activation
# requests on this client (e.g. a
# Picture-in-Picture popup re-activating
# itself); marked urgent instead of focused.
# Default (unset/1) keeps today's behaviour.
#
# Debug a rule with `mctl rules --appid X --title Y` — it prints which
# rules a hypothetical client would hit, in order.
# ── Tag-pinned applications ─────────────────────────────────────────────────
# When a tagrule pins the tag to a monitor (section 6), placing a window
# with `tags:N` automatically lands on that monitor — no need for
# `monitor:NAME` here.
windowrule = tags:1, appid:^(firefox|librewolf)$
windowrule = tags:2, appid:^(kitty|Alacritty|foot|com\.mitchellh\.ghostty)$
windowrule = tags:3, appid:^(Code|code|vscodium|neovide)$
windowrule = tags:5, appid:^(discord|WebCord|signal|element|telegram-desktop)$
# Electron apps frequently leak as `app_id="electron"` regardless of which
# product they ship. Match the title to disambiguate.
windowrule = tags:5, appid:^electron$, title:WebCord
windowrule = tags:5, appid:^(electron|Electron)$, title:Discord
# Music + media on tag 8.
windowrule = tags:8, appid:^(spotify|Spotify|com\.spotify\.Client)$
windowrule = tags:8, appid:^mpv$
# ── Browsers, editors, terminals — scroller proportions ─────────────────────
# Wider columns for content-heavy clients, slightly narrower for editors.
windowrule = scroller_proportion:0.85, scroller_proportion_single:0.9, appid:^(firefox|librewolf|brave-browser|Chromium|chrome)$
windowrule = scroller_proportion:0.85, appid:^(kitty|Alacritty|foot|com\.mitchellh\.ghostty)$
windowrule = scroller_proportion:0.80, appid:^(Code|code|vscodium|neovide)$
# ── Floating dialogs / utility windows ──────────────────────────────────────
# Most "transient" pop-ups should float at a sensible default size. Width
# / height implicitly mark the window as floating.
# (the setup wizard is now an in-shell layer-shell menu, not a window —
# mshell auto-opens it on first launch, or run `mshellctl wizard`)
windowrule = isfloating:1, width:960, height:720, appid:^(xdg-desktop-portal-(gtk|gnome|wlr))$
windowrule = isfloating:1, width:600, height:240, appid:^(polkit-gnome-authentication-agent-1|gcr-prompter|pinentry.*)$
windowrule = isfloating:1, width:640, height:260, title:^(Authentication Required|Unlock Keyring|OpenSSH Authentication Passphrase)$
windowrule = isfloating:1, width:960, height:720, title:^(Open File|Choose (File|Folder)|Save As|Confirm to replace files|File Operation Progress)$
windowrule = isfloating:1, width:900, height:650, appid:^(blueman-manager|nm-connection-editor|org\.gnome\.Settings|gnome-disks|pavucontrol|pwvucontrol|virt-manager)$
windowrule = isfloating:1, width:540, height:640, appid:^(org\.gnome\.Calculator|kcalc|qalculate-gtk)$
windowrule = isfloating:1, width:960, height:720, appid:^(org\.gnome\.FileRoller|file-roller)$
# ── Named scratchpads ───────────────────────────────────────────────────────
# These windows start hidden. The matching `toggle_named_scratchpad` bind
# (section 11) shows / hides them on demand. `offsetx`/`offsety` position
# them relative to the focused monitor's center.
windowrule = isnamedscratchpad:1, width:1980, height:660, offsetx:0, offsety:-100, appid:^dropdown-terminal$
windowrule = isnamedscratchpad:1, width:1600, height:900, appid:^yazi-scratchpad$
windowrule = isnamedscratchpad:1, width:760, height:960, appid:^clipse$
# ── mpv main window vs. Picture-in-Picture popup ────────────────────────────
# Treat the PiP popup separately from the main mpv window. Rust regex has
# no lookahead, so we use `exclude_title` to keep the main rule from
# capturing the popup.
windowrule = isfloating:1, width:640, height:360, appid:^mpv$, exclude_title:^(Picture-in-Picture|Picture-in-Picture - mpv)$
# Pin browser/mpv PiP popups at exactly 640x360. min_*/max_* clamps stop
# the layout from inflating the window when its column would be wider.
windowrule = isfloating:1, width:640, height:360, min_width:640, max_width:640, min_height:360, max_height:360, title:^(Picture-in-Picture|Picture-in-Picture - mpv)$, open_focused:1
# ── Privacy: hide sensitive windows from screencast streams ─────────────────
# `block_out_from_screencast:1` replaces the window with a solid black
# rectangle in any wlr-screencopy / portal stream. The window is still
# fully visible to *you* — only screen recordings / share-screen flows
# see the blackout.
windowrule = block_out_from_screencast:1, appid:^(org\.keepassxc\.KeePassXC|KeePassXC|com\.bitwarden\.desktop|Bitwarden|com\.1password\.1Password|1Password|com\.github\.hluk\.copyq|copyq|gcr-prompter|pinentry.*|polkit-gnome-authentication-agent-1)$
# Browser private/incognito windows — match by title, since the appid is
# the same as a normal browser.
windowrule = block_out_from_screencast:1, appid:^(firefox|librewolf|brave-browser|Chromium|chrome)$, title:(?i)(private browsing|private window|incognito|inprivate)
# Generic auth-prompt fallback — catches anything the appid list above
# missed, just by title content.
windowrule = block_out_from_screencast:1, title:(?i)(password|passphrase|authentication|unlock|two-factor|verification code|one-time|otp)
# ── Games / remote play: tearing on, blur/shadow/animation off ──────────────
# `force_tearing:1` only takes effect when `allow_tearing = 2` (section 3).
windowrule = force_tearing:1, noblur:1, isnoshadow:1, isnoanimation:1, appid:^(gamescope|steam_app_.*|lutris|net\.lutris\.Lutris|com\.heroicgameslauncher\.hgl)$
# ── Terminal swallowing ─────────────────────────────────────────────────────
# NOT IMPLEMENTED YET: `isterm` / `noswallow` are accepted (mango
# compatibility) but margo does not hide a terminal behind the graphical
# child it spawns. The rules below are kept so they start working once
# swallowing lands.
windowrule = isterm:1, appid:^(kitty|Alacritty|foot|com\.mitchellh\.ghostty)$
windowrule = noswallow:1, appid:^(mpv|org\.telegram\.desktop|Discord|WebCord|Signal|Slack)$
# ── Screenshot editors (swappy / satty) ─────────────────────────────────────
# `is_overlay:1` lifts the window above tiled siblings while editing a
# screenshot. `noswallow:1` keeps the parent terminal visible.
windowrule = isfloating:1, width:1700, height:1100, max_width:1700, max_height:1100, open_focused:1, noswallow:1, is_overlay:1, appid:^(swappy|satty|com\.github\.jtheoof\.swappy|io\.github\.gabm\.satty)$
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 8. Layer rules — tweak layer-shell surfaces by namespace │
# ╰───────────────────────────────────────────────────────────────────────────╯
#
# A `layerrule` matches a layer-shell namespace (the string the client
# passes to `zwlr_layer_shell_v1.get_layer_surface`). Common namespaces:
# waybar, fuzzel, rofi, wofi, swaync, mako, fnott,
# noctalia-{bar,launcher,dash,notifications,osd,...}.
#
# Effect keys:
# noanim:1 noshadow:1 noblur:1
# animation_type_open:zoom|slide|fade animation_type_close:...
# Selection / screenshot UIs: instant, no blur — never make the user wait.
layerrule = noanim:1, noblur:1, layer_name:^(selection|screenshot).*
# Launchers (rofi, fuzzel, wofi): zoom-in feels right for a search-and-go.
layerrule = animation_type_open:zoom, animation_type_close:zoom, layer_name:^(rofi|fuzzel|wofi|launcher).*
# Control-centers / dashboards: horizontal slide matches the global default.
layerrule = animation_type_open:slide, animation_type_close:slide, layer_name:^(control-center|dash|session-menu|wallpaper|settings).*
layerrule = noshadow:1, layer_name:^(control-center|dash|session-menu|wallpaper|settings).*
# Notifications / OSD / volume popups: instant. The user is often blind-
# typing volume keys; the animation budget is zero.
layerrule = noanim:1, noshadow:1, noblur:1, layer_name:^(notifications|toast|osd|volume|brightness).*
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 9. Monitor rules │
# ╰───────────────────────────────────────────────────────────────────────────╯
#
# Format: `monitorrule = name:NAME, key:value, ...`
#
# name:NAME — connector name (DP-1, eDP-1, HDMI-A-1; see `mctl outputs`)
# width:N — preferred mode width (px); 0 = output's preferred mode
# height:N — preferred mode height
# refresh:N — refresh in Hz; needs width + height. Without it the
# highest rate at width × height is used.
# custom:1 — use exactly width × height @ refresh as a generated
# (CVT) mode, even if the panel doesn't advertise it.
# Falls back to an advertised mode if the driver refuses.
# x:N / y:N — logical position, set both. Omit both → auto-arrange left-to-right.
# scale:F — fractional scale (1.0, 1.25, 1.5, 2.0)
# rr:N — transform: 0=normal 1=90° 2=180° 3=270° 4=flipped 5..7=flipped+rot
# vrr:0|1 — variable refresh rate (FreeSync / Adaptive-Sync)
# adaptive_sync:0|1 — alias for vrr
#
# Live changes (e.g. via `wlr-randr` or `mctl dispatch dpms ...`) override
# these for the current session. To persist a layout across reboots,
# either edit these lines or use `mlayout` to manage named profiles —
# the bottom of this file shows how to include `mlayout.conf`.
#
# Examples (commented; uncomment + adjust to your hardware):
#
# monitorrule = name:DP-3, width:2560, height:1440, refresh:60, x:0, y:0, scale:1
# monitorrule = name:eDP-1, width:1920, height:1200, refresh:60, x:320, y:1440, scale:1
# monitorrule = name:HDMI-A-1, width:1920, height:1080, refresh:144, vrr:1
# Advertise `wp_color_management_v1` so HDR-aware clients (mpv,
# Chromium) negotiate colour management. Opt-in while the probe path
# is being verified against real clients — flip to 1 to test HDR.
# Globals register at startup only: changing this needs a re-login,
# `mctl reload` is not enough.
color_management = 0
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 10. Startup commands │
# ╰───────────────────────────────────────────────────────────────────────────╯
#
# `exec-once` runs ONCE at compositor startup; it does NOT re-run on
# `mctl reload` (so reload doesn't respawn your bar when you tweak a colour).
# `exec` (no `-once`) re-spawns on reload. Most daemons, though, are better
# managed by `systemctl --user`.
# ── The first-party shell: mshell ───────────────────────────────────────────
# mshell IS the desktop shell. It ships the bar(s), menus, notifications, OSD,
# wallpaper, app launcher, dock, clipboard history, the Polkit auth-prompt
# agent, the lock screen, and a Settings GUI. With mshell you do NOT need any
# of the standalone daemons further down — no separate notifier, wallpaper
# tool, or polkit agent.
#
# mshell runs as a **systemd --user service** bound to `margo-session.target`,
# so it starts automatically at login — there is deliberately no
# `exec-once = mshell`. Drive it with systemctl, not the compositor:
# systemctl --user status mshell
# systemctl --user restart mshell # e.g. after rebuilding it
#
# First-launch setup wizard: an in-shell layer-shell menu (NOT a compositor
# exec-once). mshell opens it the first time it starts with no saved profile;
# re-open it any time with `mshellctl wizard` (or the `mwizard` shim).
# ── External shells / daemons (only if you are NOT using mshell) ─────────────
# margo also exposes state via the standard `ext-workspace` protocol plus its
# own Unix-socket IPC (`mctl get/watch/dispatch`), so ext-workspace bars
# (sfwbar, ironbar) work. The legacy `dwl-ipc-v2` protocol was removed, so
# dwl-ipc-only shells (waybar's dwl module, noctalia's dwl backend) need a
# margo-socket adapter. If you go this route, exec-once your bar plus the
# pieces mshell would otherwise have provided:
#
# exec-once = waybar # classic bar (ext-workspace)
# exec-once = fnott # notifications (mshell has one)
# exec-once = swaybg -i ~/.local/share/wallpapers/default.jpg # wallpaper (mshell paints one)
# exec-once = /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 # polkit (mshell has one)
# ╔═══════════════════════════════════════════════════════════════════════════╗
# ║ 11. Keybindings ║
# ╚═══════════════════════════════════════════════════════════════════════════╝
#
# Bind format:
#
# bind = MODIFIERS, key, action [, arg1 [, arg2 [, arg3]]]
#
# MODIFIERS — one of `super`, `ctrl`, `shift`, `alt`, `NONE`. Combine
# with `+`: `super+shift`, `super+ctrl+alt`. `NONE` is
# for unmodified keys (e.g. media keys, Print).
# key — xkbcommon symbol name. Look at
# /usr/include/xkbcommon/xkbcommon-keysyms.h for the
# canonical list — names there are written `XKB_KEY_foo`,
# use the `foo` part here.
# action — any name from `mctl actions --names`.
#
# Mouse: `mousebind = MODIFIERS, lmb|mmb|rmb, action[, arg]`
# Scroll: `axisbind = MODIFIERS, UP|DOWN|LEFT|RIGHT, action[, arg]`
# Touchpad: `gesturebind = MODIFIERS, direction, fingers, action[, arg]`
#
# Discover everything: `mctl actions --verbose` prints a documented list
# with examples.
# ── Application launchers ───────────────────────────────────────────────────
bind = super, Return, spawn, kitty
bind = super, d, spawn, fuzzel
bind = super+ctrl, f, spawn, nautilus
# `uwsm app -a NAME --` wraps spawns in a transient systemd scope so they
# inherit the graphical-session.target and clean up on logout. Optional
# but recommended; install the `uwsm` package.
# bind = super, Return, spawn, uwsm app -a kitty -- /usr/bin/kitty
# ── Named scratchpads ───────────────────────────────────────────────────────
# Bind syntax: toggle_named_scratchpad, <appid>, <title|none>, <spawn-cmd>
# First press: runs the spawn-cmd and shows the scratchpad. Subsequent
# presses just toggle visibility. The matching
# `windowrule = isnamedscratchpad:1, ...` in section 7 supplies size and
# position.
bind = super+ctrl, Return, toggle_named_scratchpad, ^dropdown-terminal$, none, kitty --class dropdown-terminal
bind = super+alt, f, toggle_named_scratchpad, ^yazi-scratchpad$, none, kitty --class yazi-scratchpad -e yazi
bind = super+alt, v, toggle_named_scratchpad, ^clipse$, none, kitty --class clipse -e clipse
# Recover from "I accidentally scratchpad'd a regular window" — un-tags
# the focused window so it behaves like a normal client again.
bind = super+ctrl, Escape, unscratchpad
# ── Summon — "bring this app to my tag, or launch it" ───────────────────────
# `summon` (alias `taghere`) is the in-process port of mango-here.sh.
# Three args: <appid-regex>, <title-regex|none>, <spawn-cmd>.
#
# On press:
# 1. If a window matching the regex is open *anywhere* (any monitor,
# any tag), move it to the focused monitor's active tag and focus.
# 2. If nothing matches, run the spawn-cmd.
# 3. If the window is already on the active tag, just refocus.
#
# Hidden scratchpads are excluded — they have their own dispatch.
#
# Idiomatic alt+N "app deck": one bind per app, always reaches the same
# window regardless of which tag it currently lives on.
# bind = alt, 1, summon, ^firefox$, none, firefox
# bind = alt, 2, summon, ^kitty$, none, kitty
# bind = alt, 3, summon, ^Code$, none, code
# bind = alt, 4, summon, ^spotify$, none, spotify
# bind = alt, 5, summon, ^electron$, WebCord, webcord
# Optional 4th field combines appid+title: `and` (default) | `or` | `difference`.
# ── Run-or-raise — "go TO this app, or launch it" ───────────────────────────
# `focusapp` (aliases run_or_raise / raiseapp) is summon's counterpart: same
# <appid-regex>, <title|none>, <spawn> args, but instead of pulling the window
# to your current tag it switches TO the window's tag/monitor and focuses it.
# Repeated presses cycle through matching windows.
# bind = super, b, focusapp, ^firefox$, none, firefox
# ── sendkey — inject a synthetic key combo into the focused window ───────────
# `sendkey, <combo>[, <appid-regex>][, <fallback-action[:arg]>][, <tmux-appid-regex>]`.
# COMBO is `+`-joined modifiers + ONE layout-independent key (Tab, Page_Up/Down,
# arrows, F-keys, Home/End, Esc, Return, Space, digits). With an anchored
# appid-regex the combo is sent only when the focused app matches; otherwise
# the optional fallback action runs. Classic use: a 3-finger touchpad swipe →
# browser tab switch, falling back to a workspace move elsewhere.
# bind = ctrl+alt, l, sendkey, ctrl+Tab, ^(firefox|Chromium)$, focusdir:right
# gesturebind = NONE, right, 3, sendkey, ctrl+Tab, ^(firefox|Chromium)$
# gesturebind = NONE, left, 3, sendkey, ctrl+shift+Tab, ^(firefox|Chromium)$
#
# A 4th arg targets a terminal multiplexer instead of a browser: when the
# focused app matches THIS regex, margo skips the keystroke injection
# entirely (unreliable through a terminal's keyboard-protocol encoding +
# the multiplexer's own key table) and drives tmux directly over its
# control CLI — `tmux next-window` normally, `tmux previous-window` when
# the combo holds Shift, so one line still covers both targets with the
# same "switch forward/back" semantic:
# gesturebind = NONE, up, 3, sendkey, ctrl+shift+Tab, ^(firefox|Chromium)$, focusdir:up, ^(TmuxKenp)$
# gesturebind = NONE, down, 3, sendkey, ctrl+Tab, ^(firefox|Chromium)$, focusdir:down, ^(TmuxKenp)$
# ── Translate — select/copy text, run one command, get the result back ─────
# `mshellctl translate selection|clipboard|text "<str>"` captures whichever of
# the primary selection / clipboard was set most recently (own freshness
# window each — Settings → Translate), translates it (key-free Google
# Translate by default, optional DeepL), copies the result back, and toasts
# it. Headless — no menu panel. `mshellctl menu translate` is the separate
# manual-entry surface (type/paste + pick a target language).
# bind = super, t, spawn, mshellctl translate selection
# ── Keyboard-layout cycle ───────────────────────────────────────────────────
# Cycle to the next configured `xkb_rules_layout` (no-op with a single layout).
# The active layout is published for mshell's keyboard-layout bar pill.
# bind = super+alt, space, cycle_kb_layout
# ── Tip: the full action surface ────────────────────────────────────────────
# `mctl actions --names` lists every action; `mctl actions --verbose` prints
# the argument shape + a usage example for each. The bindings here are a
# starter set — the canonical, always-current reference is that command.
# ── Screenshots ─────────────────────────────────────────────────────────────
# One front door for everything: `mshellctl screenshot <area> [--copy|--save]`.
# Areas: region (in-shell selector) · window · output (pick a monitor) ·
# full (all outputs) · repeat (re-grab the last region, no reselect).
# Default delivery is file + clipboard; add `--save` for file-only, `--copy`
# for clipboard-only, `--edit` for the editor, `-d N` to delay N s. All of
# it — keybinds, CLI, and the GUI menu (`mshellctl menu screenshot`,
# Super+Shift+S) — runs the shell's one screenshot engine.
# Print region → file + clipboard
# shift+Print region → file only
# ctrl+Print pick a monitor → file + clipboard
# alt+Print focused window → file + clipboard
# ctrl+shift+Print repeat last region → file + clipboard
bind = NONE, Print, spawn, mshellctl screenshot region
bind = shift, Print, spawn, mshellctl screenshot region --save
bind = ctrl, Print, spawn, mshellctl screenshot output
bind = alt, Print, spawn, mshellctl screenshot window
bind = ctrl+shift, Print, spawn, mshellctl screenshot repeat
# ── Volume / brightness / media (XF86 keys) ─────────────────────────────────
# With mshell, drive these through `mshellctl` so they show the on-screen
# volume / brightness OSD and stay in sync with the bar — no extra daemon.
bind = NONE, XF86AudioRaiseVolume, spawn, mshellctl audio volume-up
bind = NONE, XF86AudioLowerVolume, spawn, mshellctl audio volume-down
bind = NONE, XF86AudioMute, spawn, mshellctl audio mute
bind = NONE, XF86AudioMicMute, spawn, mshellctl audio mic-mute
bind = NONE, XF86MonBrightnessUp, spawn, mshellctl brightness up
bind = NONE, XF86MonBrightnessDown, spawn, mshellctl brightness down
bind = NONE, XF86AudioPlay, spawn, mshellctl media toggle # optional: append a player (spotify, browser…)
bind = NONE, XF86AudioNext, spawn, mshellctl media next
bind = NONE, XF86AudioPrev, spawn, mshellctl media prev
#
# Not running mshell? Use the generic CLIs instead (no OSD):
# bind = NONE, XF86AudioRaiseVolume, spawn, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+
# bind = NONE, XF86AudioLowerVolume, spawn, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
# bind = NONE, XF86AudioMute, spawn, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
# bind = NONE, XF86AudioMicMute, spawn, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle
# bind = NONE, XF86MonBrightnessUp, spawn, brightnessctl set 5%+
# bind = NONE, XF86MonBrightnessDown, spawn, brightnessctl set 5%-
# bind = NONE, XF86AudioPlay, spawn, playerctl play-pause
# bind = NONE, XF86AudioNext, spawn, playerctl next
# bind = NONE, XF86AudioPrev, spawn, playerctl previous
# ── Window management ───────────────────────────────────────────────────────
# Close
bind = super, q, killclient
bind = alt, F4, killclient
# Focus (vim hjkl)
bind = super, h, focusdir, left
bind = super, l, focusdir, right
bind = super, k, focusdir, up
bind = super, j, focusdir, down
bind = super, Tab, focusstack, 1
bind = super+shift, Tab, focusstack, -1
#
# `focus_window_or_workspace` (mango 0.15.5) is `focusdir` with a fallback:
# if there's no window that way, it views the adjacent tag instead of doing
# nothing. Needs the true 4-way direction (left/right/up/down), same as
# `focusdir` above — pick a different combo if you bind both.
# bind = super+alt, h, focus_window_or_workspace, left
# bind = super+alt, l, focus_window_or_workspace, right
# bind = super+alt, k, focus_window_or_workspace, up
# bind = super+alt, j, focus_window_or_workspace, down
#
# Alternative: the niri-style MRU switcher (see the "MRU window switcher"
# settings below) instead of instant stack-order cycling. `mru_next` /
# `mru_prev` take an optional scope arg (all | output | workspace) and an
# optional filter arg (all | appid) — `workspace` scope is "cycle the
# current tag's windows only" without ever jumping you to another tag,
# which `focusstack` above already does too; the difference is the held-
# modifier overlay with live thumbnail preview and release-to-commit.
# Replace the two binds above with these to switch:
# bind = super, Tab, mru_next, workspace
# bind = super+shift, Tab, mru_prev, workspace
# (alt,Tab is already the grid/scroller overview's own cycle — see
# "Overview keybinds" below — so pick a different combo if you bind both.)
# Swap focused window with its neighbour (preserves layout slot)
bind = super+shift, h, exchange_client, left
bind = super+shift, l, exchange_client, right
bind = super+shift, k, exchange_client, up
bind = super+shift, j, exchange_client, down
# Layout
bind = super, t, setlayout, tile
bind = super, m, setlayout, monocle
bind = super, s, setlayout, scroller
bind = super+ctrl, g, setlayout, grid
bind = super+ctrl, m, setlayout, mosaic
bind = super+ctrl, w, switch_layout
bind = super, r, switch_proportion_preset
bind = super+shift, r, set_proportion, 0.500
bind = super, z, zoom
# Master pane size (super+shift+h/l is taken by exchange_client; use
# super+alt for setmfact to keep the hjkl muscle memory)
bind = super+alt, h, setmfact, -0.05
bind = super+alt, l, setmfact, +0.05
bind = super, i, incnmaster, +1
bind = super+shift, i, incnmaster, -1
# Gaps
bind = super+shift, g, togglegaps
bind = super+shift, minus, incgaps, -2
bind = super+shift, equal, incgaps, +2
# Float / fullscreen / sticky
bind = super, f, togglefullscreen
# `togglefullscreen_exclusive` also suppresses every layer-shell surface on
# this output while active — the bar disappears too, not just other
# windows. Handy for games/video on a separate key rather than replacing
# the bind above.
# bind = super+shift, f, togglefullscreen_exclusive
bind = super, g, togglefloating
# Sticky: show the focused window on every tag of its monitor. Press again
# to restore the original tag mask. Niri's float-sticky integrated.
bind = super+ctrl, s, sticky_window
# Move / resize floating window with the keyboard (40 px steps)
bind = super+ctrl, h, movewin, -40, 0
bind = super+ctrl, j, movewin, 0, 40
bind = super+ctrl, k, movewin, 0, -40
bind = super+ctrl, l, movewin, 40, 0
bind = super+ctrl+shift, h, resizewin, -40, 0
bind = super+ctrl+shift, j, resizewin, 0, 40
bind = super+ctrl+shift, k, resizewin, 0, -40
bind = super+ctrl+shift, l, resizewin, 40, 0
# Move / resize floating window with the mouse
mousebind = super, lmb, moveresize, curmove
mousebind = super, rmb, moveresize, curresize
# ── Twilight (built-in blue-light filter / colour-temperature scheduler) ────
# Replaces external tools (sunsetr / gammastep / redshift) with a tick that
# lives inside the compositor's event loop. The schedule, interpolation, and
# DRM gamma write all share margo's frame clock — no IPC roundtrips, no
# polling, no second process. Disabled by default; flip `twilight = 1` to
# opt in.
#
# Modes:
# geo — sun-elevation driven, reads twilight_latitude + twilight_longitude
# manual — explicit wall-clock twilight_sunrise / twilight_sunset
# static — bypass schedule, hold one fixed temp/gamma 24/7
# schedule — multi-step time-of-day presets (HH:MM → preset), interpolated in
# mired space; reads twilight_schedule_dir (see below)
#
# Temperature is interpolated in *mired space* (1/Kelvin) so a 1000 K step at
# 3000 K reads the same perceptual jump as a 5500 K step at 10000 K. Gamma %
# is linear (the sRGB encode curve lives in the LUT builder downstream).
#
# Live control: `mctl twilight {status, preview <K> [pct], test [seconds],
# set <field>=<value>, reset}`. Live-tweaks persist until next reload.
twilight = 0 # 1 = on, 0 = off (default)
twilight_mode = geo # geo | manual | static | schedule
twilight_day_temp = 6500 # Kelvin; D65 daylight reference
twilight_night_temp = 3300 # warm evening
twilight_day_gamma = 100 # % — 100 = pass-through
twilight_night_gamma = 90 # dim slightly at night
twilight_transition_s = 2700 # 45 min total transition window
twilight_update_interval = 60 # idle tick seconds (transition uses 250 ms)
# Geo mode (mode = geo). Use one of:
# * decimal degrees (e.g. Istanbul 41.008, 28.978)
# * 0 / 0 leaves geo disabled — falls through to "Day" all day
twilight_latitude = 0.0
twilight_longitude = 0.0
# Manual mode (mode = manual). HH:MM or HH:MM:SS local time.
# Leave both blank to fall back to geo on a misconfigured manual schedule.
# twilight_sunrise = 06:30
# twilight_sunset = 19:00
# Static mode (mode = static). One fixed temp/gamma 24/7.
twilight_static_temp = 4000
twilight_static_gamma = 95
# Schedule mode (mode = schedule). A directory holding `schedule.conf`
# (lines of `HH:MM <preset-name>`) plus `presets/<name>.toml` files, each
# pinning a `static_temp` + `static_gamma`. margo interpolates in mired space
# between consecutive presets across the day. First run seeds six starter
# presets (deep-night / morning / … / evening) you can edit; mshell ships a
# live editor in Settings → Display and a Twilight bar pill. Default dir below.
# twilight_schedule_dir = ~/.config/margo/twilight
# ── Overview keybinds (alt+Tab muscle memory) ───────────────────────────────
# Three actions for keyboard-driven overview navigation:
#
# overview_focus_next step to the next thumbnail (1)
# overview_focus_prev step BACK one position
# overview_activate commit the highlighted thumbnail and close
# overview onto its window's tag
#
# Each `overview_focus_next/prev` press lights the focuscolor border on
# the new pick *instantly* (no arrange, no animation gate — only the
# selected flag flips). The pointer also warps to the new thumbnail's
# centre so a click commits whatever was last highlighted.
#
# (1) Walk order is configurable via `overview_cycle_order` (above) —
# mru / tag / mixed.
#
# The recommended binding pattern is the alt+Tab triple below. Holding
# Alt and tapping Tab walks the cycle; *releasing Alt* auto-commits the
# pick (Win/GNOME/Hypr muscle memory). The modifier-release commit is
# implemented in the keyboard event filter — it watches for the moment
# every modifier you held when triggering `overview_focus_*` is let go,
# and calls `overview_activate` automatically. So you can omit the
# explicit `alt,Return` bind if you prefer; it's wired below as a fall-
# back for users who like an explicit commit key. The trigger modifier
# is irrelevant — `super,Tab,overview_focus_next` would commit on Super
# release the same way.
# These generic overview binds follow `overview_style` (Settings →
# Overview): with `grid` they drive the grid overview, with `scroller`
# the niri-style scroller overview — only the selected one ever opens.
# Keep these on alt+Tab; super+Tab is `focusstack` above. For the grid
# specifically regardless of style, use grid_overview_focus_next/_prev/
# activate (or toggle_grid_overview / toggle_scroller_overview).
bind = alt, Tab, overview_focus_next
bind = alt+shift, Tab, overview_focus_prev
bind = alt, Return, overview_activate
#
# Alternative trigger: a single `toggleoverview` bind opens / closes the
# grid without keyboard cycling. Comment-in if you prefer mouse-only
# navigation inside overview.
# bind = alt, Tab, toggleoverview
# ── Tags 1–9 ────────────────────────────────────────────────────────────────
# Three operations per tag:
#
# view — switch to viewing this tag (you go to it)
# tag — move the focused window to this tag (you stay)
# tagview — move the focused window to this tag AND follow
# toggleview — show/hide this tag in the current view (multi-tag union)
#
# Bitmask reminder: tag N → 1 << (N-1). 4294967295 = all tags.
bind = super, 1, view, 1
bind = super, 2, view, 2
bind = super, 3, view, 4
bind = super, 4, view, 8
bind = super, 5, view, 16
bind = super, 6, view, 32
bind = super, 7, view, 64
bind = super, 8, view, 128
bind = super, 9, view, 256
bind = super, 0, view, 4294967295
# Move window to tag (you stay) — dwm tradition
bind = super+shift, 1, tag, 1
bind = super+shift, 2, tag, 2
bind = super+shift, 3, tag, 4
bind = super+shift, 4, tag, 8
bind = super+shift, 5, tag, 16
bind = super+shift, 6, tag, 32
bind = super+shift, 7, tag, 64
bind = super+shift, 8, tag, 128
bind = super+shift, 9, tag, 256
# Move window to tag AND follow (Hyprland-style)
bind = super+ctrl+shift, 1, tagview, 1
bind = super+ctrl+shift, 2, tagview, 2
bind = super+ctrl+shift, 3, tagview, 4
bind = super+ctrl+shift, 4, tagview, 8
bind = super+ctrl+shift, 5, tagview, 16
bind = super+ctrl+shift, 6, tagview, 32
bind = super+ctrl+shift, 7, tagview, 64
bind = super+ctrl+shift, 8, tagview, 128
bind = super+ctrl+shift, 9, tagview, 256
# Show/hide tag in current view (multi-tag union)
bind = super+ctrl, 1, toggleview, 1
bind = super+ctrl, 2, toggleview, 2
bind = super+ctrl, 3, toggleview, 4
bind = super+ctrl, 4, toggleview, 8
bind = super+ctrl, 5, toggleview, 16
bind = super+ctrl, 6, toggleview, 32
bind = super+ctrl, 7, toggleview, 64
bind = super+ctrl, 8, toggleview, 128
bind = super+ctrl, 9, toggleview, 256
# ── Tag / monitor navigation ────────────────────────────────────────────────
bind = super, Page_Up, viewtoleft
bind = super, Page_Down, viewtoright
bind = super+ctrl, Page_Up, tagtoleft
bind = super+ctrl, Page_Down, tagtoright
# View the adjacent tag if it's empty; otherwise insert a fresh empty tag
# before/after the current one (shifting later tags up by one) and switch
# to it. Comment-in if you want a "make room" tag instead of overloading
# an already-occupied neighbour.
# bind = super+alt, Page_Up, view_insert, prev
# bind = super+alt, Page_Down, view_insert, next
bind = super, comma, focusmon, -1
bind = super, period, focusmon, +1
bind = super+shift, comma, tagmon, -1
bind = super+shift, period, tagmon, +1
# ── Touchpad gestures ───────────────────────────────────────────────────────
# Format: gesturebind = MODIFIERS, direction, fingers, action[, arg]
# direction : up | down | left | right | up_right | up_left | down_left | down_right
# fingers : 3 or 4
#
# 3-finger horizontal: focus between windows
gesturebind = NONE, left, 3, focusdir, left
gesturebind = NONE, right, 3, focusdir, right
# 3-finger vertical: tag navigation (tag direction follows
# `tag_animation_direction` from section 2)
gesturebind = NONE, up, 3, viewtoleft
gesturebind = NONE, down, 3, viewtoright
# 4-finger horizontal: tag navigation (faster; matches GNOME/macOS habit)
gesturebind = NONE, left, 4, viewtoleft
gesturebind = NONE, right, 4, viewtoright
# 4-finger vertical: overview (Mission Control habit)
gesturebind = NONE, up, 4, toggleoverview
gesturebind = NONE, down, 4, toggleoverview
#
# Diagonals are real directions too — margo classifies a swipe as diagonal when
# BOTH axes contribute > 40 % of the motion (a near-straight swipe stays
# straight). 4-finger up-diagonals move focus between monitors:
gesturebind = NONE, up_left, 4, focusmon, left
gesturebind = NONE, up_right, 4, focusmon, right
# More to taste (uncomment):
# gesturebind = NONE, down_right, 4, togglefloating
# gesturebind = NONE, down_left, 4, togglefullscreen
# (3-finger diagonals work the same way, e.g. up_right/up_left for window moves.)
# ── Scroll-wheel binds ──────────────────────────────────────────────────────
# Niri-style: hold Super and scroll the wheel to walk through windows.
axisbind = super, UP, focusdir, left
axisbind = super, DOWN, focusdir, right
# ── System ──────────────────────────────────────────────────────────────────
# Hot-reload this config (no logout needed). A toast notification confirms
# success; on parse error the previous config stays active and the error
# is printed to margo's log (see `journalctl --user -u margo`).
bind = super+ctrl, r, reload_config
# Emergency unlock — the only `quit`/`force_unlock` action allowed while
# the session is locked by `ext-session-lock-v1`. Useful if your lock
# screen daemon hangs.
bind = super+ctrl+alt, BackSpace, force_unlock
# Quit margo (drops back to the TTY / display manager).
bind = super+shift, q, quit
# Blank every output's panel (arg = on | off | toggle, default toggle;
# arg2 = a specific output name, default all). Any input afterwards wakes
# the screen again, so `off` is always recoverable.
bind = super, F10, dpms, off
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 12. Includes — split your config across files │
# ╰───────────────────────────────────────────────────────────────────────────╯
#
# `source = path` and `include = path` are interchangeable. Bare relative
# paths resolve against the directory of the file that contains the
# directive (NOT the current working directory). Absolute paths and `~`
# expansion both work.
#
# Common pattern: keep monitor topology in its own file managed by
# `mlayout`. `mlayout suggest` writes presets for the detected setup;
# `mlayout set <name>` swaps profiles and re-positions outputs live.
#
# source = mlayout.conf
# source = ~/.config/margo/keybinds-extra.conf
#
# mshell users: source its generated palette to make window borders follow
# the wallpaper's matugen colors. Place it AFTER the [Palette] block above so
# the generated colors win; rootcolor stays static. Missing file = no-op.
# source = colors.conf
# ════════════════════════════════════════════════════════════════════════════
# Newer / advanced knobs
# ════════════════════════════════════════════════════════════════════════════
# Added since this manual's first draft. All optional. Only the logging block
# below is shown live (its values ARE the defaults); everything else is a
# commented example, so copy-pasting this file changes nothing until you opt in.
# ── Logging ─────────────────────────────────────────────────────────────────
# Durable, rotating log files under ~/.local/state/margo/logs/ (the last
# `log_keep_sessions` starts are kept, plus a `margo-latest.log` symlink).
# `log_file_level` is the FILE sink's level and follows config, independent of
# RUST_LOG / MARGO_LOG (those steer stdout). It is unrelated to the legacy dwl
# `log_level` integer below. Live-reloadable; also `mctl log level <lvl>`.
log_to_file = true
log_file_level = info # off | error | warn | info | debug | trace
log_keep_sessions = 3
# log_level = 3 # legacy dwl stderr verbosity (0 silent … 7)
# ── Per-output frame clock ──────────────────────────────────────────────────
# Pace each output by its OWN refresh rate (e.g. a 144 Hz and a 60 Hz monitor
# tick independently) instead of the single global render tick. Default false —
# the default keeps the unchanged global-tick path. Opt in only if you run a
# mixed-refresh multi-monitor setup and want the fast monitor smooth while a
# slower one idles.
# per_output_frame_clock = false
# ── Pointer / trackpad acceleration ─────────────────────────────────────────
# profile: 0 = none (raw), 1 = flat (no accel), 2 = adaptive (default).
# speed: -1.0 (slowest) … 1.0 (fastest); 0 = libinput default.
# mouse_accel_profile = 2
# mouse_accel_speed = 0.0
# trackpad_accel_profile = 2
# trackpad_accel_speed = 0.0
# trackpad_scroll_factor = 1.0 # scroll-distance multiplier
# disable_trackpad = false # hard-disable the touchpad
# ── Keyboard / cursor extras ────────────────────────────────────────────────
# xkb_rules_layout / _variant / _options are documented above; these two are
# the rarely touched rules-set + model fields of the same XKB config.
# xkb_rules_rules = evdev
# xkb_rules_model = pc105
# capslock = false # start a session with CapsLock enabled
# keymode = default # named key mode (advanced; <= 27 chars)
# cursor_theme = Adwaita # cursor theme name for the compositor cursor
# ── Touch screen ────────────────────────────────────────────────────────────
# Recognition tuning for `touchgesturebind` (touchscreen gestures).
# touch_timeoutms = 150
# touch_distance_threshold = 30.0
# touch_degrees_leniency = 20.0
# touch_edge_size_top = 0.0 # edge-swipe zones, fraction of the screen
# touch_edge_size_bottom = 0.0
# touch_edge_size_left = 0.0
# touch_edge_size_right = 0.0
# tablet_map_to_mon = DP-3 # bind a drawing tablet to one output
# ── Hot corners ─────────────────────────────────────────────────────────────
# Fire a dispatch action when the pointer dwells in a screen corner.
# hot_corner_dwell_ms = 100
# hot_corner_top_left = toggle_overview
# hot_corner_top_right =
# hot_corner_bottom_left =
# hot_corner_bottom_right =
# ── Session end (reboot / power off / log out) ──────────────────────────────
# `session_end reboot|poweroff|logout` (mshell's power buttons use it) asks
# every window to close first, so browsers exit cleanly instead of being
# killed with the session. Windows still open after the grace period get a
# notification with a Cancel button; the action runs when the countdown ends
# or as soon as they close. `session_end cancel` aborts.
# session_end_grace_ms = 5000
# session_end_countdown_ms = 15000
# hot_corner_disable_on_fullscreen = 1
# ── Overview ────────────────────────────────────────────────────────────────
# overview_style = grid | scroller (scroller mirrors the scroller layout).
# overview_style = grid
# overview_backdrop_color = 0x241f31ff
# overview_backdrop_image = # path; overrides the backdrop colour
# Scroller-style overview tuning:
# scroller_overview_zoom = 0.30 # 0..1 scale of the shrunk windows
# scroller_overview_gap = 20 # px between columns
# scroller_overview_loop = true # wrap around at the ends
# ── Scratchpad sizing ───────────────────────────────────────────────────────
# Size of a window summoned to the scratchpad, as a fraction of the output.
# scratchpad_width_ratio = 0.8
# scratchpad_height_ratio = 0.8
# ── Layout extras ───────────────────────────────────────────────────────────
# auto_layout : re-pick a layout automatically as the window count changes.
# taglayout_force : a `taglayout` rule overrides the per-tag remembered layout.
# auto_layout = false
# taglayout_force = false
# enable_gaps = true # master gaps on/off (alias: gaps_enabled)
# Per-tag default layout — precedence: taglayout > tagrule > default_layout:
# taglayout = tag:2, layout:scroller
# ── Blur parameters ─────────────────────────────────────────────────────────
# Kawase blur tuning (blur is wired but not shipped yet — see `blur` above).
# blur_params_num_passes = 3
# blur_params_radius = 5
# blur_params_brightness = 1.0
# blur_params_contrast = 1.0
# blur_params_saturation = 1.0
# blur_params_noise = 0.0
# ── Compositor-drawn wallpaper ──────────────────────────────────────────────
# margo can paint a wallpaper itself; the mshell shell usually owns this.
# wallpaper = ~/Pictures/wall.jpg
# wallpaper_fit = cover # cover | contain | fill | center
# ── Per-output config (compact `monitor` form) ──────────────────────────────
# Same fields as `monitorrule` above, in one CSV line:
# monitor = name:DP-3, width:2560, height:1440, refresh:60, x:0, y:0, scale:1
# ── Switch / touch-gesture binds ────────────────────────────────────────────
# switchbind : laptop lid / tablet-mode switch → action.
# touchgesturebind : touchscreen gesture → action (see touch_* tuning above).
# switchbind = lid, close, spawn, true
# touchgesturebind = swipe, up, 3, toggle_overview
# ── Misc ────────────────────────────────────────────────────────────────────
# maximizescreencolor : backdrop colour behind a maximised / monocle window.
# allow_lock_transparent : let the lock surface stay see-through (debugging).
# sloppyfocus_arrange : raise / re-arrange on sloppy-focus pointer-enter.
# disable_middle_paste : disable middle-click paste (wp_primary_selection).
# Wayland apps only; X11 unaffected. A client that
# already bound the global keeps using it until it
# restarts — this only stops *new* binds.
# maximizescreencolor = 0x000000ff
# allow_lock_transparent = false
# sloppyfocus_arrange = false
# disable_middle_paste = false
# drag_tile_small = false # mango 0.13: shrink a tile while super+LMB dragging it
Where to next#
- Configuration overview — curated walkthrough of the high-traffic options.
mctl actions --verbose— the full enumerated dispatch catalogue (40+ actions).- Scripting — when window rules and keybinds aren't
expressive enough, reach for
~/.config/margo/init.rhai.