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 `"..."`.
#
# ─── 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.
#
# ─── 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
# 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
# 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
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 ────────────────────────────────────────────────────────────────────
# Kawase blur is wired to the rendering pipeline but not yet shipped — keep
# `blur = 0` until margo ships the implementation. The `blur_optimized = 1`
# flag lets the renderer pre-pass the framebuffer once for static frames
# (≥30% saving on idle), and is harmless to leave on.
blur = 0
blur_layer = 0
blur_optimized = 1
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 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
# ── 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
animation_duration_canvas_pan = 300 # PaperWM-style pan (canvas layout)
animation_duration_canvas_zoom = 300
# ── 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
animation_curve_canvas_pan = 0.16, 1.00, 0.30, 1.00
animation_curve_canvas_zoom = 0.16, 1.00, 0.30, 1.00
# ── 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
# XWayland persistence: keep the surface mapped during resize storms so
# Electron / Steam clients don't flicker. Off only if you suspect a leak.
xwayland_persistence = 1
# ── Mouse-drag rearrange ────────────────────────────────────────────────────
# Holding `super+lmb` (see `mousebind` below) drags a tiled window into
# another slot. `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 = 8.0 # ms; lower = smoother but more CPU
drag_floating_refresh_interval = 8.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 # 0 = grid overview, 1 = column overview
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
# ── 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 idle inhibition apply only when the inhibiting window is visible?
# 0 = always inhibit (matches mango), 1 = inhibit only when on-screen.
idleinhibit_ignore_visible = 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.
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
# vertical_grid — grid laid out column-major
# dwindle — recursive split (Hyprland default)
# canvas — free-form pan/zoom canvas (PaperWM-meets-Excalidraw)
#
# `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: master can overrun stacks (looks like floating-on-top)
center_when_single_stack = 1 # center_tile: when only one stack pane, center the master
canvas_tiling = 0 # canvas: 1 = auto-arrange new windows in a grid
canvas_tiling_gap = 10
canvas_pan_on_kill = 1 # canvas: re-center after closing a window
canvas_anchor_animate = 0 # canvas: animate manual anchor changes
# `circle_layout` defines what `switch_layout` cycles through. Order matters.
circle_layout = scroller, tile, center_tile, grid, deck, monocle, vertical_grid
# ── 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
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.
#
# ─── 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 (used by terminal swallowing)
# noswallow:1 — never get swallowed even if the parent is a term
#
# 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 ─────────────────────────────────────────────────────
# When a terminal spawns a graphical child (e.g. running `mpv file.mkv`
# from the shell), margo can hide the terminal until the child exits.
# Mark which apps are terminals and which children should NOT trigger the
# behaviour.
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; 0 = preferred
# x:N / y:N — logical position. 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, vrr:1, refresh:144
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 10. Startup commands │
# ╰───────────────────────────────────────────────────────────────────────────╯
#
# `exec-once` runs ONCE at compositor startup. It does NOT re-run on
# `mctl reload` — that's intentional, otherwise reload would respawn your
# bar every time you tweak a colour. Use a separate `exec` (no `-once`)
# if you want re-spawn on reload, but most users prefer to manage daemons
# via `systemctl --user`.
#
# NOTE: the first-launch setup wizard is an in-shell layer-shell menu, not
# a compositor exec-once. mshell opens it automatically the first time it
# starts with no saved profile; you can re-open it any time with
# `mshellctl wizard` (or the `mwizard` shim, which just calls that).
#
# margo speaks `dwl-ipc-v2`, so any compatible external shell drops in —
# bar + notifications + launcher + OSD + system tray live outside the
# compositor. Pick one of the lines below (or your own favourite).
# ── Shell (pick exactly one) ────────────────────────────────────────────────
# noctalia-shell — `dwl-ipc-v2` shell with `osc-shell` IPC. Comprehensive
# bar / notifications / launcher / OSD / settings; matches the README
# acknowledgement and PKGBUILD optdepend.
# exec-once = qs -c noctalia-shell --no-duplicate
# waybar — minimal classic bar with `dwl-tags` module + your own
# notification / launcher / OSD daemons alongside.
# exec-once = waybar
# ── Notification daemon (only if your shell doesn't ship one) ───────────────
# exec-once = fnott # lightweight wlroots-native
# exec-once = mako # alternative
# ── Wallpaper (only if your shell doesn't paint one) ────────────────────────
# exec-once = swaybg -i ~/.local/share/wallpapers/default.jpg
# ── Auth agent (Polkit prompts — pinentry, mount, geo permission, …) ────────
# exec-once = /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
# ╔═══════════════════════════════════════════════════════════════════════════╗
# ║ 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
# ── Screenshots (delegates to mscreenshot — installed alongside margo) ──────
# Print region selection → editor → file + clipboard
# shift+Print region selection → editor → file (no clipboard)
# ctrl+Print focused output → editor → file
# alt+Print focused window → editor → file
bind = NONE, Print, screenshot-region-ui
bind = shift, Print, screenshot-region
bind = ctrl, Print, screenshot
bind = alt, Print, screenshot-window
# ── Volume / brightness / media (XF86 keys) ─────────────────────────────────
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
# 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, 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
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.
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
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
# ── 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
# ╭───────────────────────────────────────────────────────────────────────────╮
# │ 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
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.