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 `"..."`.
#
# ─── 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.