Changelog#
All notable changes to margo are documented here.
The format follows Keep a Changelog, and the project adheres to Semantic Versioning.
Unreleased#
0.8.0 – 2026-05-24#
Added#
- In-shell setup wizard — the guided first-run flow is now a real
layer-shell menu (never a floating window), opening contiguous with the
bar like every other menu. Eight steps: Welcome, Theme (mode / preset /
font scale / clock), Keyboard (xkb layout / variant /
xkb_rules_optionssuch asctrl:nocaps), Touchpad (tap-to-click, natural scroll, disable-while-typing), Wi-Fi (scan + connect vianmcli), Wallpaper (with a sensible directory fallback so rotation always has a source), Bar (top / bottom), and a Review summary. Apply writes everything live and runsmctl config reload, so the keyboard layout/options take effect immediately — no logout — with an optional reboot offer. The base profile can be fresh defaults or a snapshot of the running config ("active"). Reachable from Settings → Setup, the bar's Setup pill,mshellctl wizard(and themwizardshim), and automatically on first launch when no profile is saved. - Microphone-mute key with an OSD —
XF86AudioMicMute/mshellctl audio mic-mutetoggles the default audio source and pops the same bottom-centre pill as the volume keys (the input-side twin of the volume OSD, with the muted-mic glyph). - Settings overhaul — an account section (
~/.faceavatar + user identity + picker), a sidebar search box that jumps to any section or widget page, widget gears that deep-link straight to their own page, an embedded Setup page, keyboard focus from first open, and a window size proportional to the screen resolution. - Settings → Fonts — a monospace family slot, a global UI font scale and a separate bar-pill font scale, each with a live preview.
- Settings → Display — a GNOME-style drag-to-arrange monitor editor
with visual mini-maps, backed by a new
mlayout outputs --jsonlive state (and hex colours inmlayout list --json). - Dashboard — the Overview tile shows the pending-update count (its own throttled, always-visible probe) in place of the duplicated CPU temperature.
- OSD — the volume / brightness OSDs show the numeric level and are slimmer.
Changed#
- Window glow is now a soft ambient halo that sits below the content in the visual hierarchy — desaturated, low-opacity, roughly 5–10 % of the surface — instead of the previous neon-outline look.
Fixed#
- The wizard's Wi-Fi dropdown spun the GTK main loop at ~100 % CPU. A
#[watch] set_modelrebuilt theStringListevery view pass, whoseselected-notifyfed back into the update cycle; it self-triggered at startup and starved every other reactive update (the margo-tags pill stopped tracking the active tag and window occupancy). The dropdown model is now held and mutated in place, so CPU returns to idle. - The wizard menu now builds lazily on first reveal (its
nmcliscan included), so it no longer adds startup cost on sessions that never open it. - Wizard buttons use the canonical
ok-button-*component classes per DESIGN.md — one accent region (ok-button-primary) per step, neutral actions onok-button-surface.
0.7.9 – 2026-05-23#
Added#
- DESIGN.md §13–§14 — the interaction-philosophy layer — eight binding subsections (cognitive load, the one-accent attention hierarchy, spatial logic, the responsiveness motion budget, surface ownership, density, state continuity, accessibility) plus a "visual restraint & identity" section. Codifies why the shell feels calm, not just which tokens to use.
Changed#
- The whole shell now draws from the design tokens — the last surfaces
carrying hardcoded values were swept onto the token scales: the standalone
mlock lock screen is themed from the full matugen palette (background /
text / accent / danger) instead of a fixed scheme; the media-player bar
cover, notification, clipboard, power and session widgets had their stray
radius / motion / colour literals replaced with
--radius-*,--motion-*and matugen colour vars. No off-scale radii, raw-millisecond transitions, or gruvbox hex left in the SCSS.
Fixed#
- Browser screen-sharing works again (Meet window/screen pick) — a regression from the lazy-menu refactor destroyed the screenshare picker's pending portal reply on the menu's first reveal, so the chooser never appeared and the browser saw an instant "cancel". The screenshare menu is now marked built when its widget is installed, so the lazy first-reveal rebuild can't wipe it.
0.7.8 – 2026-05-23#
Added#
- uwsm session, shipped by the package —
margo-uwsm.desktopplus themargo-uwsm-session/margo-sessionwrappers now install to/usr/share/wayland-sessionsand/usr/bin, and/etc/xdg/uwsm/env-margorestores the standard XDG user-bin dirs (~/.local/bin,~/bin) onto the sessionPATH(uwsm rebuilds it from a POSIX login shell and would otherwise drop them). uwsm moves from optional to the default way to run margo;uwsmis now a runtime dependency. - Tracy profiling support — building with
--features profile-with-tracynow starts a Tracy client and marks frames, so the existingspan!instrumentation actually records and a Tracy GUI can attach.
Changed#
- Menus build lazily — the menu pollers (network / IP / DNS / UFW /
podman) and the entire menu content widget tree are now constructed on a
menu's first reveal instead of eagerly at shell startup. A menu the user
never opens does zero work: no GTK trees, no background polling, no
sudo/ subprocess probes. state.jsonwrites coalesced — a burst of compositor state changes (one layout switch touches focus + windows + tags) now serializes the snapshot once per event-loop iteration instead of on every change.- One gtk-rs generation — cairo/pango pinned to gtk4 0.10's 0.21 stack, dropping a duplicate 0.22 gtk-rs build (glib / gio / cairo / pango / pangocairo + proc-macros) for faster compiles.
- Workspace is clippy-clean — zero warnings, with a documented lint policy; the substantive lints were fixed rather than silenced.
Fixed#
- Integrated polkit agent now works — mshell-polkit registers for the
logind Display session instead of the unset
$XDG_SESSION_IDit sees under the systemd user manager, so it actually receives authentication requests; and the password dialog wraps to fit its window. - Twilight toggle reflects state —
state.jsonis refreshed on everymctl twilightchange, so the night-light button no longer looks stuck "on" after toggling. - Network / IP / DNS / UFW / podman menus populate on open — those menus now receive the reveal signal that drives their first fetch.
- Client misbehavior can't crash the session — the layer-shell and screencopy handlers degrade to a logged no-op instead of panicking the whole compositor on a map failure or a destroy/copy race.
0.7.7 – 2026-05-23#
Added#
- DESIGN.md §12 — the "panel archetype" — a binding spec for spacious, app-like menu surfaces: panel surface metrics, a reusable header (leading glyph + SemiBold title + circular actions), a segmented control, a pill query field, and lightweight content rows.
- Reusable
MenuWidget::PanelHeader—[icon] title+ a live date - a settings gear. The dashboard leads with it in place of the old Clock hero, and it's the shared implementation behind every menu's header.
- Clipboard panel (Phase 2) — segmented type tabs (All · Text · Images · Files · ★) with live counts, relative timestamps, a pill search field, copy/pin toasts, and a panel-density setting (comfortable / compact).
- System Updates — its own Settings page — menu size/position plus a "check every N hours" interval and per-source toggles (repo / AUR / Flatpak).
- New MargoMaterial symbolic icons — refresh, copy, open-in-new, and a cube glyph.
Changed#
- §12 panel header rolled out everywhere — clipboard, dashboard, and
every menu (UFW, DNS, Podman, Notes, Power, Valent, Network,
Bluetooth, Audio, CPU, Public IP, Keep Awake, Notifications, Margo
Layout, Twilight, Media Player, Weather, Screenshot, Wallpaper, Theme
Picker, SSH Sessions, Keybinds, Session, Screen Record, Screen Share,
System Updates) plus the Settings window sidebar, all sharing the
.panel-header/.panel-title/.panel-action-btnchrome. Header titles settled at a calm--font-md(16) SemiBold. - System Updates no longer re-probes on every restart — the last
result is cached to disk and the check runs once per configured
interval (deduplicated across monitors); the panel re-probes only when
opened. This stops the AUR helper — and its
sudo— firing on each shell start.
Fixed#
- Dashboard columns — left and right columns end at the same bottom edge again (the Weather anchor stretches to fill its column).
- Clipboard — Tab cycles past an empty type tab instead of sticking; timestamps and the per-row trash sit at the dim hint tier; the search reads as a proper pill query surface.
- Bluetooth — the §12 header sits on the panel surface, not the tile-card colour.
0.7.6 – 2026-05-22#
Added#
- mshelldash — a standalone tabbed dashboard
(Overview · Media · Weather · Wallpaper · System), rebuilt on margo's
DESIGN.md language and coexisting with the classic dashboard. The
Overview tab is a live mosaic (clock hero + a
/proc-sampled system glance); the other tabs reuse the existing menu-widget components so they stay in sync. Open it withmshellctl menu mshelldash [tab]to land straight on a named view. - CPU dashboard enrichment — current frequency, CPU model / core / thread identity, a history sparkline, a user vs. system load split, and memory detail; the bar pill gained per-metric glyphs (a new processor-chip and RAM-module symbolic icon ship in MargoMaterial, plus the thermometer), recoloured by the calm/warn/danger ladder.
- Weather — a standalone bar pill + menu; the standalone menu is the full all-in-one Current / Hourly / Daily surface, today's high / low rides on a right-click toggle in the pill, a dedicated Weather page in Settings, and clearer city / district querying.
- Twilight — the menu now surfaces each preset's actual values (colour temperature + time), not just its name.
- Notifications — configurable popup-toast width, a read / unread bell dot in the bar (unread → error dot, seen history → muted dot), and toggleable history grouping.
- Bar — a unified, configurable pill hover strength; the keep-awake pill gained a 24 h preset; the bluetooth pill shows the connected device name.
- Dashboard — wallpaper + screenshot quick-action buttons.
- mlock — consolidated as margo's single lock screen: always-visible
matugen accent (card border + avatar ring), media keys and a
keyboard-layout indicator on the lock surface, and a bare
mshellctl lock.
Changed#
- Sweeping DESIGN.md conformance pass across the shell: the Settings
window was rebuilt on shared chrome; dashboard / clipboard / launcher /
theme-picker / wallpaper widgets were aligned to surfaces over
borders, the
--font-*size scale, matugen tokens, and one canonical hover; a single radius rule (scale for widgets, config for window chrome); and scatteredpx/emfont sizes were tokenized. - DESIGN.md itself was extended to codify the scrollable-list / footer ("dark band"), label-toggling-button, and read/unread-marker rules so they don't regress.
Fixed#
- Settings panel corners are now clipped via
set_overflow(GTK4 ignores CSSoverflowon aGtkBox). - Dropped the dark background band below the UFW / Podman rule lists.
- DNS preset Apply / Active buttons keep one width across both states.
- Audio-dashboard / Bluetooth revealer rows are centred and slimmer.
- Reaped keybind-spawned child processes (no more zombie pile-up) and
stopped zombie
mlockprocs from wedginglock_session. - Weather menu is registered in Settings → Menus; Valent pill icon states; assorted clipboard spacing fixes.
[0.7.5] – 2026-05-21#
Changed#
- License set trimmed + reorganised to match the actual code lineage.
Removed the wlroots / tinywl / sway license files — margo is a
pure-Smithay Rust compositor and carries no code derived from any of
them (audit confirmed: 0 derived files; the only mentions are
interop/behaviour comments, and the one shipped wlr protocol XML
self-attributes its own copyright). Added attributions that were
actually missing: niri (GPL-3.0-or-later) for the three protocol
files ported in 0.7.4, and noctalia (MIT) for the mshell widgets
that reimplement noctalia patterns. The project's own
LICENSEstays at the repo root; all upstream attributions (mango, dwl, dwm, OkShell, niri, noctalia) now live in alicenses/directory.PKGBUILD,install.shand the post-install smoke test ship/verify the whole set; README / CONTRIBUTING updated to match.
[0.7.4] – 2026-05-21#
Added#
- Three Wayland protocols, closing the gap with mango / Hyprland on the set wlroots compositors get for free:
zwlr_foreign_toplevel_manager_v1(write-side). Taskbars and docks can now act on toplevels — activate, close, (un)fullscreen — not just list them. Runs alongside the existing read-onlyext-foreign-toplevel-list-v1; activate jumps to the window's tag and focuses it. The mshell active-window pill becomes clickable.ext_workspace_v1. The standardized workspace protocol, so shells that don't speak dwl-ipc (sfwbar, ironbar, …) can show margo's tags. Each output is a workspace group with 9 fixed tag-workspaces; "active" mirrors the monitor's tag bitmask. dwl-ipc still runs in parallel.zwlr_virtual_pointer_manager_v1. Synthetic pointer injection — companion to the existing virtual-keyboard.wtype --click, remote desktop and accessibility tools can drive the cursor, buttons and scroll through margo's normal input path.
margo now advertises ~57 Wayland globals — ahead of mango (~53) and
niri (~41), pursuing Hyprland (~67). See docs/protocol-comparison.md.
- Configurable notification buttons. Toast action buttons and the close button are now toggled from Settings → Notifications (default: action buttons off, close button on), trimming the default toast.
Fixed#
- Stuck notification toast. A dismissed toast could linger as a half-collapsed remnant (a dangling "View" button) because the always-visible layer-shell overlay kept its last committed frame. The popup surface now hides when the list empties, so the compositor drops it — matching the mshell-osd lifecycle.
Notes#
- Test coverage expanded: integration tests for the 14 tiling-layout algorithms, window-rule parsing, the mshell-config YAML schema, and multi-output placement. CI runs the full suite in an Arch container.
- Three protocols remain unadvertised —
zwlr_output_power_management_v1,wp_tearing_control_v1,wp_drm_lease_device_v1— blocked by smithay capability (no tearing / async page-flip), margo's State/backend split (drm-lease), or untested-DRM risk (DPMS). Tracked inroad_map.md§15.10.
[0.7.3] – 2026-05-21#
Added#
- Cross-distro
install.sh. A single self-contained installer at the repo root that detects the distribution and builds, installs, and uninstalls margo. On Arch / CachyOS it builds via the repoPKGBUILDwithmakepkgand installs withpacman(uninstall =pacman -R margo-git). On Debian / Ubuntu it installs the apt build deps, bootstraps a current Rust viarustupwhen the system one is too old, buildsgtk4-layer-shellfrom source when it isn't packaged, compiles the workspace, and installs to/usr— recording every path in a manifest souninstallremoves exactly what was added. Validated end-to-end on Ubuntu 24.04.3.
Notes#
- Ubuntu requires GTK ≥ 4.20. margo's gtk4-rs (0.10) needs GTK 4.19+,
so Ubuntu 24.04 LTS (GTK 4.14) cannot build the shell, and
apt upgradewon't change that for the LTS lifetime. Use Ubuntu 25.10+ / 26.04 LTS or any distro with GTK 4.20+.install.shchecks the GTK version up front and stops early with a clear message. README install section rewritten around the installer; design indocs/install-script.md.
[0.7.2] – 2026-05-21#
Added#
- Native Screenshot portal.
margo-portalnow servesorg.freedesktop.impl.portal.Screenshotin addition toScreenCast, driving the compositor'sorg.gnome.Shell.Screenshotshim. The shim's screenshot path was previously a stub that always failed; it now captures the desktop viagrimto a temp PNG (asynchronously, off the compositor event loop) and returns afile://URI to the requesting app.
Changed#
- gnome-portal-free sessions.
margo-portals.confno longer routes any interface toxdg-desktop-portal-gnome:ScreenCast=margo,Screenshot=margo,RemoteDesktop=none,default=gtk.Secretstays ongnome-keyring(the standalone freedesktop secret daemon — not the gnome portal).margo.portaladvertisesScreenshotwithUseIn=margo, and the wayland-session entries declareDesktopNames=margo(droppingmango;wlroots) to keep portal config resolution unambiguous. - Packaging.
xdg-desktop-portalandxdg-desktop-portal-gtkare now hard dependencies; thexdg-desktop-portal-gnomeandpolkit-gnomeoptdepends were dropped.
[0.7.1] – 2026-05-21#
Added#
- Twilight (blue-light filter) UI. Bar pill + quick-control
panel — master toggle, live temperature / phase / mode readout,
source-mode selector (Auto / Manual / Static / Schedule), a
temperature slider that previews live, and schedule-preset
chips. A live schedule-preset editor lands in Settings →
Display. In Schedule mode the preset whose time slot is current
is tinted with the accent.
mshellctl menu twilight. - Keyboard-shortcuts cheatsheet. Bar pill + searchable menu
built live from
config.confbind =lines (followingsourceincludes), grouped by action category with colour-coded modifier chips.mshellctl menu keybinds. - Valent Connect. Bar pill + panel — paired-phone battery / connectivity status plus find / ping / browse / share / pair actions. Ported from the noctalia plugin.
- System Updates. Pill + panel listing pending updates grouped by source (repo / AUR / Flatpak) with Refresh + Update and per-source toggles in Settings.
- Keep Awake (idle inhibitor). Bar pill + duration-picker menu (30 m / 1 h / … / ∞) with a live countdown and quick-extend.
- Audio dashboard. Combined output+input bar pill — scroll the icon to change volume — opening a revealer-row menu with sliders, mute toggles, and device pickers for both sides.
- CPU + Power pills. Combined CPU dashboard pill + per-core / RAM / load menu; combined Power pill (profile + battery) with right-click cycle, plus a battery-only mode.
- Network Console. Live activity arrows and TX/RX traffic graphs on the pill + panel; default ↓/↑ speed readouts coloured per direction.
- Clipboard history. Persistence, favorites / pin,
sensitive-skip, All / Favorites tabs, keyboard navigation,
vim-style
/search, debounced writes, configurable entry limit. Persists the full history by default. - Notifications. Album art / app icon, click-to-open,
swipe-to-dismiss, action icons, body markup, 2FA-code copy,
per-app grouping, mute blocklist, and
mshellctlDND / count. - Launcher startup scripts. User-managed autostart list — add by name, per-script enable toggle + startup delay, delete.
- Dashboard widgets. CompactAudio, Connectivity, OverviewIntel and SystemStatus tiles; equal-width two-column body with a last-child-fill so bottom cards match.
- Bluetooth standalone layer-shell menu.
- matugen-driven window borders. The compositor's border
colours now follow the shell's palette: mshell generates
~/.config/margo/colors.conffrom the active scheme andmctl reloads on change.sourceit fromconfig.confto override the static border colours (rootcolorstays static).
Changed#
- Material-3 design language. Sweep across bars, menus, the
dashboard, and the Settings window, codified in
mshell-frame/DESIGN.md. - margo tag / layout widgets redesigned to the M3 spec — active-indicator pill, tag occupancy as an accent digit.
- De-
n-prefixed the ported plugin widgets and removed six redundant standalone bar pills. - Twilight temperature slider now reads whole kelvin.
Fixed#
- Twilight: presets now appear in the menu, the mode no longer
reverts on a poll race, and the temperature slider is visible
(it was an unstyled, invisible
GtkScale). - Notifications: resolve icon-name
image-pathhints (and the-symbolicfallback) sonotify-send -i <name>icons render instead of a broken-image placeholder. - Keep Awake countdown was invisible on the active pill.
- Frame: re-place menus whose anchor was changed in Settings.
- Valent file-share crash + clearer no-connectivity state; Settings ghost Battery / PowerProfile pills; dashboard CompactAudio percentage and Bluetooth-connected detection.
[0.7.0] – 2026-05-18#
Added#
-
mwizard— first-launch setup wizard. New top-level binary that opens when no~/.config/margo/mshell/profiles/default.yamlexists and walks the user through 5 pages: Welcome → Theme (Dark/Light matugen + 24h/12h clock) → Keyboard (xkb layout from 14 common codes, defaults from$LANG, with free-form layout + variant overrides) → Wallpaper (FileDialog picker) → Done. Writes bothdefault.yaml(full shell profile) and~/.config/margo/config.conf(surgicalxkb_rules_*line patch).mwizard --forcere-runs even when a profile exists. No-flagmwizardis a no-op when a profile is already there, so it's safe to hook into a session-start script. -
mpicker— native colour picker binary. Replaces the hyprpicker fallback that used to ship inmshell-utils. Frozen wlr-screencopy + 10× zoom lens overlay + hex chip; CLI supports--autocopy,--notify,--no-zoom,--format hex/rgb/hsl/cmyk,--lowercase-hex,--quiet. Bundled in themargo-gitpackage; mshell's launcher ColorPicker button callsmpickerdirectly with no hyprpicker fallback. -
Dashboard menu — compound clock + calendar + weather + quick settings. Two-column 860 px panel: left holds Clock hero / CalendarGrid / Weather / MediaPlayer; right is a verbatim clone of the standalone Quick Settings widget stack (Network / Bluetooth / Audio Out / Audio In / Power Profile + the two QuickActions rows). Columns equalised at 400 px each. Open via
mshellctl menu dashboardor the new Dashboard bar pill. -
Dashboard bar widget. New
BarWidget::Dashboard— Clock-style pill that shows the current time/date using the shared[tempo]chrono format list. Left-click toggles the dashboard menu, right-click double-press cycles formats. -
Settings → Widgets → Dashboard + Settings → Menus → Dashboard. Both entry points are now in the Settings UI; Menus page covers position / min-width / max-height / widget list.
-
Per-menu
maximum_heightconfig. Spinbox in the Menus settings page caps the vertical viewport of any menu; scroll-past behaviour kicks in when the content overflows. -
Launcher right-click context menu — Pin/Unpin + Hide/Unhide. New
HiddenStoremirrorsPinStoreat~/.cache/margo/launcher_hidden.json. Hidden items only appear in search results (non-empty query), suppressed from empty-query browse mode so the user can curate the at-a-glance app pile without losing the ability to type the app name. Right-click menu auto-suppresses on rows without ausage_key(calculator, command palette). -
mshellctl menu app-launcher --tab <name>and--list-tabs. Open the launcher pre-selected on a category (All / Run / System / Insert / Search / Compositor / Connect).--list-tabsprints the known categories. -
mscreenshot region selector via mshell IPC.
mscreenshot areanow bridges to mshell's rich in-shell selector (preview state, snap, aspect info) when mshell is running, drops to the bareslurpoverlay otherwise. New IPC methodSelectRegionreturns the picked geometry to the CLI. -
Screenshot widget UX polish. Area selector grew preview state (Enter to commit, arrows to nudge, Shift+arrows for 10 px jumps), aspect-ratio chip, snap-to-window helper, Ctrl+S / Ctrl+E shortcuts that override the commit target. Inline annotate path now prefers
sattyoverswappyto match the rest of the workspace.
Changed#
-
menu_settings.rscollapsed 4041 → 394 LOC via a newMenuConfigPanelsub-component + extendedMenuKindwith Notifications/Wallpaper variants +read_widgets/tracked_widgets/write_widgets. Adding a new menu to the aggregate Menus page is now ~10 lines instead of ~250. -
HyprPicker → ColorPicker rename across the workspace. Drops the Hyprland brand from a margo-native helper that was never tied to Hyprland after the mpicker port. No serde aliases — user YAMLs need a one-time
s/HyprPicker/ColorPickersweep; the in-tree default profile is already migrated. -
App launcher row padding tightened 8 → 1 px so each app visually reads as 2 lines (name + description) instead of the previous "blank / name / desc / blank" 4-line feel.
-
mshell-config::pathsmodule is nowpubso external binaries (mwizard, future tools) can resolve the profile path without re-deriving the layout. -
mshell + launcher caches honour
$XDG_RUNTIME_DIRinstead of/tmp/for fallback paths — per-user, race-free on shared machines.
Fixed#
-
Compositor zbus/tokio panic at session start. Packaging bug: a single
cargo build -p margo -p ...invocation that included mpicker pulled inmshell-screenshot→mshell-services→wayle-*→zbus[tokio]via Cargo's per-invocation feature unification. The compositor linked against the tokio-enabled zbus and panicked atstart_object_server("there is no reactor running, must be called from the context of a Tokio 1.x runtime"). Fix: PKGBUILD puts mpicker in the shell-side invocation alongside mshell so the compositor's zbus staysasync-io-only. -
Dashboard right column wasn't rendering card chrome.
MenuModel'scss_classfield stored"quick-settings-menu dashboard-menu"as a single literal class name (set_css_classeswas being passed the whole string as one entry), so.quick-settings-menu .network-menu-widgetdescendant selectors never matched. Split on whitespace post-view_output!. -
matugen "no progress, hangs forever" bug. The output-log drainer thread used
.flatten()on aBufReader::lines()iterator; a single persistent IO error spun the thread forever instead of bailing. Switched to.map_while(Result::ok). -
PKGBUILD bundled bin list complete. mwizard + mpicker now appear in both the build invocations and the install loop. Ldd verification comment refreshed.
Engineering#
cargo clippy --fix --workspacesweep — 37 auto-fixes landed.CODE_REVIEW.md— full audit report (Critical / High / Medium / Low findings) added at repo root, with a status table tracking which findings landed in this release.- Production
unwrap()audit (#187) — case-by-case review confirmed the flat-grep count of 265 prod unwraps was largely a false alarm; 80 %+ are framework guarantees (Mutex::lock in single-threaded GTK, GTK downcast_ref, const-string parsing, PipeWire format invariants).capture.rsdocumented its in-vec invariant viaexpect().
[0.6.3] – 2026-05-17#
Added#
- Power-user keyboard bindings for the launcher. Walker- and noctalia-inspired shortcuts that work out of the box, no config required:
- Ctrl+1..Ctrl+9 — activate the Nth result (no arrow keys).
- Ctrl+Shift+P — toggle pin on the selected item. Pinned
items rank at the top of every browse pass with a ★ marker,
saved to
~/.cache/margo/launcher_pins.json. Ctrl+Shift+P rather than plain Ctrl+P so the emacs "previous selection" binding stays intact. - Tab / Shift+Tab — cycle through provider categories (Apps → Compositor → System → Run → Insert → Search → Connect → All).
- Delete — drop the selected frecency / history entry
(Apps frecency forget, Command history
forget(expr), Scripts frecency forget). Provider opts in viacan_delete. - Ctrl+E — toggle fuzzy ↔ exact-substring matching. Visible
~/=chip indicator next to the search entry. - Ctrl+R — repopulate the search entry with the last query the launcher saw before closing.
- Ctrl+Enter — run the provider's alt action: Apps launch
in
$TERMINALwith a "press enter to close" shell wrapper; Websearch copies the resolved URL to the clipboard. -
PageDown / PageUp — jump 10 rows at a time.
-
Category tab strip — small pill row above the result list, one pill per provider category with an icon + label pair. Selected pill picks up the primary accent; tooltip shows the full category name on hover. Pills also accept direct mouse clicks. Mappings: All → view-grid, Apps → app-grid, Compositor → display, System → preferences-system, Run → terminal, Insert → input-keyboard, Search → search, Connect → server.
-
Walker-style keybind hint footer. Small chip strip at the bottom of the launcher that lists the currently-relevant shortcuts. Always-on chips (↵ Activate / Ctrl 1-9 Quick / Tab Categories / Ctrl E Exact / Ctrl R Last / Esc Close) anchor the strip; contextual chips (Ctrl ↵ Alt action / Ctrl ⇧ P Pin·Unpin / Del Remove) only render when the selected row actually supports the action.
-
Provider::browse(filter)trait method. Lets prefix-only providers (Symbols, Emoji, Clipboard, Scripts, Tags, Bluetooth, Wireplumber, Playerctl, ProviderList, Ssh, Command) fill their category tab with real content — typing inside the tab also narrows by filter. Default impl falls through tosearch(filter)so providers like Apps / Calculator / Websearch work unchanged. -
Provider::can_delete(item),delete_item(item),alt_action(item),category()— four new optional trait methods that drive the Delete / Ctrl+Enter / Tab strip features. All have sensible defaults so existing providers compile without edits. -
DisplayItemwrapper — runtime-stamped decorations (pinned flag, quick-key digit) handed to the UI. Providers still emit rawLauncherItem; the runtime wraps each one after scoring so the prefix-only providers don't need to know about pins or quick keys.
Changed#
- Launcher redesign — clock-menu visual language. The launcher now reads like the rest of the mshell card stack rather than the previous flat list:
- Search header: bigger entry font, accent ring on focus (mirrors the calendar-hero day number tone), small chip-style fuzzy/exact badge.
- Result list: deep margo-tone card (
--surface-container-lowest- 1 px
--outline-variantborder) — clear "well inside the panel" effect instead of the previous mid-grey wash that blended into the menu surface on the tight Margo palette.
- 1 px
-
Result rows: transparent default, hover picks up
--surface-container-high, selected row flips to--primarywith icon + label + quick-key + ★ all reflowing to--on-primaryso contrast survives the tint swap. -
Default browse pipeline — runtime tracks an
active_category. Selecting a specific tab bypasseshandles_searchand callsbrowse(filter)on every provider in the category, so the prefix-only providers actually contribute to their tab. The All tab keeps the standard search pipeline. Runtime also adds a name+description substring post-filter for category-tab mode so providers that don't filter themselves still respond to typing.
Fixed#
-
Insert tab icon — switched from
format-text-symbolic(missing in MargoMaterial → rendered as missing-icon glyph) toinput-keyboard-symbolicwhich exists in MargoMaterial / kora / breeze / Adwaita. -
Bind hint chips repaint contextually — selecting a calculator result drops Pin / Remove / Alt-action; selecting a pinned app flips the chip label from "Pin" to "Unpin" automatically.
[0.6.2] – 2026-05-17#
Added#
- mshell-launcher — provider-based app launcher with 19 providers.
The legacy single-purpose
AppLauncherwidget is replaced by a provider runtime + a uniform 0–200 scoring scale so results from every source interleave cleanly. Providers ship in two crates:
Compositor-independent (in mshell-launcher):
- Apps — fuzzy-search desktop entries, frecency-boosted.
- Calculator — inline math via evalexpr, 2+2 → 4, sqrt(2) etc.
- Session — Lock / Logout / Suspend / Reboot / Shutdown.
- Settings — jumps directly to a Settings sidebar section.
- Command (>cmd echo hi) — run a shell command, history-aware.
- Scripts (>start brave) — fuzzy-launch start-* scripts from
$PATH, frecency-boosted.
- Clipboard (>clip) / Clear (>clear) — history browse / wipe.
- Symbols (.arrow) — Unicode special chars (→ ± π …).
- Emoji (:smile) — keyword emoji picker.
- Websearch (g/y/ddg/gh/aur/arch/wiki) — open the
query in the default browser.
- ProviderList (;) — discoverable cheatsheet of every prefix.
- Playerctl (player) — MPRIS play / pause / next.
- ArchPkgs (p) — Arch / AUR package search.
- Wireplumber (audio) — sink / source switcher.
- Bluetooth (bt) — bluez 5.65+ paired-devices picker.
- Ssh (ssh <host>) — opens $TERMINAL -e ssh <name> against
hosts in ~/.ssh/assh.yml (assh format).
Compositor-aware (in mshell-frame, pull mshell-margo-client):
- Windows (win [query]) — alt-tab-style open-window switcher.
- Mctl — margo compositor quick-actions (wallpaper next, twilight,
screenshot region, …).
- Tags (tag [N]) — switch focused output to tag N (1–9), with
glyph indicators ● active / ◐ occupied / ○ empty.
Cross-cutting:
- Frecency cache at ~/.cache/margo/launcher_usage.json (boost
5·log2(1+count) applied in both browse and command-mode dispatch).
- Command history cache at ~/.cache/margo/launcher_command_history.json.
- Toast notification on activation (visual feedback).
- > palette enumerates every provider's commands().
- Launcher settings page with cache-clear buttons + scripts list.
-
mshell-auth: real PAM authentication. Shared
libpamFFI extracted from mlock so mshell-lockscreen can actually unlock. The previous PAM stub always failed; mlock's libpam wiring now lives in a sharedmshell_auth::pammodule (avoids the bindgen/clang-sys problem thepam-syscrate has on Arch). -
Margo layout switcher — in-frame menu (rewrite). Replaces the legacy in-bar
gtk::PopoverMenu(xdg_popup, detached feel) with a regular menu surface that slides out from the bar like every other menu in mshell. -
mshell-settings — Menus promoted to top-level sidebar entry. Previously buried under a sub-section. Tab / Up / Down now walk the left sidebar.
Changed#
-
mshell-margo-client:
Reactive::watch()snapshot-on-subscribe. New subscribers receive the current value before any subsequentset()broadcasts (BehaviorSubject semantics). The old change-only stream missed the single startupset()for the workspaces vec (margo'stag_count = 9is fixed, so the membership never changes in steady state) — symptom was bar widgets that sat empty until the user opened a window. The watcher now fills in on the first scheduler tick. -
mshell-margo-client: inotify-based
state.jsonwatch. Replaces the 250 ms steady-state poll with kernel-driven wakeups (notify v9, parent-dir watch so atomic-rename writes survive). A 2 s polling loop stays as a safety net (init failure, parent dir not yet created). Idle CPU drops since mshell is no longer waking up 4× per second forever. -
mshell-style: compile-time baseline = Margo brand palette. SCSS
_colors.scssships the Margo (Dracula-style) palette instead of Everforest so the first paint on first login matches the steady state. -
mshell-style: matugen output cached to disk. Last successful matugen CSS is atomically written to
~/.cache/mshell/last_theme.cssand loaded synchronously at startup. On every login after the first there's no theme flash — the cached palette paints from frame one and the async matugen run that follows is visually a no-op. -
margo: state.json
active_outputis now pointer-first. Previously the field tracked the focused-client's monitor, which left it stuck on the old output when the user moved the cursor (or ranfocusmon) to an empty monitor. Mshell's IPC handler then routedSuper+Spaceto the wrong frame. The field now follows the pointer monitor and refreshes on cursor crossings +focusmondispatches. -
mshell: settings deep-navigation race + +1 pt fonts + scale slider. Launcher → Settings → specific section no longer slams Settings shut. All Settings fonts wrapped in
calc(Npx * var(--font-scale-settings, 1.0))so the Settings → General "Settings font scale" slider rescales the panel dynamically. -
mshell: symlink-preserving config writes. Writing through
dcli/stow/chezmoisymlinks no longer replaces the link with a regular file. -
margo_tags widget cleanup (~150 lines). Now that the underlying Reactive race is fixed (BehaviorSubject + inotify +
focused_idxnull-parse), the five-layer belt-and-suspender stack (cold-start poll, brute-force timer, bootstrap_rows fallback, …) is gone. One clean subscriber loop.
Fixed#
-
The big one — mshell-margo-client parses
nullfocused_idx. Root cause of "tag pills on the bar stay empty until the first window opens, every login". At session start margo writesfocused_idx: null(no client focused yet); the old schema requiredi64and rejected the whole document withinvalid type: null, expected i64.apply_snapshot()never ran,service.workspacesstayed empty, and the snapshot-on-subscribe stream yielded an empty vec. Opening any window flippedfocused_idxto a real integer → parse OK → 9 workspaces published → pills appeared. Now declaredOption<i64>with an explicit deserializer documenting the wire shape. Verified end-to-end after a fresh reboot: 9 pills paint before any window opens. -
mshell-launcher: dispatch fires for every prefix, not just
>. Symbols (.), Emoji (:),;,audio,bt,player,p,ssh,tag,winpreviously hit no provider and silently returned nothing. Every provider'shandles_command()now participates in dispatch. -
mshell-launcher: frecency boost applies in command-mode too.
>start braveno longer always sorts alphabetically. -
mshell-launcher:
btprefix works with bluez 5.65+. Upstream removed thebluetoothctl paired-devicessubcommand; trydevices Pairedfirst, fall back to the old form. -
mshell-launcher:
lockactually locks. Session provider routes throughmshellctl menu session lock(mshell's in-process session dispatcher); the oldloginctl lock-sessionis a no-op under margo (no logind session-locking integration). -
mshell-launcher:
;cheatsheet click writes to the search entry. Previously updated the internal filter without touching the visibleGtkEntry. -
mshell-launcher: Settings deep navigation race. Activating
settings:displayno longer racesCloseMenusagainstOpenSettingsAtSection. -
mshell-ndns: probe accumulates DNS from Global + per-link. Old parser only read Global; DNS servers configured on a single link were missed. Presets now apply via
nmcli con mod+up. -
mshell: DNS preset active highlight + 8 layout icons.
-
mshell: session menu Tab navigation. Tab cycles entries; key controller attached to the menu's root so focus traversal works even before any child has been clicked.
Removed#
- mshell-launcher: stale
set_on_activatedfrom AppsProvider. Dead code from an earlier provider trait shape.
[0.6.1] – 2026-05-16#
Added#
- mshell bar pills — A1, A2, A3, A6, A7, A9 + B6 shipped.
- A1 Privacy — mic + camera in-use indicator with PipeWire backend; pill lights up while any app holds the device.
- A2 SysStat — CPU / RAM / Temp / GPU pills with configurable poll cadence; matches the bar pill density.
- A3 LockKeys — Caps / Num / Scroll lock state via libinput; discrete on/off rendering, no flicker.
- A6 DarkMode — light/dark toggle pill that flips the GTK color-scheme preference + persists across sessions.
- A7 KeepAwake — idle-inhibit toggle pill backed by
ext_idle_notify_v1; the bar shows a coffee glyph while active. - A9 Screen Corners — per-monitor rounded overlay, off by default, exposed in Settings → General. Matches GNOME / macOS rounded display edges.
- B6 System Update — package-manager update count badge with right-click refresh, configurable polling cadence, fixes a pre-shipped exit-1 false-error.
- mshell A5 Calendar — noctalia-style calendar grid inside the clock menu, locale-aware day-of-week header, week numbers.
- mshell Dashboard menu — clock + weather + quick-settings composed into a single panel (hero + 2-col grid + power footer); replaces the old triple-menu pattern.
- mshell quick-settings — card stack matching the clock-menu visual language; rows surface real per-toggle state at-a-glance instead of opaque labels.
- mshell S1 — Settings embedded in frame menu stack. Settings no longer pops a separate window; lives in the same panel as other menus, sharing the frame's animation pipeline.
- mshell-osd — network change OSD + Settings toggle, lowered to a noctalia-style 320 px wide pill.
- mshell Settings — alphabetic sidebar + Widgets group. Sub- sidebar surfaces every pill + menu individually; Bar moves to top level; Display gains a Layout sub-page that drives mlayout; Fonts gets its own entry.
- Twilight Schedule mode — multi-step time-of-day preset
schedule with sunsetr-compatible TOML preset files under
~/.config/margo/twilight/. Readsschedule.conf(HH:MM → preset name) and interpolates in mired space between consecutive presets. First-run seeds a starter set of six presets the user can edit. mctl twilight presetsubcommand family —list,show,set <name> <K> [%],remove,schedule set <HH:MM> <name>,schedule remove <HH:MM>. Writes the TOML / schedule.conf files directly, then best-effort dispatchesreload_configso the change is live immediately.mctl twilight set mode=geo|manual|static|schedule— themodefield is now live-tweakable from the CLI (previously only the six numeric fields).- mshell Settings → Twilight — Open presets folder button
(xdg-open shortcut into
~/.config/margo/twilight/), plus stronger hint text pointing at the newmctl twilight presetfamily. - mscreenshot — three new options:
--delay N/-d Nglobal flag: pop a notification, wait N seconds, then capture. Catches menus / tooltips that close when focus moves to a selector.--output NAME/-o NAMEglobal flag: pin screen-capture modes (screen/sc/sf/si/sec) to a specific output regardless of focus.- Notification action buttons after save (Open / Show in
folder / Delete) — spawns a detached
mscreenshot notify-handlehelper that drivesnotify-send --wait --action, executes the click viaxdg-openorfs::remove_file. Main process exits immediately. - mango 0.13+ backports — three runtime feature ports:
- Split mouse / trackpad acceleration (
mouse_accel_profile,mouse_accel_speed,trackpad_accel_profile,trackpad_accel_speed,trackpad_scroll_factor). Legacyaccel_*keys populate both fields so old configs keep working. width:50%/height:50%fraction syntax in windowrules, capped at 100 %. Prefers the fraction when both absolute and fraction are set.drag_tile_to_tile+drag_tile_smallruntime — dragging a tiled window with the flag on shrinks it to a 300×300 thumbnail centred on the cursor; releasing over another tiled client swaps the two viadata.clients.swap. Restores pre-grab float_geom on release so the thumbnail never lingers.- MargoMaterial icon theme — renamed from OkMaterial, +17 new glyphs covering the new pill set.
Fixed#
isfloating:1rule with no size hint → invisible 0×0 window.apply_window_rulesonly synthesisedfloat_geomwhen the rule carried a width/height/offset hint, soisfloating:1on its own flagged the client floating but leftfloat_geomat (0,0,0,0). Arrange'sif float_geom.width > 0then skipped the apply and the toplevel got configured at 0×0 — listed inmctl clients, rendered in overview, but invisible on the output. Post-loop fallback now synthesises a default geometry (60 % of work_area centred) whenis_floatingended true andfloat_geomis still empty.- windowrule typo class — silent drops.
monitor_name:(the tagrule key) on a windowrule now aliases torule.monitor;is_overlayandoverlaynow alias toisoverlay. Both used to parse but be silently ignored, leading to "the rule doesn't work" reports for typos that look like docs spellings. - mshell menus opened on the wrong monitor after first
reboot. Frame routing now reads the focused-client's monitor
instead of
active_output, which stayed pinned to the pre-restart selection. - mshell dashboard duplicate hero — the panel rendered two clocks at the top after the hero + grid restructure; cleaned.
- mshell-settings —
Add widgetmenu didn't scroll for users with > ~12 widgets; wrapped in aScrolledWindow. - mshell-settings —
Widgets → Layoutrenamed toWidgets → Menusfor accuracy. - mshell-settings — sidebar icons for Fonts and Display were missing or swapped after the alphabetic restructure.
- mshell — bar minimum_height crash + debounce spin. A spin button driving live re-layout could push the bar's minimum below the actual content and crash gtk's measure pass.
- mshell — screen corners default off (the previous default enabled them globally, surprising users) + Twilight schedule panel becomes visible when Mode = Schedule.
- mlayout — follow symlinks in gather_layouts so users who symlink their layouts directory get listed correctly.
- mshell session menu Tab / Shift+Tab / Ctrl+N / Ctrl+P / Ctrl+J
/ Ctrl+K focus-walk attempts shipped (four iterations:
EventControllerKeydefault + Capture phase,ShortcutControllerLocal-Bubble + Local-Capture). Number keys 1–5 work; Tab + Ctrl-letter cluster still doesn't. See road_map B9 for the open follow-up. - PKGBUILD — also remaps C build-script paths to silence the
$srcdirdebug-info warning.
Changed#
- mshell-matugen owns its own CLUT — Margo's theme is independent of Dracula references; previous wrapper around matugen-pure for the Margo palette is now a first-class palette inside mshell.
- Twilight owns
~/.config/margo/twilight/instead of sharing sunsetr's directory. Migration is automatic — the first run of Schedule mode bootstraps the new directory. - mshell-settings restructure — Bar moves to top level; Widgets becomes a group containing per-pill + per-menu pages; sidebar is alphabetised.
- Bar font scaled to noctalia size (~13.3 px) for visual parity with the noctalia reference.
[0.6.0] – 2026-05-15#
Added#
- Wayland protocol surface — 16 new globals advertised in one sweep.
Cross-checked margo's smithay
delegate_*!macros + hand-rolled globals against niri and Hyprland on 2026-05-15. Margo's surface grew from ~38 to ~54 advertised globals, passing niri (~41) and pursuing Hyprland (~62) on standard protocols. Three protocols (zwp_xwayland_keyboard_grab_v1,xdg_toplevel_icon_v1,xdg_toplevel_tag_v1) are advertised by margo alone among the three. Full side-by-side audit lives indocs/protocol-comparison.md; work plan inroad_map.md§15.10. Shipped in commitsdc44818+74a0edb+c146aac: zwp_keyboard_shortcuts_inhibit_v1— VNC / RDP / VM clients can grab host shortcuts.input_handler.rsshort-circuits the keybinding match when the focused surface has an active inhibitor; auto-activate policy matches niri.zwp_pointer_gestures_v1— touchpad pinch / swipe / hold forwarded to clients (Firefox pinch-zoom, GNOME, Inkscape).xdg_foreign_v2— cross-process surface embedding for Firefox / Chromium Picture-in-Picture and xdg-desktop-portal screencast.wp_single_pixel_buffer_v1— solid-color buffer fast-path.zwp_tablet_manager_v2— Wacom / Huion drawing tablets. Folds the orphanTabletSeatHandlerimpl that was sitting unwired in state.rs into a proper handler module.wp_security_context_v1— Flatpak / sandboxed clients. Handler inserts the listener source into margo's calloop; restricted- client enforcement is a follow-up.org_kde_kwin_server_decoration— legacy KDE deco for older Qt5 / KDE apps. Default mode Server (matches SSD-first policy).wp_content_type_v1— game / video / photo surface hints.wp_fifo_v1+wp_commit_timing_v1— newer presentation pacing protocols.wp_alpha_modifier_v1— per-surface alpha hint.xdg_wm_dialog_v1— modal-dialog hint.zwp_xwayland_keyboard_grab_v1— XWayland-side keyboard grab. Direct complement tokeyboard_shortcuts_inhibit_v1— same VNC / VM story via the X11 mechanism. Handler maps the XWayland-managed wl_surface to itsMargoClient.windowso the grab attaches to the correct toplevelFocusTarget.xdg_toplevel_icon_v1— toplevels ship inline PNG / SVG icons; smithay caches them on the surface asToplevelIconCachedState. mshell taskbar / active-window pill consumer is the natural next step.xdg_system_bell_v1— logged-only for now; routing to a sound daemon / notification toast is a future enhancement.wp_pointer_warp_v1— programmatic cursor warp; default no-op (opt-in policy).xdg_toplevel_tag_v1— semantic tags + description strings; default no-op, could feed window-rule matching down the road.- mshell session power menu — Lock / Logout / Suspend / Reboot /
Shutdown with 1-5 number-key actions, 3-second countdown
confirmation, configurable command overrides per-action via
[session]config, Settings UI entry,super+deletekeybind, andmshellctl menu session [action]IPC. Tab / Ctrl+N navigation works (focus delivered through a 160 ms post-reveal glib timeout to clear smithay'ssync_keyboard_modedebounce). mshellctl menu notifications {clears,read}—clearsis destructive (history wipe);readmarks currently-visible popups as read without dismissing history.zwp_virtual_keyboard_v1— wayvnc / wtype / ydotool / IMEs can inject synthetic key events into the focused surface. Opens the protocol to all clients (the wayland socket is already per-user).
Fixed#
- XWayland keyboard input not delivered to X11 clients — the
KeyboardTarget for FocusTargetimpl forwarded only viainner_wl_surface(), which returnsNonefor X11-backed windows. As a result, smithay'sKeyboardTarget for X11Surface(the path that callsXSetInputFocus+ sendsWM_TAKE_FOCUS) never ran when an X11 window had keyboard focus. Pointer events arrived through the Wayland-pointer-surface path, so touchpad worked but keys never reached the X11 client. NowFocusTarget::Windowvariants with an X11 underlying surface forward keyboard events to theX11Surfacedirectly. Fixes vncviewer / xfreerdp / X11 apps under margo not receiving keyboard input while niri / Hyprland worked. - mshell config directory rationalized. Moved from
~/.config/mshell/to~/.config/margo/mshell/so all margo- related config lives under one tree. Affectsprofiles/,styles/, andicons/lookup paths. - Session menu keyboard nav was unreachable. The session menu's
EventControllerKey was wired but the focus path was broken —
RevealChangedwas not forwarded into the session widget, and the bar's hardcoded broadcast list omitted it. Both fixed, plus a 160 ms post-revealglib::timeout_add_local_oncesofirst.grab_focus()lands after smithay's layer-shell focus debounce. - Settings → Session entries typed right-to-left. The
gtk::Entrywidgets had#[watch] set_textbindings that fed back into themselves on every keystroke, resetting the cursor to position 0. Entries now seed text once at init and write viaconnect_changedonly — no reactive read-back loop.
Changed#
KeyboardTarget for FocusTargetdispatches X11 vs Wayland through a newinner_x11_surface()accessor. Wayland-native variants (Window/Wayland, LayerSurface, SessionLock, Popup) keep the existingWlSurfaceforwarding.
[0.5.0] – 2026-05-14#
Fixed#
- Menu content widgets recreated ~once per second. The menu's
SetWidgethandler tore down and rebuilt every menu's content controllers unconditionally; the coarse config store re-notifies every effect bound to it, so any unrelated config touch recreated all the menus. The ndns / nufw / npodman menu widgets shell out toufw/nmcli/podman/resolvectl/mullvadon init, so this meant a steady subprocess storm — their 30/60/120 s refresh intervals never even applied because the widgets never lived that long. The bar already guarded against this; the menu now does too. Idle CPU ~25% → ~2%. - Startup RSS spike into the gigabytes. The wallpaper menu's
GridViewfactory spawned one bare OS thread per thumbnail decode; a directory of a few hundred wallpapers, times one bar per monitor, meant hundreds of threads each loading an image at once (~557 threads / 2.2 GB RSS at peak, cgroup peak 6.6 GB). Decodes now run through a fixed six-worker pool — extra binds just queue. Startup peak drops to ~1.5 GB; the mshell process settles at ~400 MB.
[0.4.9] – 2026-05-14#
Added#
- In-tree
mshelldesktop shell. margo now ships its own bar / shell / menu system (GTK4 + relm4 + gtk4-layer-shell), built from the same Cargo workspace — three binaries (mshell,mshellctl,mshellshare) plus helper crates undermshell-crates/. Replaces the need for a separate third-party panel. - Margo-native widgets.
MargoTags(single-row capsule workspace pills with occupancy dots),MargoLayoutSwitcher(driven bymctl layout), media-player pill + rich menu (cover art, seek, controls, follows the playing player), battery pill (charge % + AC/battery state), and an ActiveWindow pill showing the focused window title. - Ported noctalia plugins as first-class mshell modules,
each with a bar pill + layer-shell menu:
npower(power profiles + battery + Cycle / Lock Auto / Idle Toggle),nnetwork(Network Console — Wi-Fi list / connect / rescan),ndns(DNS mode switcher),nufw(firewall),npodman(containers / images / pods),nip(public-IP panel),nnotes(scratchpad / notes / todos). - Wallpaper rotation — change every N minutes, configurable
in Settings → Wallpaper, plus
mshellctl menu wallpaper next/prev/randomto cycle from the CLI. - Idle manager — staged dim → lock → suspend on inactivity,
timeouts configurable in Settings (built on
ext-idle-notify-v1). - Bundled the MargoMaterial icon theme (margo-branded fork of OkMaterial) + new plugin glyphs so the shell renders consistently without relying on the host icon theme.
Changed#
npowerandnnetworkare now reactive over D-Bus. Both widget pairs previously ran per-monitor poll loops that shelled out topowerprofilesctl(a Python script) andnmcli— a sustained ~25% idle CPU and a multi-GB RSS climb on multi-monitor setups. They now read state from the wayle services (power_profile_service(),battery_service(),network_service()); idle CPU drops to ~2-3% with no steady-state subprocess spawning.- The super+d night-light button drives
mctl twilightinstead ofmshell-gamma. - Bar layout: dropped the vertical Left / Right bar surfaces; all widgets migrated to the Top bar. Clock font shrunk one step to match the other pills.
- PKGBUILD builds the compositor-side binaries and the
mshell trio in two separate
cargoinvocations — a single--workspacebuild unifiedzbus'stokiofeature into the compositor, which then panicked at startup.
Fixed#
- margo bar flicker — ported niri's render + frame-callback
pacing, paced
frame_doneto VBlank, dropped thewp_linux_drm_syncobj_v1global, disabled DRM overlay-plane scanout (Intel MTL quirk), and fixed margo-clientArcidentity churn. - margo startup panic —
zbuswas pulled in with itstokiofeature via workspace feature unification; the compositor driveszbusoverasync-ioand has no Tokio runtime, so it panicked before the session came up. - Settings crash from unsanitised
GActionnames derived from widget labels. - Cleared all mshell build warnings.
[0.4.8] – 2026-05-13#
Added#
- Compositor-side wallpaper renderer. margo now paints the
wallpaper itself, behind every window and layer surface, instead
of waiting for an external daemon (
swaybg/swww/ a noctalia background widget) to cover the root color. New top-level config fields: wallpaper = PATH— explicit image path. Resolution chain when unset:~/.local/share/margo/wallpapers/default.jpg(user override)/usr/share/margo/wallpapers/default.jpg(package default)
wallpaper_fit = cover|contain|fill|center— onlycoveris wired through the renderer right now; the other variants parse cleanly so configs picking them don't fail validation. Cover mode crops a centred sub-rectangle of the source whose aspect ratio matches the output, then scales it to the output rectangle (no letterboxing, no stretch). External shells can still draw on top via layer-shell — layer surfaces sit above the background, so a noctalia / swww overlay wins the z-fight regardless.
Changed#
- mlock + compositor share the same wallpaper resolution chain. Both now check the same three locations in the same order, so a clean install never lands on flat dark for either the desktop or the lock screen.
[0.4.7] – 2026-05-13#
Added#
- Default lock-screen wallpaper. A 4K JPG ships at
assets/wallpapers/default.jpgand lands at/usr/share/margo/wallpapers/default.jpgafter install, so a fresh margo session never falls through to a flat dark lock backdrop just because the user's external shell hasn't populatedstate.jsonyet.
Changed#
mlockwallpaper resolution is now tiered. Previous behaviour was state.json or nothing; new chain:state.jsonactive output'swallpaperfield (margo tagrule passthrough — unchanged primary path).~/.local/share/margo/wallpapers/default.jpg— user override./usr/share/margo/wallpapers/default.jpg— package default (shipped bymargo-git). Every layer ismetadata().is_file()-checked, so a stale path in state.json no longer wins against a real fallback. The candidate that lands is logged viatracing::info!so the source of the current lock wallpaper is one log line away from diagnosis.
[0.4.6] – 2026-05-13#
Added#
start-margowatchdog supervisor. New Rust binary in the workspace — wraps margo with a rolling crash budget (--max-restarts 3 --restart-window-secs 60by default), emitssd_notifyREADY=1after spawn andSTOPPING=1on graceful shutdown, preserves the incoming signal when forwarding SIGTERM / SIGINT / SIGHUP to the compositor, and setsPR_SET_PDEATHSIG(SIGKILL)so akill -9 start-margocan never leave an orphaned margo. Single source file (start-margo/src/main.rs, ~230 lines), depends only onanyhow/clap/tracing/tracing-subscriber/libc. Three concrete improvements over Hyprland'sstart-hyprland: crash budget (vs. unbounded respawn), systemd-notify integration (vs. pipe-handshake), and original-signal forwarding (vs. always SIGTERM).contrib/sessions/integration examples. Ready-to-copy Wayland-session glue:margo-uwsm.desktop— display-manager session entry.margo-uwsm-session— UWSM wrapper that resolves the best compositor command (margo-session>start-margo>margo).margo-session— minimal launcher that prefersstart-margo, falls back to baremargo.systemd/user/wayland-wm@margo-session.service.d/10-session-lifecycle.conf— drop-in that setsMARGO_LOG, fires the session target, bumps Nice / CPUWeight. Seecontrib/sessions/README.mdfor the install recipe and the full session chain diagram.
Fixed#
- PKGBUILD now keeps debug symbols.
options=(!lto)was missing!strip, so makepkg's outer strip pass was wiping the symbol table on every install — exactly the failure modeCLAUDE.mdwarns against ("mesa abort inside the render path on overview trigger" coredumps were resolving to?? ??:0for every margo frame).options=(!lto !strip)now matches thestrip = "none"setting that's been in the Cargo release profile all along. The next time margo trips an ABRT,coredumpctl info/addr2linewill name the exact Rust source line instead of a hex offset.
Changed#
- README binary table + install loop.
start-margois now in the table (betweenmargoandmctl), the source-install one-liner installs seven binaries, and a new "Supervisor (start-margo)" section +contrib/sessions/pointer explain the recommended session topology.
[0.4.5] – 2026-05-13#
Fixed#
- Example
config.confnow passesmctl check-configcleanly. The shipped reference produced 32 errors and 2 warnings against the real parser. Three causes: line-continuation\is not honoured (32 multi-line windowrule / layerrule entries collapsed to single lines);super+shift,h/lwas bound twice (thesetmfactpair moved tosuper+alt,h/l, hjkl muscle memory preserved);focuslastaction used in the example doesn't exist in the dispatch table (orphan bind removed). The mirrored README windowrule snippet lost its trailing\too. Result: 121 binds, 30 windowrules, 5 layerrules, 9 tagrules, ✓ no problems.
Changed#
exec-onceblock modernised. Bar / notifications / launcher recommendations updated to reflect the external-shell-first architecture:qs -c noctalia-shell --no-duplicateorwaybarside by side, withfnott/makonotification-daemon alternatives broken out.unreachable!()panic messages. Eight bareunreachable!()sites acrossprotocols/screencopy.rs,protocols/gamma_control.rs,mctl/bin/mctl.rs,mlayout/main.rs,mscreenshot/main.rs, andlayout/snapshot_tests.rsnow carry a one-line why string so post-mortems read context instead of the generic "entered unreachable code" line. Theok_or_else(|| unreachable!())pattern in mctl's output-index resolver rewrote to plainunwrap_or(0)— the original.or(Some(0))already guaranteedSome.- mlock
wl_globalsbinding tightened.if self.X.is_none()guards insidematch g.interface.as_str()collapsed into match-arm guards, and threemin().max()clamp chains rewritten with.clamp(lo, hi).
Removed#
- Stale
#[allow(dead_code)]attributes. margo/src/screencasting/pw_utils.rslost its crate-level#![allow(dead_code)]— the niri-port scaffolding was fully wired up over Phases C / D / F.mlock/src/state.rsfieldconn: the allow was a holdover;Connectionis read every iteration viastate.conn.flush()andstate.conn.backend().poll_fd()inmain.rs.margo/src/state.rs: orphaned attribute aboveDmabufImportHook(blank line in between) moved onto the type alias soempty_line_after_outer_attributestops firing.- Unused dependencies pruned. Manual audit confirmed zero source-level use sites:
margo:keyframe,nix,log(the codebase standardised ontracing).margo-config:regex(window-rule regexes are compiled in the compositor crate, not the parser crate).
Cargo.lock dropped 32 lines of now-unreferenced transitive deps.
Quality#
cargo clippy --workspace --all-targets: 0 warnings (previously 9 — 8 inmlock/src/state.rs, 1 inmargo/src/state.rs).cargo test --workspace: 244 tests, 0 failures.
[0.4.4] – 2026-05-13#
Removed#
mshellcrate. The iced-then-GTK4 bar / notifications / OSD / settings / system-tray stack is gone. margo no longer paints any shell chrome of its own; the bar, launcher, notification daemon, OSD, and settings panels are delegated to anydwl-ipc-v2client (noctalia, waybar-dwl, fnott, …). The compositor side ofdwl-ipc-v2is unchanged.midlecrate. Idle management moves out of the workspace. Anyext-idle-notify-v1client (swayidle, hypridle, stasis, …) works as a drop-in.- Matugen integration.
mshell matugen, the~/.cache/margo/margo-colors.confsource =hook, and the associated PKGBUILD wiring are removed. The Catppuccin Mocha default palette stands on its own; bring your own colour generator if you want Material You. - mlock
mshell.tomlwallpaper fallback. Wallpaper resolution insidemlocknow reads exactly one source —state.json's active outputwallpaperfield — and falls through to the solid dark backdrop on miss.tomlis dropped frommlock'sCargo.toml.
Changed#
- README rewrite. Intro, binary table, install paths, file-layout
block,
At a glancerecipe list, scripting example, and acknowledgements are now consistent with the six-binary scope. Thedwl-ipc-v2bullet was promoted to call out external-shell integration explicitly. - PKGBUILD overhaul.
depends=lost the panel-only runtime pulls (libpulse,pipewire) and gained the previously-implicitpam/cairo/pango(mlock's actual link-time set).optdepends=dropped eleven panel-only recommendations (networkmanager,iwd,bluez,bluez-utils,pipewire-pulse,wireplumber,pavucontrol,nm-connection-editor,blueman,ttf-jetbrains-mono-nerd,checkupdates) and gainednoctalia-shell-git+fnottas the suggested external shells.package()walks a six-binary install loop, ships a hicolor scalable icon, and installs the Rhai init template.
[0.4.3] – 2026-05-12#
Fixed#
- mshell bar no longer shakes on CPU / network refreshes.
system_info(CPU%, Memory%, Temperature) andnetwork_speed(Download/Upload) refreshed every 1-3 seconds. Each refresh changed the value's text-width by a digit-advance (5% → 23% → 100%) and the bar'sanimated_sizewrapper was tweening that width swing over 150 ms — visible as a 1-2 s shake burst every time a background process spiked CPU. Fixed in three layers: Font::MONOSPACEon every numeric bar value — equal advance per digit, so two-digit values are pixel-stable.Length::Shrink(text widget hugs its content) instead ofLength::Fixed— no leading/trailing slack between an indicator and its neighbour. The earlier "fixed-width" iteration over-padded short values ("9KB/s 62KB/s") so the design read as broken on idle systems.build_module_itemnow skips theanimated_sizewrap for SystemInfo and NetworkSpeed specifically. Cross-decade width changes still happen ("9%" → "100%") but reflow is instant rather than animated. Other modules (Workspaces tag switch, Notifications badge churn) keep their animation. Measured: 5× fewer state.json content-burst clusters during passive idle, and zero perceptible bar shake.
[0.4.2] – 2026-05-12#
Fixed#
- Bar layout no longer shakes when an mshell menu opens. Menus
now grab keyboard focus via
KeyboardInteractivity::Exclusive(see 0.4.1 ESC fix), which makes margo report the menu's layer surface as focused — and the C client list only tracks toplevels, soCompositorState.active_windowcollapsed toNone.WindowTitle::recalculate_valuewas overwriting its cached string with thatNone, blanking the bar item, and the resultingLength::Shrinkcontent collapse rippled across every neighbour capsule.recalculate_valuenow early-returns wheneveractive_windowisNoneor the title is empty, holding the last-known toplevel title until a real toplevel regains focus. - IPC menu bindings are globally-scoped toggles again.
mshell msg notifications(and every other menu IPC: media, settings, tempo, dns, ufw, power, podman, updates, system, network) was routing every keypress through the currently active output →ToggleMenuon that monitor's bar surface. With two monitors, ifactive_outputshifted between presses the handler picked the other monitor as the target, opened a fresh menu there, andtoggle_menu's "close menus on other outputs" pass closed the prior surface as a side-effect — visible to the user as "the binding moved the menu instead of closing it". The IPC handler now scans every output for an already-open instance of the requested type first; if any exist, it closes them all and bails before reaching the open path.
[0.4.1] – 2026-05-12#
Highlights#
Polish + correctness pass over the 0.4.0 release. Two visible themes:
- mshell bar gets a noctalia-grade information density layer — active workspace accent stripes, audio/brightness progress fill rails, battery threshold borders, tray collapse, tempo two-line composite, notification dot indicator — without abandoning the minimal/sakin character of the original design.
- midle becomes browser-aware — D-Bus screensaver / session-manager / portal Inhibit eavesdropping ported from stasis. Helium / Firefox / Chrome no longer block idle just by being open; they only inhibit while they're actually claiming the system's idle inhibitor (e.g. playing a YouTube video).
Added#
- mshell
restartsubcommand. Scans/procfor siblingmshellprocesses, SIGTERMs them, polls for exit with a 3s graceful budget (SIGKILL fallback), gives the compositor 200ms to tear down the bar's layer surfaces, then spawns a detached fresh instance viasetsid(). Replaces thepkill mshell && setsid -f mshell …shell incantation. - midle D-Bus inhibit monitor. Eavesdrops the session bus for
org.freedesktop.ScreenSaver,org.gnome.SessionManagerandorg.freedesktop.portal.Inhibittraffic, correlates method-call serials with their cookie / handle returns per sender, and drops sender rows onNameOwnerChangeddisconnects.Settings::enable_dbus_inhibit(defaulttrue) gates it.midle infonow reports aninhibitorsbreakdown (manual / app / media / dbus) so "why isn't midle firing?" becomes a one-liner instead of a log dive. - Workspace pill polish. Active workspace gets a 2.5px accent bar across ~55% of the pill width (Stack overlay, no height shift); inactive workspaces with open windows get a row of up to 4 small accent dots. Switch animation curve goes from symmetric EASE to EASE_OUT.
- Status cluster density polish.
format_indicatorwraps Warning / Danger states in a 1px tinted border + 10% accent background;BatteryData::get_indicator_stategains a Warning threshold at <30% in addition to the existing <15% Danger.format_indicator.progress(0..=1)stacks a 2px accent fill along the bottom edge — audio (sink) and brightness now expose their live level on the bar. Muted sink hides the bar. - Tray chevron collapse. Once more than 3 icons are registered, the tray compacts to 2 icons + a chevron toggle; click to expand. Keeps the right-cluster from sprawling.
- Module active-state indicator. Every bar capsule now signals
"my menu is on screen" with a 2px accent stripe along its bottom
edge (~60% width, centred). Stack overlay so toggling never
changes bar height.
Outputs::open_menu_type_for_barresolves the open menu once per render and threads it through toModuleItem::is_active. - Tempo rich composite. Opt-in
[tempo] secondary_format = "%a %d %b"renders a 2-line Column: primary clock in semibold atbar_font, secondary string beneath atfont_size.xsand 65% foreground alpha. Tracks tz cycles and the live update tick alongside the primary. - Notification dot indicator. When there are pending notifications, the bell icon gets a 5px accent dot in its top-right corner, hairlined with the bar background. Critical urgency swaps the dot to the danger palette. Independent of the existing count badge — heavy users can keep both.
Fixed#
- ESC inside an open menu now closes it. Previously menus
opened with
KeyboardInteractivity::OnDemand, which margo doesn't auto-focus — the keypress went to the background app instead. Menus now openExclusive; the compositor moves focus onto the menu surface as soon as it appears, ESC reaches mshell'slisten_withEscape handler, and the menu closes. - No more blank flash on menu open. A new
MENU_OPEN_PREROLL_MS = 30constant backdatesopen_atso the first paint lands at ~42% opacity (after ease-out-cubic) instead of α=0; the animation finishes ~150ms later with no perceptual flash. Whentheme.animations_enabled = false,open_at/closing_atare backdated past the animation window so menus render and tear down instantly. - Updates module bar item sizing.
view()was missing.size(bar_font)on both the StaticIcon and the count text, so it rendered visibly bigger than every other capsule. Same fix here: size totheme.bar_font_size, drop the pointless wrapping container, tint the row withpalette.primarywhen there are pending updates. - midle daemon no longer panics margo at startup. The
tokiofeature on midle'szbusdependency was getting unified across the workspace, forcing margo's zbus (pulled via mctl) into a tokio runtime that doesn't exist in the calloop loop. Dropped todefault-features = false, features = ["async-io"]— margo stays on async-io, midle's own tokio runtime can still.awaitzbus futures regardless of the reactor.
[0.3.0] – 2026-05-11#
Highlights#
Phase 2 closing release. Three technical success criteria from §15.8 of the roadmap landed on this branch:
- Snapshot test count ≥ 200 — at 244 workspace-wide (margo 230, margo-config 14). T1 (window-rule matcher), T2 (animation curves), T6 (screenshot region), T8 (theme preset), T9 (session round-trip) drove the expansion.
- state.rs < 3k LOC — at 2944 after eleven sibling-module
extractions (see
Changedbelow). - Cold-path structured-logging migration complete (Q5) — every
tracingcall instate.rs,dispatch/mod.rs,scripting.rs,plugin.rsnow emits structured fields.
Added#
-
Screenshot region selector geometry tests (roadmap T6). 14 new tests lock
ActiveRegionSelector::selection_rectnormalisation across all four drag directions (TL→BR, BR→TL, TR→BL, BL→TR), degeneracy handling (zero area, sub-pixel, vertical/horizontal line),grim -ggeom-string format, drag-lifecycle (begin_dragsnaps anchor,update_dragno-ops withoutbegin,end_dragpreserves rect), and half-pixel rounding edge cases. -
Theme preset tests (roadmap T8). 13 new tests cover
apply_theme_presetfordefault/minimal/gaudy: - Lazy baseline capture on first call.
- Field-deltas locked per preset.
- Preset chains (minimal→gaudy→default, gaudy→minimal→default) restore the captured baseline.
defaultis idempotent under repeated calls.- Baseline survives intermediate manual config tweaks (doesn't refresh from post-tweak state).
-
Unknown preset returns
Errwith a clear "trydefault,minimal,gaudy" hint. -
Window-rule matcher edge-case tests (roadmap T1). 16 new focused unit tests lock the algebra cell-by-cell, complementing the existing two snapshot tests that lock the integration shape:
- id pattern semantics — anchored vs unanchored, case sensitivity, regex alternation, character classes.
- empty / absent pattern semantics —
None,Some(""), empty value against non-empty pattern (the "newly-mapped Electron toplevel before app_id settles" corner case). - multi-field AND semantics — id + title both required; id-only ignores title; title-only ignores id; no patterns matches everything.
- exclude_* precedence —
exclude_idandexclude_titleveto otherwise-matching rules; unmatched exclude does NOT block. -
invalid-regex fallback —
[invalid(unclosed character class) falls back to substring, including the anchor-stripping path (^[invalid$→[invalidsubstring). Workspace test count: 164 → 180. -
Animation curve snapshot tests (roadmap T2). Nine new tests lock the 4-point Bezier evaluator + spring-baked curve shapes against accidental coefficient drift:
near_linear_bezier_endpoints_exact— sanity check.ease_out_expo_shape_locked— sample(0.25/0.50/0.75) bands locked. A real coefficient swap (p0↔p2) pulls each sample out of its band.ease_in_quad_shape_locked— mirror of the above.bezier_bake_is_non_decreasing_in_y— 4 curves × 256 points: non-monotone tables produce mid-flight stutter, so the property is locked in stone.sample_endpoints_round_to_zero_and_one— binary-search ceiling behaviour documented + tested.spring_bake_overshoot_clamped_to_1_05— under-damped spring overshoots get clamped at 1.05 to bound the consumer's slot stretch.critically_damped_spring_is_monotone—damping = 1.0spring reaches target without bouncing.animation_curves_dispatches_every_variant— full AnimationType ↔ curve dispatch exercised.-
sample_clamps_out_of_range_t— defensive boundary check. Workspace test count: 155 → 164. -
Session save/load round-trip test suite (roadmap T9). Nine new tests cover the JSON contract:
save_to_then_load_from_round_trips_every_field— every nested field on both monitors + scratchpads spot-checked after a real disk round-trip (write.tmp→ rename → read back).save_to_is_atomic_via_rename— the tmp file gets cleaned up on success.load_from_rejects_malformed_json— no panic, just an Err.load_from_missing_file_is_io_error— error message chain starts with "read", not parse failure.pertag_lengths_clamp_on_either_side— snapshot shorter or longer thanMAX_TAGSboth deserialise cleanly.unknown_layout_name_in_snapshot_does_not_break_serde— snapshots survive a future layout-name renaming (the loader'sLayoutId::from_name()?silently skips unknowns).scratchpad_entry_defaults_round_trip— defends against a future serde flag tweak.save_to_produces_pretty_indented_json— locks the pretty-printed shape sosession.jsonstays human-diff-able.captured_at_round_trips_through_serde— belt-and-braces on the hand-rolledchrono_like_nowstring. Workspace test count: 146 → 155.
Changed#
- state.rs split to <3k LOC (roadmap Q1). Reduced from 6858 →
2944 LOC (−57 %) by lifting eleven self-contained pieces into
siblings under
margo/src/state/:
| File | LOC | Content |
|---|---|---|
dispatch.rs |
1274 | every keybind / IPC action: kill, focus_stack, view_tag, set_layout, toggle_floating, fullscreen, gaps, zoom, focus_mon, tag_mon, etc. |
scratchpad.rs |
496 | named + anonymous scratchpads, summon, unscratchpad_focused |
data.rs |
450 | MargoClient, MargoMonitor, ResizeSnapshot, ClosingClient, LayerSurfaceAnim, FullscreenMode, HotCorner, rule-match helpers |
overview.rs |
445 | open / close / toggle, alt-Tab cycle, overview_visible_clients_for_monitor |
focus_target.rs |
295 | FocusTarget enum + every smithay trait impl (IsAlive, WaylandFocus, Keyboard/Pointer/Touch/DndTarget) |
state_file.rs |
247 | write_state_file + build_state_snapshot (the JSON mctl reads) |
animation_tick.rs |
245 | per-frame tick_animations body — opacity, open, layer slide, close, move/resize (bezier + spring) |
screencast.rs |
217 | on_pw_msg + stop_cast + start_cast, all #[cfg(feature = "xdp-gnome-screencast")] |
twilight_methods.rs |
132 | force_tick_twilight + tick_twilight + apply/clear_twilight_ramp |
theme.rs |
102 | ThemeBaseline snapshot + tests |
debug_dump.rs |
78 | MargoState::debug_dump (SIGUSR1 / mctl debug-dump) |
Pure lift-and-shift: every method is still an inherent impl on
MargoState and every call site is unchanged. Workspace test
count holds at 244. Touching the overview cycle no longer
recompiles the screencast path, twilight ramp, or state.json
serializer — Phase 2 success criterion §15.8 ticked.
- Cold-path structured-logging migration complete (roadmap
Q5). Every
tracing::info!/warn!/error!/debug!call instate.rs(21 sites),dispatch/mod.rs(10 sites),scripting.rs(12 sites),plugin.rs(3 sites) now uses structured fields (field = ?value, "msg") rather than format-string interpolation. Net wins: journalctl -u margo --output=json | jqslices cleanly: e.g.... | jq 'select(.fields.error)'for every error record, orselect(.fields.cmd | test("nautilus"))for every spawn of a specific command.FocusTarget::enter/FocusTarget::leavedemoted from INFO to DEBUG. They fire on every sloppy-focus crossing and every overview hover sweep — under normal use the journal was 90 %+ enter/leave noise. Thetargetfield keeps full pretty-debug detail for users who actively want to trace focus routing.- Hot-path callers (
backend/udev/{frame,hotplug},input_handlerkeybind + gesture) were already on the structured pattern from earlier sprints; this commit closes the gap. Phase 2 success criterion §15.8 ticked.
[0.2.1] – 2026-05-11#
Rust 2024 edition migration + clippy zero-warnings sweep. No behavioural change — every patched site uses the modern 2024 idiom the compiler now stabilises (let_chains, struct-init spread, end-of-file test modules).
Changed#
- Workspace migrated to Rust 2024 edition.
cargo fix --edition --workspacehandled the mechanical temp-lifetime rewrites across 9 files; the rest of this commit is the modern-idiom follow-up: - 7 collapsible nested
if letblocks rewritten asif let A && let B(let_chains is stable in 2024). Sites:margo-config::parser,margo-ipc::migrate,margo-ipc::bin::mctl(×3),mlayout::main,mscreenshot::main. - 2
let foo = …; fooblocks collapsed to direct return (margo::input_handler,margo::state). theme_baseline_testsrewroteConfig::default() + 9 reassignmentsinto theConfig { borderpx: 3, ..Config::default() }struct-init spread idiom.gesture_testsmod moved from mid-file to end-of-file (clippy::items_after_test_module).gamma_lut::extreme_inputs_clamp_safelytest's tautologicalassert!(v <= u16::MAX)(always true foru16) replaced withstd::hint::black_box(v)so the optimiser can't elide the iteration without losing the "no panic / no NaN cast" intent.
After: cargo clippy --all-targets is zero warnings,
cargo test --workspace still 146 passing. Zero #[allow(...)]
escape hatches added — every warning got a real-code fix.
The 2024 idioms are now in place to enable future let_chains /
gen / async-closure work without per-site nags.
[0.2.0] – 2026-05-11#
First minor bump beyond the 0.1.x sweep. Two headline features — Twilight (built-in blue-light filter, full replacement for sunsetr / gammastep / redshift) and niri-style config validation (structured diagnostics + on-screen overlay + compositor fail-soft) — plus the overview cinematic finishing touches and a fistful of bug fixes from live use.
Highlights#
| Feature | Tagline |
|---|---|
| Twilight | Built-in colour-temperature scheduler inside the compositor's own event loop. Zero new deps, planar wlr_gamma_control_v1 wire format, mired-space interpolation, adaptive tick (60 s ↔ 250 ms), mctl twilight {status,preview,test,set,reset} live control. |
| Config validation | niri-style diagnostics on mctl check-config, fail-soft reload (compositor keeps the previous good config), mctl config-errors query, 10 s on-screen red-bordered banner overlay, warning-aware notify. |
| Overview muscle memory | Modifier-release auto-commit, cinematic dim + thicker focuscolor border on the pick, visual grid order = cycle order, pointer hover no longer reshuffles the grid. |
| Quick wins | 50 ms hotplug rescan coalescer, scratchpad persistence in session-save, on_output_change Rhai hook, dwl-ipc arg-slot mapping finally documented. |
Compared to 0.1.9#
- +21 twilight tests, +6 validator tests → workspace 123 → 146.
- +14 config keys (twilight) + 2 cinematic +
overview_cycle_order. Cargo.toml[profile.release]now keeps line tables in the installed binary so future coredumps symbolize cleanly.- mctl subcommand list reformatted — one neat row per command, no more mid-row wraps.
Added#
- 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. One less moving part, smoother ramps, live config swap.
- Three modes:
geo(sun-elevation from lat/lon — inline NOAA math, nosunriseorchronodeps),manual(HH:MM sunrise/sunset),static(one fixed temp/gamma 24/7). - Temperature interp in mired space; gamma linear. Tanner Helland blackbody fit → 16-bit per-channel RGB LUT, sRGB encode curve baked in, monotonic per channel.
- Adaptive tick: 60 s at steady Day / Night, ~250 ms during a
transition, ~50 ms during a forced
mctl twilight testsweep. - Reuses the existing
wlr_gamma_control_v1plumbing —pending_gammais fed from the tick, the udev frame handler pushes ramps toGAMMA_LUTon the next render. Zero new surface. - 14 new config keys (
twilight,twilight_mode,twilight_day_temp,twilight_night_temp,twilight_day_gamma,twilight_night_gamma,twilight_transition_s,twilight_update_interval,twilight_latitude,twilight_longitude,twilight_sunrise,twilight_sunset,twilight_static_temp,twilight_static_gamma)- new
TwilightModeenum. All clamped at parse time;parser::OPTION_KEYSextended so the validator picks them up automatically.
- new
- Live control via
mctl twilight {status, preview, test, set, reset}.statusreadsstate.json(no IPC roundtrip); the rest dispatch through the compositor. - Disabled by default — flip
twilight = 1to opt in. -
21 new unit tests across gamma LUT, schedule, interpolation, override stack. Workspace test count 123 → 144.
-
Config validation with niri-style diagnostics. Three pieces:
margo-config::validator— new module that re-walks the config file and emits structuredConfigDiagnostics with file, line, column, severity, code, and the offending line snippet. Catches trailing/leading/doubled commas in CSV-shaped values (bind,gesturebind,windowrule, …), missing=separators, unresolvedsource/includepaths, and unknown top-level keys. The allowlist is sourced fromparser::OPTION_KEYS— adding a new option to the parser automatically expands what the validator accepts.mctl check-configrewrite — now drives the new validator plus the existing regex / duplicate-bind checks and renders every diagnostic in niri format (caret arrow, gutter, ANSI colour when the terminal supports it). Exit code 1 on errors, 0 with warnings only.mctl reload --force— pre-flight validation by default; refuses to reload when the file has errors and prints them in the same niri format.--forcekeeps the old "fire and see what happens" behaviour.- Compositor fail-soft on reload —
reload_configruns the validator before parsing; if there are errors it keeps the previous config, setslast_reload_diagnostics, and triggers a 10 s on-screen overlay flag (renderer wiring lands in a follow-up commit). The compositor never applies a broken config. mctl config-errors— queries the live compositor forlast_reload_diagnosticsvia state.json (Hyprland'shyprctl configerrorsanalogue). Empty when the last reload was clean.- On-screen banner overlay — niri-style red-bordered dark
rectangle pinned to the top-right of every output for 10 s
after a rejected reload. Drawn through the existing
SolidColorRenderElementpath (no new shader, no font rasterizer), sits above windows + layer surfaces but below the cursor. Lives inrender::config_error_overlay. The banner is a visual cue only; the actual error list comes fromnotify-send,mctl check-config, andmctl config-errors.tick_animations' event-loop sibling watches the deadline and clears the overlay one repaint after it expires.
Fixed#
-
Alt-release auto-commit now actually fires on the Alt-release event. Previous attempt read
modifiersfrom the release-event filter callback and checked whether the snapshot still overlapped. Problem: xkbcommon updates its modifier state after the filter runs, so on theAlt_Lrelease event the callback still seesmodifiers.alt = true. The intersection check never went empty and overview stayed open until a second alt+Tab press happened. New approach reads the released keysym (handle.raw_syms()) and maps it to itsmargo_config::Modifiersbit directly, subtracts that bit from the pending-cycle snapshot, and commits when the snapshot empties. Works regardless of which order the user releases modifiers — Alt+Shift+Tab still needs both keys released, but in either order. -
Alt+Tab opening overview now auto-commits on Alt release. When the user pressed Alt+Tab with overview closed,
overview_focus_stepcalledopen_overview()first — andopen_overviewresetoverview_cycle_pending+overview_cycle_modifier_maskto default "fresh open" values. That clobbered the snapshot the input handler had just set milliseconds earlier in the keybind-match path. So the Alt-release branch readcycle_pending = false, did nothing, and overview stayed open after the user let go of Alt. Fix: drop the defensive reset fromopen_overview.close_overviewandoverview_activatealready handle the flag's lifetime on the way out; opens reached throughoverview_focus_stepcarry the freshly-set snapshot through to the release branch. -
Alt+Tab first press now jumps to the previously-used window, not back to the focused one. The cycle anchor was
is_overview_hovered.position()only — which isNoneon the very first press while overview is freshly open. TheNonefallback landed at index 0, and in MRU mode index 0 is the currently-focused window (most-recent entry infocus_history). So the first Tab tap looked like a no-op: highlight didn't move, then the second press moved one step. Standard alt+Tab on every other DE (i3 / sway / Hypr / niri / GNOME) is "one tap = jump to the other window." Fix: when there's no in-progress hover, anchor on the focused client's position in the list.dir = +1then moves to index 1, which in MRU is the previously-used window. Same fix benefitstag/mixedmodes: the user's first cycle step moves away from where they already are, not onto it.
Added#
overview_cycle_orderconfig — let the user pick the alt+Tab walk order. New three-valued config key on top of the existing MRU-only behaviour, all wired through one match inoverview_visible_clients:mru(default, preserves 0.1.9 behaviour) —focus_historyfirst (most-recent first), then any remaining visible clients in clients-vec order. The Win/GNOME/Hypr muscle memory.tag— strict tag-1-to-9 order, clients-vec inside each tag. Spatial-memory model: tag 1's windows always come first.mixed— current tag's clients in MRU order, remaining tags in strict tag order. The "MRU where you live, tag elsewhere" hybrid.
Implementation reuses two helpers (push_mru with optional tag
filter, push_tag_order with optional skip mask) — adding any
future mode is now one match arm. Unknown / typo'd values fall
back to mru with a tracing::warn!.
[0.1.9] – 2026-05-10#
Overview reborn. The whole release is one focused theme: nail the
zoom-out-grid UX so it beats Hyprland, niri, and the upstream
mango-ext on the metric the user actually feels — keyboard latency,
spatial continuity, modifier muscle memory. Three iterations to get
there (Phase 3 spatial reverted, fixed 3×3 thumbnails reverted,
mango-ext overview(m){grid(m);} shipped); then cinematic dim +
thicker selection border + MRU cycle + alt-release auto-commit on
top of the same single-arrange path. End state is one of the
shortest overview implementations in any Wayland compositor and the
most responsive.
Added#
- Alt+Tab muscle-memory commit — release modifier to confirm.
Holding Alt and tapping Tab to walk thumbnails was already
smooth, but the user still had to press Enter (
alt+Return → overview_activate) to commit the pick. Now, releasing Alt (or whichever modifier the binding uses) is enough — overview closes onto the highlighted thumbnail and focus moves there. Matches the Win/GNOME/Hypr "hold modifier, tap to cycle, let go to confirm" muscle memory the user expects from alt+Tab outside this compositor. - Implemented as a modifier snapshot taken when an
overview_focus_next/prevkeybind fires, plus a release- branch in the keyboard handler that watches for the snapshot set going to zero (every snapshotted modifier released). - Works for any modifier —
super,Tab,overview_focus_nextbinding would commit on Super release. alt+shift+Tabwalks backwards: releasing Shift alone won't commit (Alt is still held); releasing both Alt and Shift will.-
Two new
MargoStatefields:overview_cycle_pendingandoverview_cycle_modifier_mask. Cleared byopen_overview,close_overview, andoverview_activate.alt+Returnstill works as the explicit commit path. -
Overview cinematic selection — dim + thicker border on the pick. Two new config keys, both clamped, both default-on:
overview_selected_border_multiplier(default1.6, range[1.0, 4.0]) — multiplies the normal border width on the keyboard / hover-selected thumbnail. Border already paintsfocuscoloron selection; the multiplier makes the pick read even at small thumbnail sizes without a separate render path.-
overview_dim_alpha(default0.6, range[0.1, 1.0]) — alpha multiplier applied to non-selected thumbnails while overview is open. The selected thumbnail stays at full opacity. Result: a spotlight on the focuscolor-bordered selection, the cinematic feel niri/Hypr ship by default. The multiplier folds into the existing alpha parameter onrender_elements_from_surface_tree(Wayland live surface) and the X11AsRenderElementspath, so no new render element type is needed — one f32 per window per frame. Set either to1.0to opt out individually. -
Overview alt+Tab now MRU-ordered.
overview_visible_clientswalks the per-monitorfocus_historyfirst (most-recent first), then appends any remaining visible clients in clients-vec order for completeness. Result:alt+Tabsteps through windows in the order the user last touched them — matches every other alt+Tab in existence (i3, sway, Hypr, niri, GNOME). Previous behaviour cycled in map-then-rearrange order, which felt random when the user switched between long-running windows.
Fixed#
- Overview alt+Tab border lit up instantly. The cycle path
(
overview_focus_step) was running a snap-no-slidearrange_monitorafter every Tab press to push the new selection through the layout pipeline. Even at 1 ms duration, the arrange-timeborder::refreshran against per-client move state in flux and the focuscolor border landed one frame after the user expected. Removed the arrange call entirely — Mango-ext overview is a Grid layout, every cell stays put across a cycle, and only the selected state changes. The cycle now flipsis_overview_hovered, callsborder::refresh, requests a repaint — single render to focuscolor, no animation gate, no recompute. ("border anında diğer pencerede değil" → fixed.)
Changed#
- Overview switched from fixed 3×3 per-tag thumbnails to mango-ext
overview(m) { grid(m); }semantics. The per-tag thumbnail grid always carved the work area into 9 cells regardless of window count, so a tag with 1-2 windows ended up at ~⅓ × ⅓ of the screen — "küçük gözüküyor, natif değil." Mango-ext's overview is just a Grid layout over all visible clients (tagset = !0+ Grid + floating-included filter), so cell count = window count. Net effect: 1 window ≈ 90% × 90% of the screen, 2 → side-by-side halves, 4 → 2 × 2 quarters, 9 → 3 × 3 evenly. Cells shrink as window count grows, matching the native MangoWM feel. - Removed
MargoState::arrange_overview_per_tag_gridhelper (~95 LOC including doc) and itsis_overviewbranch inarrange_monitor. - The
is_overviewsetup at the top ofarrange_monitor(layout = Grid+tagset = !0+is_tiledfilter relaxed) is now sufficient — a singlelayout::arrange(layout, &ctx)call produces the dynamic grid. -
hot-corner / alt+Tab cycle / alt+Return commit / 4-finger swipe / snap-no-slide cycle animation all unchanged.
-
Overview reverted from "Infinite Spatial" back to Mango-style per-tag thumbnail grid. Five commits of camera-pan canvas (foundation + state + nav + auto-fit + window-centred cycle) were reverted in one pass after live UX feedback: the live camera felt fiddly compared to a fixed-grid that the user's spatial memory could rely on. Final shape:
- Fixed 3×3 grid (tag 1 top-left → tag 9 bottom-right). Same cell index every time, spatial memory carries.
- Each thumbnail runs that tag's configured layout (Tile / Scroller / Grid / Canvas / …). Scroller tag stays scroller-shaped, grid tag stays grid-shaped.
- alt+Tab MRU cycle keeps the snap-no-slide arrange from the
spatial attempt — each Tab press lights
focuscolorborder on the new selection instantly, no animation kaos. spatial_overviewmodule + design doc + 7 dispatch actions +OverviewMode/overview_modeconfig +MargoState::spatial/spatial_panningfields +SpatialCamera+ frame-tick momentum + scroll-zoom intercept + LMB-drag pan handler all removed. ~600 LOC out, simpler render path, no spatial state to debug.
Added (replaces previous Phase 3 entries)#
- Phase 3 — Spatial Overview live navigation (3 / 3, final). Mouse + scroll + keyboard navigation all wired through the spatial camera; momentum decays every frame on the animation tick. Phase 3 is now fully usable.
- Mouse left-drag on empty overview space pans the camera —
every motion event streams its delta through
pan_by_screen_deltaso velocity feeds momentum on release. - Scroll wheel zooms around the cursor (world point under the cursor stays fixed, niri/paperwm/Aerospace default).
- Keyboard: seven new dispatch actions —
overview_pan_left/right/up/downstep ¼ of the panel each,overview_zoom_in/out× 1.2 / × 1/1.2,overview_zoom_resetsnaps to active tag at config zoom. Bind any of them inside overview for accessibility / no-mouse flows. - Frame tick: the same
tick_animationshop that drives window animations now also ticksMargoState::spatial. While momentum is non-zero or the camera is interpolating toward a target,arrange_allruns and the next frame schedules — so the camera keeps coasting until friction settles it (FRICTION = 0.92per frame,VELOCITY_FLOOR = 0.5 px/framesnap-to-rest). - mctl actions catalogue grew seven entries documenting the pan/zoom/reset surface.
Phase 3 is now feature-complete. Bind freely:
overview_mode = spatial # default
overview_zoom = 0.5
overview_transition_ms = 180
hot_corner_top_left = toggle_overview
bind = alt,Tab,overview_focus_next
bind = alt+shift,Tab,overview_focus_prev
bind = alt,Return,overview_activate
bind = super,Left,overview_pan_left
bind = super,Right,overview_pan_right
bind = super,Up,overview_pan_up
bind = super,Down,overview_pan_down
bind = super,equal,overview_zoom_in
bind = super,minus,overview_zoom_out
bind = super,0,overview_zoom_reset
- Phase 3 — Spatial Overview wired into arrange + state (2 / 3).
Spatial mode is now the default at config level (
overview_mode = spatial— opt out withoverview_mode = grid). On open,arrange_monitorbranches into the newarrange_spatial_overview_geometrieshelper: - Every tag's clients arrange in that tag's configured layout (Tile / Scroller / Grid / Canvas / …) inside a monitor-sized world slot — no override to a single Grid.
- Each client's world rect (tag anchor + local layout output) is
transformed through
SpatialCamera::world_to_screento land itsgeomon screen. Render, border, hit-test paths all readclient.geomunchanged — they don't know spatial mode is on. open_overviewsnapsMargoState::spatialto the active tag's world centre atoverview_zoomso the open transition reads as "stay where I was, zoom out".
Camera is loaded at default centred-zero state from
MargoState::new; pan/zoom input handlers + frame-tick momentum
arrive in commit 3 (final slice). Until commit 3 ships, spatial
overview displays correctly but is static — exactly the visual
the design doc calls for, just without live navigation.
- Phase 3 — Infinite Spatial Overview, foundation (1 / 3). New
module
margo/src/spatial_overview.rs(~450 LOC, 12 unit tests) carrying the foundation for the spatial canvas overview that replaces the legacy single-Grid overview as the default in commit 3 of this slice. Design doc atdocs/design/spatial-overview.mdcovers the whole arc.
This commit is foundation-only — no behaviour change:
* OverviewMode { Grid, Spatial } enum + from_config_str
parser (Grid alias: grid / legacy / flat; Spatial alias:
spatial / infinite / canvas)
* SpatialCamera struct — current + target position, momentum
velocities, zoom clamps (ZOOM_MIN = 0.1, ZOOM_MAX = 1.5),
friction (0.92 per frame), velocity floor (0.5 px/frame for
snap-to-zero)
* Methods: snap_to (hard re-centre), pan_to / zoom_to_target
(set targets without snapping), pan_by_screen_delta (mouse
drag), zoom_around_screen_point (scroll-zoom keeps the
cursor's world point fixed), tick (per-frame integration:
momentum → target, friction, smooth-step current → target)
* Coordinate transforms world_to_screen / screen_to_world —
the single transform every consumer goes through, so arrange,
render, and input can't drift out of step
* World layout: tag_anchor (3×3 grid, tag 1 top-left, tag 9
bottom-right), client_world_rect (tag anchor + local layout
rect), world_bounds
* TAG_PADDING const (64 logical px between tag slots)
Commit 2 (next) wires MargoState::spatial, arrange_monitor
spatial branch, render path passthrough. Commit 3 adds input
handlers (mouse pan, scroll zoom, keyboard dispatches),
frame-tick momentum decay, and spatial-aware
overview_focus_next/_prev.
Fixed#
-
Hot corner no longer leaks through to the lock screen.
update_hot_cornernow early-exits whensession_lockedis true, when the screenshot region selector is active, or when smithay holds a pointer / keyboard grab (xdg_popup grabs, drag-and-drop). Symptom was: pointer in the top-left corner while the lock surface owned focus →dispatch_action("toggle_overview")fired → Tab / Return reached greetd's authentication form and the user landed in the login screen. Three guards added; armed_at stays None so re-entry restarts the timer cleanly after the guard lifts. -
overview_focus_next/_prevborder highlight tracks the selection. The previous attempt calledfocus_surfaceon every Tab press, which fired margo's focus-crossfade opacity animation for each step. The crossfade re-painted all borders mid-cycle (interpolating between focuscolor and bordercolor), so the visible cue was "cursor warps but borders all look smudged". Now the cycle relies on theis_overview_hoveredpath thatborder::refreshalready paints withfocuscolor(border.rs:64), with no crossfade kick. Border, cursor, and hover flag move together on every Tab; commit goes throughoverview_activate(Enter), which runs the focus path once.
Changed#
- Overview rewritten — Mango/Hypr geometric continuity + niri
alt+Tab MRU cycle. The Round 2b/3/4 per-tag thumbnail grid is
reverted in favour of the previous "single Grid arrangement of
every visible client over the zoomed work area" — windows keep a
deterministic spot in the thumbnail, overview reads as a
zoom-out of the desktop, the user's spatial memory survives.
arrange_overview_per_tag_grid,compute_overview_grid_layout,overview_cell_rect,overview_cell_at_cursor,overview_client_at_cursor, andOverviewDragall removed — ~600 LOC out, much simpler render path, no drift between three grid-math implementations. Round 1 (hot corner + zoom config + 4-finger swipe wiring) and Round 2a (geometric zoom + transition_ms wiring) stay.
overview_focus_step now opens the overview on its first press
if it's closed, and every step calls
focus_surface(FocusTarget::Window(...)) so border + smithay
keyboard focus track the cycle. Bind to alt+Tab and the gesture
feels like a real alt+Tab on every other DE: focus moves with
the selection, overview stays open between presses, commit via
Enter (overview_activate).
Try it:
bind = alt,Tab,overview_focus_next
bind = alt+shift,Tab,overview_focus_prev
bind = alt,Return,overview_activate
hot_corner_top_left = toggle_overview
gesture = swipe, 4, up, toggle_overview
overview_zoom = 0.5
mctl actions catalogue now documents the three nav actions
(overview_focus_next, _prev, _activate) explicitly with
the new auto-open / focus-follows behaviour.
Phase 3 mandate (separate sprint): "Infinite Spatial Overview"
— workspace → space, 2D pan-zoomable canvas, semantic grouping,
inertial camera, minimap. Design doc + opt-in overview_mode =
spatial config. Not in this sprint; this overview ships now,
spatial mode lands as an alternative later.
Added#
-
Niri-overview port — Round 4 (dynamic grid). The overview no longer hard-codes a 3×3 grid of all 9 tags; instead, only tags with visible clients on the monitor are shown, and the grid shape is picked to fit: 1 occupied → 1×1 (full-screen thumbnail), 2 → 2×1, 3 → 3×1, 4 → 2×2, 5–6 → 3×2, 7–9 → 3×3. Even at
overview_zoom = 1.0thumbnails were too small on a 1080p monitor because we were always burning 6 cells of pixel budget on empty tags; now a single-tag day uses the whole screen. While a drag is past the 5 px threshold every tag is shown so empty tags become valid drop targets — drag UX unchanged. NewMargoState::compute_overview_grid_layouthelper is the single source of truth for the cell list;arrange_overview_per_tag_grid,overview_cell_rect, andoverview_cell_at_cursorall consume it. Three-way drift gone. -
Niri-overview port — Round 3 (mouse drag-and-drop windows across tags). Inside the overview, left-press on a window thumbnail starts a drag; cursor motion past 5 px arms drag mode and highlights the target tag's cell with a
focuscolorborder; release on a cell rect retags the dragged window to that tag and re-arranges (overview stays open so the user can keep moving things). Release below the 5 px threshold, or outside any cell, falls back to the legacy click-to-activate-and-close behaviour — so a quick click on a thumbnail still opens that window like before. NewMargoState::overview_drag: Option<OverviewDrag>state, plusoverview_cell_at_cursor/overview_cell_rect/overview_client_at_cursorhit-test helpers (kept in math lock-step witharrange_overview_per_tag_grid). Visual feedback is a 4 px accent outline around the target cell — drawn after cursor so the cursor stays on top, beforeupper_layersso the bar still wins z-order.
niri's "drag a window across workspaces" feature, adapted: niri inserts new workspaces between drop columns; margo doesn't (tags are abstract, no spatial "between"), so the drop simply retags onto the cell-tag.
Changed#
-
toggle_overviewis the single dispatch name. Thetoggleoverview/toggle-overview/overviewaliases that briefly landed in 0.1.8 have been removed in favour of one canonical name. Update any keybinds / hot-corner config strings that used the underscore-less spelling. Themctl actionscatalogue entry now readstoggle_overview. -
Niri-overview port — Round 2b (per-tag thumbnails). Overview no longer dumps every visible window into one Grid; instead, each tag (1-9) gets its own thumbnail cell in a 3×3 layout over the zoomed work area, and each cell runs that tag's configured layout — a scroller tag stays scroller-shaped at thumbnail size, a grid tag stays grid-shaped, etc. Per-tag
mfact/nmaster/ layout fromPertag::ltidxsflow through unchanged. Empty tags get an empty cell. Tag → cell mapping: tag 1 top-left, tag 9 bottom-right (matches the 1-9 keypad mental model). NewMargoState::arrange_overview_per_tag_gridhelper drives the cell-by-cell arrange;arrange_monitorbranches into it whenis_overviewis set. Round 3's drag-and-drop will hit-test against these cell rects to drop windows onto target tags. -
Niri-overview port — Round 2a (geometric zoom + transition wiring).
overview_zoom(added in 0.1.8) is now consumed byarrange_monitor: while overview is open, the work area shrinks tozoom × work_areacentered inside the monitor's logical work rect, so windows arrange inside a smaller, centered region — niri's "zoom 0.5" feel without a true scene-tree transform. Layer-shell positioning is unchanged on purpose: top + overlay layers (the bar) stay anchored to the panel edges, matching niri's "background + bottom would zoom in lockstep, top + overlay stay at 1.0" pattern.overview_transition_msconfig is now honoured via a newoverview_transition_ms()helper (fallback 180 ms when config value is 0).
Round 2b (per-tag thumbnails — every tag gets its own mini-layout area, not the current "every window in one Grid") and Round 3 (mouse drag-and-drop windows across tags inside overview) are the next two slices.
[0.1.8] – 2026-05-10#
Niri-overview port — Round 1 (trigger mechanics). The next two rounds (zoom-out / layer-shell handling, and mouse drag-and-drop windows across tags) ship as follow-up releases.
Added#
- Hot corner trigger. Pointer dwelling in a 1×1-logical-pixel rectangle at any of the four output corners fires a configured dispatch action — niri pattern with a dwell threshold so a quick flick past the corner doesn't trigger. Per-corner config; default is "off" until the user opts in.
hot_corner_top_left = toggle_overview
hot_corner_top_right =
hot_corner_bottom_left =
hot_corner_bottom_right =
hot_corner_dwell_ms = 100
Cleared on pointer-leave so out-and-back-in restarts the timer
(matches niri). Action string accepts every known dispatch name
(toggleoverview / toggle_overview / toggle-overview /
overview all alias to the same handler).
- Overview config knobs. Two new fields:
- overview_zoom (default 0.5, clamped [0.1, 1.0]) — wired in
config + state today; the Round-2 layer-shell + zoom-out render
pass consumes it.
- overview_transition_ms (default 180) — replaces the
previously-hardcoded transition duration.
- toggle_overview dispatch aliases. The handler used to only
accept the no-underscore toggleoverview string; now also takes
toggle_overview, toggle-overview, and bare overview so
config strings written to the new hot-corner fields don't have to
guess the spelling. The same handler underpins the existing
keybind path and the (already-supported) 4-finger swipe-up
gesture binding:
Changed#
MargoStategainshot_corner_dwelling: Option<HotCorner>+hot_corner_armed_at: Option<Instant>to drive the dwell timer.update_hot_corner()runs at the tail of everypointer_motionhandler — cheap (4 corner checks per output, no allocation).
What's coming in Round 2 / 3#
- Round 2 (next release): real zoom-out rendering (overview
thumbnails respect
overview_zoom), layer-shell handling (background + bottom layers zoom along, overlay + top stay at 1.0 — niri pattern). - Round 3: mouse drag-and-drop windows across tags inside the overview, with target-tag visual highlight.
[0.1.7] – 2026-05-10#
First Phase 2 release. Single user-facing feature: a real fix for
fullscreen — the prior togglefullscreen looked full-screen but the
bar (noctalia / wlr-bar) kept rendering on top, covering the
window's top portion. Now there are two distinct fullscreen modes,
each on its own keybind.
Added#
togglefullscreen_exclusivedispatch action. True fullscreen: window resizes tomonitor_area(entire output) and the render path suppresses every layer-shell surface on that monitor — the bar literally disappears while exclusive fullscreen is active. Right behaviour for mpv / browser fullscreen movie / fullscreen games. Aliases:togglefullscreen-exclusive,togglefullscreenexclusive.
Changed#
togglefullscreennow respectswork_area. The default fullscreen action used to size the window to the fullmonitor_areaeven though the layer-shell bar kept rendering on top — the window's top region was permanently covered. Now the window resizes tomonitors[].work_area(after layer-shell exclusion zones), so the bar stays visible and the window covers every other pixel below it. StandardF11feel.MargoClientgains afullscreen_mode: FullscreenMode { Off, WorkArea, Exclusive }field alongside the existingis_fullscreen: bool. The bool is kept in lock-step (is_fullscreen == fullscreen_mode != Off) for backward-compat with 20+ callsites in render / IPC / window-rule paths;set_client_fullscreen_mode(idx, mode)is the new source of truth andset_client_fullscreen(idx, bool)shims toWorkArea.xdg_toplevelsize hint matches the active mode so client first-frame buffer allocations land correctly.
[0.1.6] – 2026-05-10#
A mvisual UX hot-fix. cargo run -p mvisual flashed a window for a
single frame and exited — the design tool was unusable.
Fixed#
mvisualwindow no longer flashes-and-quits. GApplication registers itself on the session bus by default; if a staledev.margo.visualname was still claimed (most commonly: a previouscargo runsession whose dbus name hadn't been released), the second start registered as remote, forwarded theactivatesignal to the (now-dead) primary, and exited immediately. Symptom was a window appearing on screen for one frame then disappearing, with no error output. Fixed by passinggio::ApplicationFlags::NON_UNIQUEon the Application builder — mvisual is a developer / design tool, multiple parallel instances are intentional.
[0.1.5] – 2026-05-10#
A 0.1.4 hot-fix. The theme / session-save / session-load
subcommands wired in 0.1.4 ran without error but had no visible
effect — every preset switch silently fell through to default.
mctl run <file> was carrying the same latent bug. One commit, one
slot-fix; everything user-facing actually works now.
Fixed#
mctl theme <preset>payload now reaches the dispatch handler. dwl-ipc-v2'sdispatchrequest takes 5 string slots; margo maps them asarg1 → arg.i(numeric parse),arg2 → arg.i2,arg3 → arg.f,arg4 → arg.v(string),arg5 → arg.v2. The 0.1.4Theme { preset }clap variant was stuffing the preset into slot 1 — the i32 parse silently failed,arg.vstayedNone, andtheme gaudyquietly resolved to thedefaultpreset. Now lands in slot 4 alongside the convention every other string-payload dispatch follows.session-save/session-loaddon't take args so they were already correct; the latentmctl run <file>bug (path stuffed into slot 1,run_scripthandler readsarg.v) is fixed in the same pass.
[0.1.4] – 2026-05-10#
A "0.1.3 follow-up" release. The 0.1.3 commit added the theme /
session_save / session_load dispatch actions on the compositor
side but didn't wire them as mctl clap subcommands — running
mctl theme gaudy died with "unrecognized subcommand". This fixes
that, plus a hot-path structured-logging migration and a road-map
reorganisation that were already pending in [Unreleased].
Fixed#
mctl theme/mctl session-save/mctl session-loadsubcommands. Three newCommandvariants in themctlclap parser route through the existing dispatch path. No compositor-side change — the dispatch handlers landed in 0.1.3, only the CLI surface was missing.mctl --helpnow lists all three;session-save/session-loadaccept the underscore alias too for symmetry with the action name.
Changed#
- Hot-path logging migrated to
tracingstructured fields.backend/udev/frame.rs,backend/udev/hotplug.rs, and the gesture + keybinding-match log lines ininput_handler.rsnow emit per-event fields (output = %name,reason = …,queued = …,error = ?e) instead of pre-formatted strings. Run withtracing-subscriber's JSON formatter andjournalctl -u margo --output=json | jqslices per-output traces cleanly. Cold-path callsites (state.rs focus / dispatch chatter, scripting, plugin loader) still use the old format-string shape and convert piecemeal as touched. Roadmap §16 do-over wishlist item.
Docs#
- Roadmap §15 reorganised into "Outstanding work — external triggers" with three sub-tables: upstream-blocked (smithay PRs), test-setup-deferred (live PipeWire), and hardware-driven (W2.2b pixman, W2.3 tablet). All margo-internal long-tail items are shipped — what's left is gated on something margo can't unblock by itself. §16 do-over wishlist marks the WindowRuleReason and RenderTarget refactors as shipped/partial; structured logging note added.
[0.1.3] – 2026-05-10#
A "post-W-sweep capability + cleanup" pass. Four features and three refactors land between the 0.1.2 release and now; together they close out every internal long-tail item the road map flagged.
Added#
mctl theme <preset>— live visual theme switch. Three built-in presets (default/minimal/gaudy) toggle border thickness, shadow depth, blur, and corner radius without touching the config file. First switch captures atheme_baselinesnapshot sodefaultalways reverts to "what the config said";mctl reloadinvalidates the baseline so the nextdefaultlands the freshly- parsed values. (feat(theme))mctl session save/mctl session load. JSON snapshot of every monitor's tag selection, per-tag layout / mfact / nmaster / canvas-pan to$XDG_STATE_HOME/margo/session.json. Atomic write via temp + rename so a crash mid-write can't shadow a good file. Open windows aren't captured (clients are bound to processes — the spawn line lives in user-space). Snapshot entries for absent monitors are logged + skipped on load. Versioned format with rejection on mismatch. (feat(session))- Touchscreen multi-finger swipe →
gesture_bindingsdispatch. True touch events (TouchDown/Motion/Up) are now distilled into the same(fingers, motion, mods) → actionlookup the touchpad swipe path uses. A binding written asgesture = swipe, 3, right, view_tagfires regardless of input surface. (feat(input)) presentation-timereal per-output VBlank seq. Theseqfield inwp_presentation_feedback.presentedwas hardcoded to 0; it's now a monotonicOutputDevice::vblank_seqbumped at the head of everyDrmEvent::VBlankhandler. Frame-pacing-sensitive consumers (mpv--vo=gpu-next, kitty render loop, gnome-shell'sgetRefreshRatepolling) now see the contract the protocol promises. (feat(presentation-time))
Changed#
- Window-rule reapply unified via
WindowRuleReasonenum. Three trigger sites (finalize_initial_map, lateapp_idsettle,mctl reload) previously calledapply_window_rules_to_clientwith no shared signal of why a rule was firing. NewWindowRuleReason::{InitialMap, AppIdSettled, Reload}is passed to a singlereapply_rules(idx, reason)path; the debug log records the trigger so aRUST_LOG=margo::state::windowrule=debugtrace tells you which call site landed. Roadmap §16 #4 do-over wishlist item. (refactor(state)) RenderTargetenum replaces(include_cursor, for_screencast)bool pair.build_render_elements_innercallsites now readRenderTarget::Display/DisplayNoCursor/Screencast { .. }instead of two anonymous booleans the reader had to remember the meaning of. Internalflags()helper unpacks back into the same two bools the function body still uses, so the hot path is unchanged. Partial address of roadmap §16 #1. (refactor(udev))
[0.1.2] – 2026-05-10#
A "catch-and-surpass-niri sweep" tail-end release. Three commits land the last three queued W-items: a GTK4 design tool, HDR Phase 4 ICC scaffolding, and the udev backend split into focused sub-modules. No behaviour changes for existing daily-driver flows — the W-sweep is about coverage and architecture, and the test suite (181 passing) + clippy gate stay green at every step.
Added#
mvisualdesign tool (W4.5). New workspace binary (cargo run -p mvisual) renders all 14 tile-able layouts side-by-side as live thumbnails plus a 1‒9 tag rail that mirrors the compositor'sPertagso users can rehearse per-tag layout pinning before committing to a config. GTK4-rs UI; live re-arrange on every parameter tweak (window count / mfact / nmaster / inner+outer gaps / focus index / scroller proportion). Wider thanniri-visual-testson two axes: every layout visible at once (no click-cycle), plus the per-tag pinning preview niri can't host since it has no tags.margo-layoutsworkspace crate. Pure layout arithmetic (~1040 LOC, no smithay/wlroots deps) extracted frommargo/src/layout/{mod,algorithms}.rsso the compositor binary andmvisualconsume the exact samearrange(). The 38-snapshot layout regression suite stays in place, just retargeted at the new crate.- HDR Phase 4 — per-output ICC profiles (scaffolding).
margo/src/render/icc_lut.rs(~390 LOC, 6 unit tests).colordD-Bus client (org.freedesktop.ColorManager+ Device + Profile proxies) resolves a DRM connector name → assigned ICC path;lcms2-backedbake_lutruns an identity 33³ grid through sRGB → display-profile transform;to_atlas_rgba32fre-lays the cube as a 1089 × 33 RGB texture so the GLES2 path can sample it without asampler3D. CPU-side trilinear sampler doubles as the GLSL reference for theICC_LUT_FRAGshader (ships asconst).MARGO_HDR_ICC=1env gate. Runtime activation upstream-blocked on smithay'scompile_custom_texture_shaderexposing a second-sampler hook.
Changed#
backend/udev.rs(3934 LOC) split into 4 sub-modules (W4.1).backend/udev/is now a directory:mod.rs(2873, ~27 % shrink, the orchestrator),helpers.rs(77, transform / CRTC pick / refresh-duration / monotonic clock),mode.rs(234, mode select + apply viaDrmCompositor::use_mode),hotplug.rs(405, rescan + setup_connector + migrate-clients-off-output),frame.rs(331, render dispatch + presentation feedback + scanout flags). Type visibility forOutputDevice/BackendData/GammaPropslifted topub(super)so submodules reach shared state without trait indirection. Behaviour-preserving — all 181 tests green at every extract step. The road map's earlier "split into separate crates" framing was rejected: niri's "7 backend crates" turn out to be smithay's feature flags, not crates, and the real wins (incremental compile + readability) land at sub-module granularity without trait-abstractingMargoState(~3000 LOC churn for no downstream consumer).
[0.1.1] – 2026-05-10#
A focused popup-handling bug-fix release. Three commits, one chain of root causes — GTK and Chromium menus (Helium 3-dot, Nemo right-click, file-picker dropdowns) were unusable because xdg_popup wasn't being driven through the full xdg-shell handshake. After this release, popups, right-click context menus, and double-click navigation work as expected on every xdg-shell client we've tested.
Fixed#
- Initial configure for xdg_popups. Margo's commit handler
was pumping the initial
xdg_surface.configurefor toplevels and layer surfaces but never for popups. Without it, GTK and Chromium would create the popup, send a bufferless commit, and sit forever waiting for an ack — the popup was tracked internally but never mapped, and clients gave up silently. Visible symptom: Helium's 3-dot menu, Nemo's right-click context menu, and any GTK chevron dropdown did absolutely nothing on click;GDK_BACKEND=x11worked because XWayland takes a different protocol path. The commit handler now mirrors smithay anvil's pattern: find the popup viaPopupManager, and ifis_initial_configure_sent()is false, callsend_configure()on the first commit. Also restores the original double-click navigation in Nemo, which was failing as a side effect of the same broken popup state. - Pointer/keyboard input no longer steals focus during an active
grab. Even after wiring up
PopupPointerGrab/PopupKeyboardGrab, GTK and Chromium menus would still flicker open and close becausehandle_pointer_buttonandapply_sloppy_focuscalledstate.focus_surface(...)before forwarding the click. The toplevel-levelfocus_under()lookup can't see popups (popups aren't instate.space.elements()), so it returned whichever toplevel the popup happened to overlap geometrically — and our side effects (selected, dwl-ipc broadcast, scripting hooks, border crossfade, sloppy-focus arrange) ran against the wrong window while the popup was still up. The visible symptoms were "menu opens for one frame, then closes", right-click producing a brief flash, and Nemo double-clicks getting routed as window focus swaps. Both call sites now skip our focus logic whenpointer.is_grabbed()orkeyboard.is_grabbed()— smithay's active grab owns focus routing for the duration, and dismissal re-establishes focus through the normal motion path. xdg_popup.grabnow sets up a real popup grab. Browser context menus (Helium / Chromium right-click), Helium's 3-dot toolbar menu, Nemo's right-click context menu, GTK file-picker dropdowns, and any other popup that requestsxdg_popup.grabcould open and instantly dismiss because margo was only flipping keyboard focus to the popup wl_surface — pointer events kept being delivered to the parent toplevel, so the toplevel saw a click "outside" the popup it had just opened and tore the popup down. The visible symptom was "menu doesn't open" / "right-click doesn't work" / "double-click does nothing". Margo now goes through the standard smithay path:PopupManager::grab_popupvalidates the serial, ensures the popup is the topmost in its chain, and returns aPopupGrab; margo then installs that grab on both the keyboard (PopupKeyboardGrab) and pointer (PopupPointerGrab) so events drill through the popup hierarchy and clicks outside dismiss the chain. Implementing this required two trivialFromimpls —From<PopupKind> for FocusTargetandFrom<FocusTarget> for WlSurface— that the previous workaround had explicitly side-stepped.
0.1.0 – 2026-05-10#
First public release. margo crosses from "in-progress Rust port of mango"
into "daily-driver Wayland compositor with full modern-protocol parity,
the dwm/dwl-style 9-tag workflow, 14-layout catalogue, niri-grade
animations, embedded scripting, an in-compositor screencast portal,
and HDR scaffolding." Every line in the workspace is original to this
project except for the deliberately-attributed portions of dwl, dwm,
sway, tinywl, and wlroots — see LICENSE.*.
Compositor#
- Tag-based workflow — nine multi-select tags per session,
view N/tag N, dwm-style press-twice-for-back, per-tag home monitor (tagrule = id:N, monitor_name:X) with automatic warp on view, per-tag layout / mfact / nmaster pinning viaPertag, per-tag wallpaper hint surfaced throughstate.jsonfor wallpaper daemons. - Layout catalogue —
tile,right_tile,monocle,grid,deck,center_tile,scroller,vertical_tile,vertical_grid,vertical_scroller,vertical_deck,tgmix,canvas,dwindle, plus a global overview mode. Each layout is a pure function ofArrangeCtx → Vec<(idx, Rect)>so every algorithm gets snapshot-tested against a committed text fixture. - Adaptive layout engine — per-tag
user_picked_layoutsticky bit + window-count / aspect-ratio heuristic; usersetlayoutpins the choice, heuristic never overrides. - Spatial canvas — PaperWM-style per-tag pan via
canvas_pan/canvas_resetactions, threaded into 5 layout algorithms. - Animations — niri-style analytical spring physics with
mid-flight retarget for window movement, carefully-tuned
bezier curves for open / close / tag / focus / layer
transitions. All five animation types support both clocks
via
animation_clock_*per-domain config. Snapshot-driven open / close so there's no first-frame "pop" before the transition starts. - Drop shadows + rounded corners — single-pass SDF GLES shader, no offscreen buffers; clipped-surface rounded-corner mask shared across windowed / fullscreen / animated paths.
- Modern protocol stack —
linux-dmabuf-v1+linux-drm-syncobj-v1(Firefox / Chromium / GTK / Qt avoid SHM fallback), DMA-BUF screencopy (zero-copy GPU→GPU full- output capture), region-based screencopy crop, runtimewlr-output-management-v1(mode + scale + position changes apply live, kanshi compatible),pointer_constraints_v1+relative_pointer_v1(FPS games / Blender),xdg_activation_v1with strict-by-default anti-focus-steal policy, VBlank-accuratepresentation-time,wp_color_management_v1(HDR Phase 1 protocol surface),ext_idle_notifier_v1+idle-inhibit,text-input-v3+input-method-v2,ext-session-lock-v1,wlr-gamma-control-v1,ext-foreign-toplevel-list-v1,wp_single_pixel_buffer_v1,ext-image-capture-source-v1+ext-image-copy-capture-v1. - Built-in xdg-desktop-portal-gnome backend — five Mutter
D-Bus interface shims (
org.gnome.Mutter.ScreenCast,.DisplayConfig,.Shell.Introspect,.Shell.Screenshot,.Mutter.ServiceChannel) + a PipeWire pipeline that lights up the Window / Entire Screen tabs in browser meeting clients (Helium, Chromium, Edge, Brave) without a running gnome-shell. Includes paced rendering, per-cast damage tracking, embedded cursor + metadata cursor sidecar, full-decoration casts (borders / shadows / popups / animations / block-out come through to the share view), HiDPI scale handling, and livewindows_changedupdates so xdp-gnome's window picker stays fresh mid-share-dialog. - Window rules with PCRE2 — regex match by
app_id/title/exclude_*, size constraints, floating geometry, per-rule animation overrides,block_out_from_screencast, scratchpad / named-scratchpad opt-in, CSD-allow whitelist. Lateapp_id/titlereapply so Qt clients don't flicker. - Scratchpad system — anonymous + named scratchpads,
cross-monitor support,
single_scratchpadmode, recovery viaunscratchpad_focusedandsuper+ctrl+Escapereset. - Embedded scripting — Rhai 1.24 sandboxed engine with
dispatch(action, args)plus state-introspection bindings (current_tag,focused_appid,monitor_count, …) and event hooks (on_focus_change,on_tag_switch,on_window_open,on_window_close) that fire from the compositor mainloop with a re-entrancy guard. Plugin packaging via~/.config/margo/plugins/<name>/{plugin.toml, init.rhai}discovers and loads multiple scripts; per-plugin errors don't take down the loader.mctl run <file>evaluates a script against the live engine for hot-edit workflows. - Hot reload —
mctl reload(and the bundledSuper+Ctrl+Rkeybind) re-applies window rules, key binds, monitor topology, animation curves, and gestures without a logout.mctl check-configis the offline validator — exit 1 on regex compile errors, unknown fields, duplicate binds, or include-resolution loops. - HDR scaffolding (Phases 1 + 2 + 3) —
wp_color_management_v1global advertising primaries / TFs / parametric creator (Phase 1, shipped); fp16 linear-light composite math + GLSL shaders + spec-value verification (Phase 2, gated on smithay's swapchain reformat API);HDR_OUTPUT_METADATAblob writer +EdidHdrBlockparser (Phase 3, gated on smithay'sset_hdr_output_metadata). Phase 4 (per-output ICC profiles) is queued. - dwl-ipc-v2 wire compat — drop-in for noctalia,
waybar-dwl, fnott, and any other dwl/mango widget. Rich
state.json sidecar exposes
scratchpad_visible,scratchpad_hidden, MRUfocus_history, per-tag wallpaper.
Companion tools#
mctl— IPC + dispatch CLI. Subcommands:status/clients/outputs/focused/watch(live JSON / table inspection),dispatch(40+ typed actions; mirrorsbind = …argument shape),actions [--names | --verbose](the dispatch catalogue),rules --appid X --title Y --verbose(offline rule introspection),check-config(offline validation),reload,run <file>(live Rhai eval),spawn,migrate --from {hyprland, sway} <file>(offline config translator). Stable JSON schema withversion: 1.mlayout— named monitor-topology profiles for laptops with frequent dock changes.mlayout suggest / list / set / save / edit. Wrapswlr-randragainst margo'swlr-output-management-v1handler so changes apply live without logout.mscreenshot— region / window / output capture. Wrapsgrim+slurp+wl-copy+ an optional editor (swappy/sattyif installed). Modes:rec,area,screen,window,open,dir. The in-compositor region selector (Print key default) replaces slurp's separate window with a dim-overlay + drag-rect UI on the margo render path itself.
Architecture#
- State management —
MargoStatelives inmargo/src/state.rs(~6,100 LOC after the W4.2 split, down from 7,651). 15 protocol-handler impls extracted intostate/handlers/files for incremental-compile wins (xdg_decoration,session_lock,xdg_activation,layer_shell,color_management,idle,pointer_constraints,input_method,selection,gamma_control,screencopy,dmabuf,output_management,x11,xdg_shell). - Workspace layout —
margo(compositor binary),margo-config(parser + types),margo-ipc(mctl + the dispatch action catalogue + Hyprland/Sway migrate),mlayout,mscreenshot. Pinned smithay revisionff5fa7df; Rust 1.85+. - Cargo features —
dbus(default; gates D-Bus + async-io),xdp-gnome-screencast(default; requiresdbus; gates pipewire),a11y(off by default; gates AccessKit),profile-with-tracy(off by default; flipstracy-clientto its full backend so a live Tracy GUI can connect). Three build configurations verified. - AccessKit a11y —
accesskit_unixadapter on a dedicated thread (zbus-on-mainloop deadlock avoidance), publishes the window list as accessible nodes. Orca and AT-SPI consumers can navigate margo's window state. - xwayland-satellite mode —
--xwayland-satellite[=BIN]spawns Supreeeme's xwayland-satellite as a separate process so X11 crashes can't take margo down.--no-xwaylanddisables X11 entirely. Default path stays in-tree (smithayXWayland::spawn). - Tracy profiler hooks — six hot-path spans
(
render_output,build_render_elements,arrange_monitor,tick_animations,handle_input,focus_surface) compile to no-ops in normal builds.
Test infrastructure#
- Layout snapshot suite — 20 committed
.snaptext fixtures locking the geometry of all 14 layouts × multiple scenarios. Insta-based; pure text diff at PR review time. - Layout property tests — 14 invariants verified across the full catalogue × {1, 2, 3, 5, 8} window counts × focus shift × gap-zero edge cases (cardinality, no-degenerate-rects, monocle / deck identity, tile-class disjointness, focus invariance for non-scroller layouts, scroller monotonic width growth, gap-zero work-area coverage, focus-centring invariant for every focused index).
- Integration test fixture — calloop-driven
Server+wayland-clientClient+Fixtureharness (port of niri'ssrc/tests/{fixture,server,client}.rs). All 15 W4.2-extracted protocol handlers have at least one integration test; 41 integration tests acrossxdg_shell,layer_shell,idle,xdg_decoration,session_lock,xdg_activation,pointer_constraints,gamma_control,screencopy,output_management,selection,globals, plus negative-invariant pinning fordmabuf/color_management/x11/xwm(gated on backend prerequisites that the headless harness can't drive). Total in-tree workspace test count: 126 (compositor: 102 layout + property + integration; config parser: 9; mctl + ipc + migrate: 15). - Smoke testing —
scripts/smoke-winit.sh(build → spawn → IPC → reload → focus → kill → empty-status, runs in CI under Xvfb),scripts/post-install-smoke.sh(binary presence, example config parses, dispatch catalogue ≥30 entries, completion paths, license install). - Clippy gating — workspace + all targets run under
-D warnings;clippy.tomldocuments the smithay-handle interior-mutability allowlist.
Documentation#
- Published site at https://kenanpelit.github.io/margo/
(mkdocs-material; deploy automated via
.github/workflows/docs.yml). Pages: Overview, Install (Arch / source / Nix flake), Configuration overview, Full configuration reference (the entire annotatedconfig.example.confrendered inline viapymdownx.snippets, syntax-highlighted), Companion tools, Scripting, Manual checklist, three design notes (HDR, Built-in portal, Scripting engine), Roadmap, Contributing. - Annotated example config — 1,028 lines at
margo/src/config.example.conf; every option documented inline. - CONTRIBUTING.md + PR template — quick-start build, code-layout map, lint posture, test workflow, conventional commit style, AI-contribution policy.
Compatibility#
- Display managers — ships
margo.desktop(direct session) andmargo-uwsm.desktop(UWSM-driven for systemd graphical-session.target plumbing). - Existing widgets / bars — drop-in for noctalia, waybar-dwl, fnott via dwl-ipc-v2.
- Migration —
mctl migrate --from {hyprland, sway}translates the high-value config subset (keybinds, spawn lines, workspace → tag bitmask conversions, modifier names, key aliases). Window rules / animations / monitor topology stay manual to avoid inventing wrong semantics.
Packaging#
- Arch / makepkg — PKGBUILD at the repo root installs
margo,mctl,mlayout,mscreenshot, the wayland- session entries, the example layouts, the XDG portal preference at/usr/share/xdg-desktop-portal/, shell completions for bash / zsh / fish, and license headers for the dwl/dwm/sway/tinywl/wlroots inheritance chain. - Nix flake —
flake.nixexposespackages.default,devShells.defaultwithrust-analyzer+clippy, plusnixosModules.margoandhmModules.margo. - GitHub Actions — three workflows:
ci.yml(build/test/clippy/check-config on every PR),smoke.yml(end-to-end nested-mode smoke under Xvfb),docs.yml(Pages deployment).