Skip to content

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/src/config.example.conf
# ╔═══════════════════════════════════════════════════════════════════════════╗
# ║ 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.