Skip to content

Changelog#

All notable changes to margo are documented here.

The format follows Keep a Changelog, and the project adheres to Semantic Versioning.

[4.5.0] – 2026-10-28#

mdots shows what an update would bring in before it installs anything, and Flatpaks can come from a remote other than flathub.

Added#

  • mdots update --check lists the available pacman, AUR and Flatpak updates (with the installed version beside the new one) and then asks Continue Update? [Y/n]. It only reads: checkupdates when pacman-contrib is installed (else the cached databases), the AUR helper's -Qua, and flatpak remote-ls --updates. With --json it prints the report (package_manager, flatpak, total_updates) and updates nothing, which suits a status-bar indicator; without a terminal it never continues. A source that can't be checked shows as a warning instead of failing the check.
  • flatpak_remote in the host file picks the remote Flatpaks install from, and a package can set its own: { name: org.gnome.Builder, type: flatpak, remote: gnome-nightly }. Unset, it stays flathub; an imported config never changes it.

Changed#

  • Every mdots sync path now installs Flatpaks from that remote; the plan-based sync used to pass none at all.
  • The mdots shell completions are regenerated (they now also cover mdots diff and mdots doctor).

[4.4.0] – 2026-10-27#

Ports from mango 0.18.1–0.18.3: custom monitor modes that actually apply, quoted config values, no more leaked shortcut keys, blur on translucent fullscreen windows, scratchpads that stay scratchpads when an app activates them, and a window title in the switcher.

Added#

  • Ports from mango 0.18.1–0.18.3:
  • monitorrule … custom:1 now works: margo generates the exact width × height @ refresh mode (CVT) even if the panel doesn't advertise it, and falls back to an advertised mode if the driver refuses it.
  • The window switcher shows the selected window's title and app id under the thumbnails (with mru_show_labels).
  • Opening the overview zooms the cards in from slightly larger; the focused window still slides into place.
  • mctl check-config warns (W007) about monitorrule fields that only work together: width without height, refresh without a size, custom:1 without a full mode, x without y.

Changed#

  • drag_tile_refresh_interval / drag_floating_refresh_interval default to 0, which means one update per frame at the monitor's refresh rate. An explicit value still overrides it.

Fixed#

  • Config values in quotes (focuscolor = "0x…", default_layout = 'tile') are read without the quotes.
  • The key of a keybinding no longer leaks its release to the focused app (e.g. Alt+Space's Space release reaching Chrome).
  • Translucent fullscreen windows (kitty with a transparent background, wp_alpha_modifier) keep their blur; opaque ones still skip it.
  • Activating a hidden scratchpad (a notification click, the dock, focuswindow) shows it as a scratchpad instead of focusing an unmapped window. Activating a hidden tab-group member switches the group to it.
  • scratchpad_width_ratio / scratchpad_height_ratio apply whenever no window rule gives the scratchpad a size, even if the window had already been floating.
  • A window rule's floating size is no longer shrunk or grown to the app's own min/max size hints; only the rule's min_* / max_* bound it.
  • A layer surface that opens or closes under a still pointer gets pointer enter/leave right away (slurp's crosshair showed only after moving the mouse).
  • In the overview, floating and overlay windows no longer fly over the other cards while sliding into the grid.
  • A close animation could briefly show on another output after a monitor was unplugged.

[4.3.0] – 2026-10-25#

A new İznik colour scheme, and the catalogue trimmed to the eleven best dark schemes.

Added#

  • İznik colour scheme, after İznik tiles: night-ink navy surfaces, cobalt primary, tile-turquoise secondary, coral-red tertiary (also the floating-window border) and warm ivory text. mshellctl theme set iznik.

Removed#

  • Colour-scheme catalogue trimmed to eleven: Kenp, Kanagawa Wave, Tokyo Night, İznik, Rosé Pine, Everforest Dark, One Dark, Catppuccin Mocha, Gruvbox Material, Horizon and Margo (plus Wallpaper and Default). The old "One Margo" is now simply Margo (a stored OneMargo still loads); the brighter original Margo, Dracula, Ayu Dark, Flexoki, GitHub Dark, Gruvbox Dark Medium, Monokai Classic, Nord Dark, Oxocarbon, Solarized Dark and Vesper are gone; a profile still pinned to one moves to Kenp on load (config v4).

[4.2.1] – 2026-10-25#

Ctrl+Alt+F1…F12 switches to a TTY again, the login screen's power keys move to Ctrl+F1/F2/F3 so a stray F-key can't shut the machine down, and clipboard history keeps copied text instead of an app's private blob.

Fixed#

  • Ctrl+Alt+F1…F12 never switched VT, on the desktop or the graphical login screen: margo-config injected the chvt binds but the action was never implemented. It now asks logind/seatd for the switch.
  • Login screen power keys are now Ctrl+F1/F2/F3 (Shutdown/Reboot/Suspend) instead of bare F1–F3, and modifiers match exactly in both mgreet and the TUI, so a stray F-key or the VT-switch chord can't power the machine off. Keys accept chords (Ctrl+F1) in power_controls.
  • Clipboard history stored some copied text as an unreadable application/x-dtsz blob: the type picker only matched exact lowercase text types and otherwise took the app's first (private) format. Matching is now case-insensitive and any text/plain spelling, then any text/* or image/*, wins over opaque formats.

[4.2.0] – 2026-10-21#

Floating windows now get their own border colour when focused, so a float sitting over the tiled stack is easy to spot.

Added#

  • floatingcolor: border colour of the focused floating window, so a float over the tiled stack stands out. Unset keeps focuscolor; the matugen colors.conf fills it from the palette's tertiary role.

[4.1.0] – 2026-10-15#

Settings, reorganised the KDE System Settings way: clearer categories, page headers that match the sidebar, and on every page "what did I change?", a Defaults button and search that lands on the matching row.

Added#

  • Changed settings at a glance. Each Settings page header shows "N changed"; its popover lists every setting that differs from its default, with the current and default value. A new header toggle, Highlight changed settings, marks those rows with an accent bar (on by default, remembered).
  • Defaults per page. Resets the page's settings after an inline confirmation, then rebuilds the page so every control shows the reset value. Shell settings return to their built-in defaults. Compositor keys are commented out in config.conf (kept, never deleted, so the old value stays visible) and margo reloads. Lists you built yourself are never reset: bar widgets, autostart scripts, calendar subscriptions, notification blocklist, saved weather locations, window/layer/tag rules and per-tag layouts.
  • Search lands on the setting. Opening a search result scrolls to the first matching row (or section) and flashes it.
  • Pages that were missing from search by name (VPN, AI, Translate, Calendar, Date & Time, Overview) are found now. Every sidebar label is searchable.

Changed#

  • Sidebar categories follow KDE System Settings: General · Appearance · Workspace · Window Management · Input · Hardware · Network · Applications · System · Help (Applications and System start collapsed). Clearer names: Window Style (was Appearance), Input Devices, Keyboard Shortcuts, On-screen Displays, Tags, Startup. Every row and group has its own icon. Groups you had collapsed may open expanded once because their names changed.
  • Page headers match the sidebar row that opens them (title and icon), enforced when the page is built so they can't drift again.
  • Launcher settings rows use the same row style as every other page.

Developer notes#

  • mshell-settings/build.rs generates which settings each page owns from the page sources (update_config field paths, reactive_settings! bindings, margo option keys). Rows opt into highlighting with set_widget_name: "setting:<key>", and a test validates every tag. See docs/config-conventions.md §9.

[4.0.0] – 2026-10-14#

Graceful reboot / shutdown, forty config keys that finally do what they say, a deep code-quality and performance pass, and smithay master HEAD. Major bump because a few existing setups behave differently — see "Changed" first.

Changed#

  • focusdir is spatial (mango's focus_direction): focusdir,left focuses the nearest window to the left instead of cycling the stack. At the edge, focus_cross_tag = 1 jumps to the nearest occupied tag, else focus_cross_monitor = 1 to the adjacent monitor; with both off nothing happens. A numeric argument (focusdir,1) and focusstack keep cycling. Direction binds and gestures bound to focusdir behave differently.
  • Center-tile defaults follow mango: with no stack windows the master keeps its mfact width, centred (center_master_overspread = 0); with one stack window the master stays centred (center_when_single_stack = 1). Configs that don't set them see a different center-tile layout.
  • Virtual keyboards are real input devices (smithay HEAD): keys from wtype, wayvnc, mkeys or an IME now pass margo's keyboard path and can trigger bindings — except over the lock screen, and never quit, force_unlock or chvt.
  • Power menu: with the command fields empty, mshell's Reboot / Shutdown / Logout use margo's new graceful session_end.
  • The built-in defaults of new_is_master and enable_hotarea are 0; neither ever worked, so this keeps everyone's behaviour as it was.
  • Settings: the no-op "Tab mode" (Overview) and "Optimized blur" (Effects) switches are gone.
  • smithay bumped to master HEAD (8a270232); winit 0.31.0-beta.3, smithay-client-toolkit 0.21.1.

Added#

  • session_end reboot|poweroff|logout|cancel — every window (all tags and monitors, X11 included) is asked to close normally, so browsers exit cleanly instead of reporting a crash on the next start; the action runs once the last window is gone. Windows still open after session_end_grace_ms (5000) get a notification naming them with a Cancel button, and the action runs after session_end_countdown_ms (15000) or as soon as they close.
  • Config keys that were parsed but ignored now work (mango semantics): warpcursor, cursor_hide_timeout, enable_floating_snap / snap_distance, drag_corner / drag_warp_cursor, no_border_when_single, no_radius_when_single, scratchpadcolor, animation_fade_in / animation_fade_out / fadein_begin_opacity / fadeout_begin_opacity, animation_clock_layer, scroller_ignore_proportion_single (+ the per-window scroller_proportion_single), center_master_overspread, center_when_single_stack, new_is_master, exchange_cross_monitor, focus_cross_monitor, focus_cross_tag, focus_on_activate, allow_shortcuts_inhibit (+ window rule), numlockon, capslock, button_map, axis_scroll_factor, trackpad_scroll_factor, axisbind / axis_bind_apply_timeout, switchbind, wallpaper_fit, allow_lock_transparent, syncobj_enable (opt-in explicit sync), drag_tile_refresh_interval / drag_floating_refresh_interval; enable_hotarea / hotarea_corner / hotarea_size and ov_tab_mode as aliases onto hot corners and overview_style.
  • mctl check-config warns W006 for the keys that are still accepted but have no effect (allow_tearing, touch-gesture and tablet keys); a test keeps that list honest.
  • Ports from mango 0.18.0: auto_reload_config (reload on save, default on), var=name,value config variables, the confine_pointer window rule, the smartkillclient action and disable_middle_paste.
  • Translate: speak the result aloud with Microsoft neural voices, voice picker in Settings → Translate.

Fixed#

  • Keyboard LEDs (Caps / Num / Scroll Lock) never followed the lock state.
  • Config auto-reload stopped after the first save from editors that save by rename, and re-triggered itself on its own reads.
  • Returning to a tag focused its oldest window instead of the last one.
  • focuslast jumped to the wrong window after windows were reordered.
  • A single-window screencast captured anything overlapping the window (notifications, floating windows).
  • A cursor parked in a hot corner never fired it.
  • Absolute pointer motion (tablets in mouse mode, VMs) only reached the first monitor and skipped pointer constraints.
  • Malformed numeric config values silently became 0 with no warning.
  • The process environment was modified while other threads ran (XWayland start-up), a rare crash risk.
  • The focus border crossfade jumped to the real colour at the end for urgent / overlay / fullscreen / scratchpad windows; the corner radius was computed in six places that disagreed (no_radius windows got a rounded border).
  • Screen captures lacked the compositor wallpaper; changed shadow colours weren't repainted after a reload.
  • mvpn: device-slot recycling keyed on hostname collided between machines, and is now strictly limited to devices it recorded itself.
  • Translate speech: a rustls panic and a misreported "clock skew".

Performance#

  • Move/resize drags are throttled by the drag refresh intervals instead of re-arranging on every pointer event (1000 Hz mice).
  • Animation ticks only re-map what moved; arrange's global steps run once per batch, not per monitor; one hit-test per pointer motion; the commit handler resolves its client and layer once.
  • Screencopy builds its elements once per frame and reports real damage to copy_with_damage clients; window casts build only the window.
  • Group-tab chips and shadows keep stable element ids; config reloads parse once and skip unchanged auto-reloads.

Tests#

  • Regression tests for the fixed focus, hot-corner, config-watcher, session-end and layout paths; clippy::undocumented_unsafe_blocks is enforced and every unsafe block is documented; panic ratchet 448 → 446.

[3.8.0] – 2026-10-10#

A native translate feature — select/copy text, press a keybind, get the translation — and native tmux gesture support. No scripts, no WASM plugin.

Added#

  • Native translate (mshell-translate): select or copy text and press a keybind to get the translation on your clipboard plus a toast — Google Translate by default (no key, no account needed), with an optional DeepL upgrade using your own key. Auto-detects the source language and auto-flips direction (configure e.g. tr↔en and it translates whichever way the text isn't already in); single-word lookups also surface Google's dictionary alternate meanings, grouped by part of speech. A Translate bar pill + menu panel cover manual lookups (mshellctl menu translate, default super+alt,t), alongside mshellctl translate selection|clipboard|text for the headless capture flow (default super+ctrl,t). Settings → Translate configures the provider/languages/DeepL key; Settings → Widgets covers the pill and menu panel's position and size.
  • Three-finger swipe tab-switching gestures (already wired for browsers) now also drive tmux window switching when the focused client's app-id matches — real tmux next-window/tmux previous-window dispatch from margo's own sendkey, not an external script.

[3.7.0] – 2026-10-08#

Screenshot repeat-region capture, a keyboard-focus fix for toggle-only menus, and two niri ports.

Added#

  • mshellctl screenshot repeat (default keybind: ctrl+shift,Print) re-grabs the last region committed through the area selector without reopening it — handy for rapid repeat captures of the same spot. Falls back to a normal region select the first time it's used in a session.
  • Clicking a window while an mshell-frame menu/pill is open now closes the menu first (GNOME quick-settings style), so typing reaches the window immediately instead of requiring Esc first.
  • windowrule = focus_on_xdg_activate:0 (ported from niri) denies focus-steal from xdg-activation requests on a window — useful for Picture-in-Picture-style windows that re-activate themselves — and marks it urgent instead. is_urgent is now actually wired end-to-end: it previously drove the urgent border color and the dock/pill state mirror, but nothing in margo ever set it.

Fixed#

  • A wlr-screencopy client supplying a valid SHM buffer from a larger shared pool (several buffers packed into one pool) was spuriously rejected (ported from niri).

Docs#

  • README: fixed stale mshellctl command examples, a missing mtm entry, and mvisual's wrong description.

[3.6.0] – 2026-09-30#

A cosmic-osk visual/feature port for the on-screen keyboard, five more mango 0.17.3 ports, and a whole-project security and robustness audit — ~35 findings fixed across the WASM plugin sandbox, IPC, secrets handling, atomic file writes, GTK main-thread blocking, and resource leaks.

Added#

  • mkeys (on-screen keyboard) cosmic-osk parity: key legends now derive from margo's real active xkb layout instead of a hardcoded keymap; floating mode with drag-to-move; function-row and numpad toggles; a toolbar (dock/undock/close); and a full visual redesign — near-transparent panel, neutral-dark keys — matching cosmic-osk's actual look rather than a guess at it.
  • Mango 0.17.3 ports: a compositor dim overlay (dim_enable/dim_focused_color/dim_unfocused_color); tag_gather wraps viewtoleft/viewtoright at the last occupied tag + 1; ~/ expands in margo-config env values; a new get layers IPC topic lists layer-shell surfaces; an explicit env = XCURSOR_SIZE now wins over cursor_size for XWayland; keybinds fall back to the layout-independent latin keysym when the active layout doesn't have the bound key.
  • A plugin's granted WASM host capabilities (process / network / clipboard) are now shown in Settings → Plugins — previously enforced but completely invisible in the UI.
  • mdots now shares the workspace version instead of tracking its own — it releases alongside margo/mshell going forward rather than on a separate, easy-to-forget cadence.

Fixed#

  • global_shortcuts_bind was silently broken over the real IPC protocol — every shortcut an app registered through the xdg-desktop-portal GlobalShortcuts backend was landing in the wrong argument slot and getting dropped before it ever reached the registry, so portal-registered global shortcuts never fired at all.
  • The MRU switcher (Alt+Tab-style window cycling) rendered its strip from a flat .take(mru_max) while the selection index kept advancing over the full candidate list — anyone with more open windows than mru_max (default 20) saw the wrong thumbnail ringed as "selected," or none at all, once they cycled past it.
  • Tray icons: Vocalinux and other apps rendering as a missing-icon glyph, and Electron tray icons (Ferdium, …) going stale after their first icon change — wayle-systray is now vendored with a fix that re-reads the icon theme path on every icon-change signal.
  • A floating window could get permanently stuck at an invisible 1×1 size: movewin/resizewin/togglefloating only seeded the remembered floating geometry from the tiled size the first time a window floated, so a window shrunk once and re-tiled silently snapped back to that sliver the next time it floated again.
  • text-input-v3 enter/leave events weren't firing on focus change — keyboard focus routing is now centralized through one set_keyboard_focus method instead of being set ad hoc at each call site.

Security#

  • Plugin (WASM) sandbox hardening: the blocking run/http host calls had no timeout and could wedge the whole shell indefinitely on a hung process or server; there was no wasmtime memory cap, so a buggy or malicious plugin could grow memory unbounded and OOM the entire shell process; http's response body had no size cap either; notify() had no capability check at all, letting any plugin spam or spoof desktop notifications regardless of its granted capabilities.
  • Path traversal: a plugin registry's id/dir fields (fetched from a remote, potentially compromised source) and a Settings-page profile name both flowed unvalidated into filesystem paths — either could escape the intended directory on install, or make a later delete remove an arbitrary file.
  • percent_decode (IPC) panicked on a multi-byte UTF-8 character right after a stray % — fed straight off the IPC socket from any local process, so this could crash the whole compositor with one crafted string. No cap on simultaneous IPC connections either.
  • The AI provider API key and the lockscreen password are now zeroized in memory on drop instead of sitting as plain, unscrubbed Strings.
  • Non-atomic config writes: importing a config bundle (config.conf/binds.conf) and mdots' internal state files were written in place — a crash or disk-full mid-write could leave the live compositor config truncated. Both now go through a temp-file
  • rename. mctl migrate --output no longer silently overwrites an existing file without a backup.
  • A notification's sound-file hint had no size or type check before being handed to fs::read — any local app could point it at a huge or special file. Capped and type-checked.
  • Resource leaks: the audio-visualizer's cava process could outlive its widget; mvpn's desktop-notification helper left zombies; the wallpaper-thumbnail texture cache and the app-icon index cache grew without bound over a long session. mvpn's auto-switch timer no longer trusts a bare PID file without first verifying it's still the process it thinks it is before signalling it.
  • GTK main-thread blocking removed or bounded: the launcher's tmux search (previously a subprocess spawn on every keystroke) and clipboard-copy path (4 providers) now run off-thread; Settings → Login and the setup wizard's hardware probe cap their worst-case wait instead of blocking indefinitely on mlogind/nmcli.
  • panic-ratchet baseline lowered 450 → 448 — two .unwrap() chains on a dotfile's filename (which could panic the whole sync on a non-UTF8 name) now degrade gracefully instead.

[3.5.0] – 2026-09-15#

Five features ported from mango 0.17, a real floating-window bug fix, and a code-review batch: IPC honesty, a complete action catalogue, and two security-relevant fixes.

Added#

  • move_client — move the focused window toward a direction, displacing whatever neighbour is there, instead of swapping with it (exchange_client already covers the swap). Works uniformly across every layout, dwindle included, since they're all pure functions of client order.
  • windowrule-once — a window rule that applies to the first client that ever matches it, then is consumed for the rest of the session (mctl reload resets it).
  • Real idle-inhibit heuristics. idleinhibit_ignore_visible and the window-rule idle_inhibit_when_focus now actually do something — both were parsed and exposed in Settings but never consumed by the compositor. New idle_inhibit_when_fullscreen keeps the screen awake for fullscreen apps that never request idle-inhibit themselves.
  • hot_corner_disable_on_fullscreen (default on) — a hot corner no longer triggers while the focused window is fullscreen.
  • open_grid_overview / close_grid_overview dispatch actions — one-shot enter/leave for a press-to-open / Esc-to-close bind pairing, alongside the existing toggle_grid_overview.
  • focuslast dispatch action (dwl parity) — toggle focus back to the previously-focused window on the current monitor.
  • A complete, single-source dispatch-action catalogue (margo-config::actions) — ~30 previously-uncatalogued actions (dpms, mru_next/mru_prev, the twilight_* verbs, focuswindow and friends, the grid/scroller overview navigation verbs, and more) are now in mctl actions, the shell's layout menu, and the new config-validator check below. A test locks the catalogue and the dispatcher's match arms in sync in both directions.
  • Config validator W005: an unknown action in a bind = line is now flagged, with a "did you mean" suggestion.

Fixed#

  • A floating window could get permanently stuck at an invisible 1×1-pixel size. movewin / resizewin / togglefloating always floated the focused window, but only seeded its remembered floating geometry from the current tiled size the first time — a window once shrunk to a sliver by resizewin, then tiled again (looking normal), silently snapped back to that sliver the next time any of the three actions floated it again.
  • mctl dispatch no longer reports success for an unrecognised action. A typo'd action name (mctl dispatch focuslsat) used to get back {"ok":true}; it now reports an error, so scripts notice.
  • IPC dispatch argument parsing now matches config-file bind parsing exactly, instead of a second, subtly different implementation that could disagree with what a bind line would do.
  • margo-portal screencast: a Start request that fails partway through (timeout, race, error) no longer leaks the compositor-side capture session.

Security#

  • mshell plugin sandbox: a symlink inside a plugin's data directory could be followed to read or write files outside that directory. Path resolution now re-checks the canonical form of the parent chain rather than trusting a single resolve-and-check.

[3.4.1] – 2026-09-09#

Tiling gap fixes — layouts stop collapsing around Discord-shaped windows, and the deck layout finally shows the card you focused.

Fixed#

  • A tiled window no longer collapses the layout to make room for its own minimum size. An app that declares a large xdg_toplevel minimum (Discord and other Electron apps ask for ~940×500) was grown past its slot, and a redistribute-and-resolve pass then shrank its neighbours and spent every inter-window and outer gap to fit it — so center_tile, tgmix and dwindle snapped to edge-to-edge the instant such a window landed on the tag. A tiled window now gets exactly the slot its layout computed, the same as every other tiling WM; the client reflows its own content. A window that genuinely needs a minimum size can float (floating windows still honour min/max).
  • center_tile: vertically stacked windows now have the inner gap (gappiv) between them. The master, left-stack and right-stack columns divided their height with no gap, so stacked tiles touched edge to edge.
  • tgmix: the seam between the master column and the stack grid is one gap wide, not two. The stack half was inset by the outer gap once by tgmix and again by the grid it delegates to, so the master/stack seam came out gappoh + gappih wide.
  • deck: the focused stack card is raised to the front. Every deck card shares one rect, so the card you see is whichever sits on top of the z-order — and nothing raised the focused one, so cycling the deck moved the border highlight but not what was visible. The focused window is now raised on every focus change (matching dwl).
  • deck: stacked masters (nmaster >= 2) now have the inner gap between them — same missing-gappiv bug as center_tile.

[3.4.0] – 2026-09-08#

A live Guide page in Settings, and four real bugfixes.

Added#

  • Settings → Guide page. A new, browsable/searchable reference page under Settings, with four tabs — every one sourced live from an existing single-source-of-truth rather than hand-copied text:
  • Actions — every compositor dispatch action (mctl actions), grouped, searchable, click-to-expand for args/detail.
  • Settings — every config.conf key, parsed live from config.example.conf, grouped by its own section headers, searchable.
  • Features — a curated tour sliced straight from docs/features.md's Compositor / Desktop-shell sections.
  • Tools — live --help output for 14 companion binaries (mctl, mshellctl, mlock, mlogind, mvpn, mtune, …), run off-thread with a timeout.

Fixed#

  • mlock: running --help (or any flag) no longer locks your screen. It never parsed arguments at all — anything on the command line fell straight through to the real lock flow. This is what let the new Guide page's Tools tab (which probes every tool with --help) trigger a real, unattended lock.
  • margo no longer crashes if a session-lock client dies while the screen is locked (e.g. mlock killed mid-lock, as above). The screen correctly stays locked either way — margo just stops touching the dead client's now-invalid resources every frame instead of crashing on them.
  • A floating window's shadow now follows its actual size, not just its window-rule slot. mtune switching between its full / mini / strip skins left the shadow pinned to the full-size slot around visibly smaller content; it now shrinks together with the border, which already did this correctly.
  • A floating window's shadow no longer pops in ahead of its content when it opens. The border already hid itself for the duration of the open animation; the shadow now does too, so a window's frame and its content appear together instead of the open looking like two separate steps.

[3.3.1] – 2026-09-07#

Two real bugfixes, and doc/site cleanup.

Fixed#

  • mosaic layout: the whole stack now centers vertically too, not just horizontally. Rows were stacked starting from the work area's top edge, so a mosaic that didn't fill the full height — a single window, or a couple of short rows on a tall monitor — dumped 100% of the leftover space below it instead of splitting it top and bottom. GNOME's Mosaic article is explicit that a new window "opens centered on the screen" — that's both axes, not just the per-row horizontal centering it already had.
  • mtune: "Restore Playlist" now actually resumes where you left off. The empty-state button restored the cached queue's song list but never looked at where you were in it — it bypassed the same resume mechanism [playback] on_start = "resume" already uses correctly on every normal startup (resume-uri / resume-position), so it always silently restarted from track 1. Now it reuses that exact mechanism.

Docs#

  • config-reference.md (rendered from margo/src/config.example.conf) gained five real, previously-undocumented features: the MRU window switcher (mru_next/mru_prev, niri-style Super+Tab) and its 9 tuning keys, focus_window_or_workspace, togglefullscreen_exclusive, movegroupwindow, and dpms.
  • README, the site, and the wiki now say 12 layouts (mosaic included) instead of a stale "15" left over from before some were consolidated, and mention mosaic explicitly. The wiki's Companion binaries table also had mplay where mtune belongs — fixed.

[3.3.0] – 2026-09-07#

A GNOME-style Mosaic layout.

Added#

  • Mosaic layout. GNOME's "Mosaic" concept — a middle ground between tiling and floating — landed in three phases: windows auto-arrange into a content-aware grid as you open/close them, dragging one window onto another swaps their spot in the packing order (drag_tile_to_tile), and a window that doesn't fit even at its own minimum size automatically moves to the first empty tag (mosaic_auto_overflow_tag) — or, if you'd rather it stay put, shrinks to a small always-visible "peek" in the corner instead (mosaic_overflow_stack). Select it like any other layout (setlayout mosaic, or pin it per-tag with taglayout).
  • Masonry-style packing: each window joins whichever already-open row has the most room while still fitting it, instead of only ever considering the row currently being filled — inspired by tilingshell's MasonryLayoutManager.
  • Drop-target highlight: dragging a tiled/Mosaic window onto another now shows a translucent highlight (in the theme's accent colour) over the current valid swap target, so it's no longer a guessing game where exactly to drop — inspired by tilingshell's tilePreview/snapAssist.
  • Placement is now a pure function of the work area and each window's own min/max size hints — it no longer depends on whatever layout (tile, scroller, ...) happened to be active on the tag right before switching to Mosaic.

  • One Margo theme. A variant of the Margo palette with One Dark's text colour (#ABB2BF instead of #CDD6F4) — backgrounds, surfaces, and accents unchanged.

  • Tune bar pill — transport controls + queue progress. Visible prev/play-pause/next buttons, a playback-position line, and a compact queue-remaining badge, all individually toggleable from a new Settings → Widgets → Tune page. The repeat badge now shows the live "2/3" iteration count instead of a static ×N (new RepeatPlays D-Bus property, surfaced in mtune's own window too).

Changed#

  • Settings sidebar visual refresh: the selected row now reads as a --surface-container-high tile with a --primary inset-shadow leading bar instead of a solid --primary fill; section headers become uppercase eyebrow labels in --primary; every row gets a unique icon; a couple of groups (Advanced, Locale & Accounts) start collapsed.

Fixed#

  • Three real Mosaic usability bugs found through actual use: a resized Mosaic window could render underneath an overlapping neighbour (z-order didn't account for an active interactive grab); the drag-to-swap target could silently fail depending on window creation order (it was hit-testing through the dragged thumbnail itself); many windows on scroller before switching to Mosaic packed into a single wasteful column with some pushed off-screen.
  • Super+Tab (MRU switcher) on scroller: cycling looked like it worked, but releasing the modifier could commit focus to a window without ever scrolling it into view. activate_window_idx now re-arranges after moving focus, same as every other focus-changing dispatch already did.

[3.2.0] – 2026-09-05#

mtune gets embedded lyrics, repeat-each, and playlist resume.

Added#

  • mtune — embedded lyrics. mtune reads synced/unsynced lyrics straight from the audio file's own tags and publishes them on org.margo.Tune (EmbeddedLyrics). The shell's Lyrics pill/menu prefer the file's own lyrics over an lrclib lookup whenever the active player is mtune.

  • mtune — "repeat each" mode. A fourth repeat mode alongside off / all / one: replay the current track a set number of times before moving on. The count is persisted in mtune.toml, adjustable from a [-] N [+] stepper in both the player window and the shell's Tune menu, and shown as a ×N badge on the bar pill. New RepeatCount on org.margo.Tune (mirrored into mshell), plus mshellctl mtune repeat each / repeat-count [N].

  • mtune — playlists resume where you left off. A saved playlist records the last-played track index in a #TUNE-RESUME: comment line (kept up to date as you listen) and starts there next load. Standard m3u readers ignore the line.

  • Tune menu — browsable queue. A §12 panel header with larger cover art and a scrollable, filterable list of the whole queue (new QueueEntries D-Bus property; rows removable in place).

Changed#

  • The Lyrics menu's Maximum Height setting now actually caps the menu.
  • smithay bumped to master HEAD (28c8e0fa) — an InputTime timestamp-wrapper migration across margo's input path — and wayland-backend 0.3.15 → 0.3.17, which carries a fix to a wl_client_destroy race that could abort the compositor during rapid client teardown.
  • install.sh now builds mtune on the Debian / Ubuntu path.

Notes#

  • The compositor binary changed (smithay / wayland-backend): rebuild and re-login to pick it up.

[3.1.0] – 2026-09-04#

A native music player, and a floating layout.

Added#

  • mtune — a folder-first music player. A de-branded fork of Amberol (org.margo.Tune), vendored into the workspace and re-themed to the margo look with a live matugen palette.
  • Folder library, not a database: point ~/.config/margo/mtune.toml [library] roots (or Settings → Tune) at one or more folders; recursive off-thread scan, an on-disk tag index with mtime invalidation, and a debounced inotify watcher keep the queue live.
  • Interop — MPRIS + MPRIS TrackList, a StatusNotifierItem tray, and a supplementary org.margo.Tune D-Bus interface for the library / queue.
  • Dedicated shell surface — a Tune bar pill (queue position + elapsed / total) and a menu with a draggable seek bar, transport, shuffle / repeat, a speed control, and the folder / playlist actions (not the generic MPRIS pill). Follows DESIGN.md.
  • mshellctl mtune — a full CLI (play-pause, next, seek, rate, repeat, jump, open, playlist-*, status --json, toggle-window, …) talking straight to mtune's D-Bus.
  • Playback speed with pitch held constant (scaletempo), playlists (m3u / m3u8 / pls, open + save), resume the last track + position on launch, compact mini / strip window skins (Ctrl+M), desktop notifications on track / setting change, start-hidden in the tray (mtune --hidden / [behaviour] start_hidden / Settings toggle), and track numbers in the queue list.
  • Optional runtime deps: gst-libav / gst-plugins-bad for AAC / M4A / ALAC / WMA and extra containers.
  • Floating layout. A LayoutId::Floating alongside the tiling layouts — a classic stacking desktop. setlayout floating, taglayout = N, floating, default_layout = floating, or pick it in Settings → Tiling Layout. On a floating tag the compositor auto-floats every window with a cascade and re-tiles them when the tag switches back to a tiling layout (a per-client floated_by_layout flag keeps hand-floated windows untouched). Not part of the cyclelayout rotation — explicit selection only. Closes #1.

Fixed#

  • mtune: an explicit "next" now advances past a track even under repeat-one (LoopStatus=Track) instead of silently replaying it; the queue-row track number no longer overflow-aborts on an unbound list item; the pitch-shift on a non-1.0 playback speed is gone.

[3.0.0] – 2026-09-02#

mplay folds into mshellctl; automatic config.conf history.

Breaking#

  • The standalone mplay binary is gone. Its entire surface — window control, playback, yt-dlp, media picking, the video-wallpaper engine — is now native mshellctl. Scripts / keybinds invoking mplay directly switch to the equivalent mshellctl play / media command; man/mplay.1 is gone too.

Added#

  • mctl config — automatic history for config.conf. Every time the config takes effect (boot or a successful mctl reload) a timestamped copy is saved ($XDG_STATE_HOME/margo/config-generations/, last 20 kept). mctl config list / diff [N] / rollback [N] [--yes] — a candidate generation is validated before it is written and a backup is kept; --config <path> is honoured throughout.
  • mshellctl gains a full native mpv companion, ported from mplay: window control, play <target> / download (clipboard / URL, YouTube via yt-dlp), and a native video-wallpaper engine (play wallpaper start/stop). mshellctl's IPC now goes straight to margo's Unix socket.
  • mshellctl media reaches native MPD parity — a unified MPRIS + MPD player-scoring path, and media toggle spotify falls back to the browser player.
  • mango 0.16.1 port — view_insert dispatch action, a tag_gather config knob, and an invalid xkb_layout no longer refuses to boot.
  • Valent (KDE Connect) panel — share text / clipboard, Accept / Reject a pairing request, per-device alias + avatar, a settings popover.

Fixed#

  • Boot-time config parse failures were silent — margo now tries the last known-good generation first and always shows the on-screen banner; mctl config-errors reports what happened.
  • The app-launcher window switcher focuses windows by exact client id instead of jumping to a tag.

[1.2.0] – 2026-07-15  ·  [2.0.0] – 2026-08-11  ·  [2.1.0] – 2026-08-14#

Release notes for these tags live on the GitHub Releases page.

[1.1.9] – 2026-07-13#

Presentation pacing done right, and repaints that stay on their own screen. wp_fifo_v1 + wp_commit_timing_v1 are advertised again — this time with the barrier-release scheduler they always needed — and the whole damage path was rebuilt to be per-output: a commit, animation frame, cursor move or focus change now repaints exactly the displays it touches. The visible payoff: a Chromium/Brave video tab no longer freezes for seconds after a hidden-tag round trip, and idle outputs actually idle.

Drop-in upgrade.

Added#

  • P15 — wp_fifo_v1 + wp_commit_timing_v1, driven for real. The globals were briefly withdrawn because Smithay's managed implementations install commit barriers margo never released, wedging any client that used them from a hidden tag. margo/src/state/pacing.rs is the missing scheduler: FIFO barriers signal at every per-output present (real vblank, empty present, estimated vblank), hidden/locked/disabled surfaces release from the frame-callback fallback tick, and commit-timing deadlines are honoured up to the next refresh — plus an exact one-shot deadline wake armed from the surface pre-commit hook, so a timed commit on an otherwise idle output releases at its target time, not at the next unrelated frame. Mesa's Vulkan WSI gets a real VK_PRESENT_MODE_FIFO_KHR on margo; video players get compositor-driven frame pacing. The advertised protocol surface is back at 57. Regression-tested end to end in margo/src/tests/pacing.rs, including the hidden-tag stall and the idle-output deadline wake.
  • Callback-only liveness fallback. A permanent 1 s timer (30 Hz during a freshly mapped hidden window's warm-up) delivers wl_surface.frame to frame-throttled trees on hidden tags without ever dirtying KMS, so Electron/CEF renderers no longer suspend so deeply that a tag return takes seconds to recover.
  • Stable window identity end to end. The compositor mints a monotonic per-window id (never reused, never shifted by another window closing), exposes it in the snapshot (focus_history_v2, client id), and the shell addresses windows by it — dock clicks and app-group focus use the new focuswindowid dispatch, so focusing a window can never hit the wrong one through a stale slot index.
  • mctl perf went time-based: windowed FPS (1 s / 10 s / 60 s), render-latency percentiles (p50/p95/p99), a 10 s empty-frame ratio and a --watch live mode, instead of lifetime-average counters.
  • Clipboard: vim-style gg/G jump-to-oldest/newest in the history list, and a wider tonal gap between alternating rows.

Fixed#

  • Session-lock input hardening. Keys are dropped unless a buffer-ready lock surface actually holds focus (nothing can leak to the desktop in the lock-acquisition gap); pointer/axis events route only to ready lock surfaces; activation, move/resize and popup grabs are inert while locked; the render path never falls through to desktop windows on an output whose lock surface hasn't committed yet; initial maps deferred during a lock finish silently at unlock.
  • Per-output frame clock recovery. Render/queue failures now back off behind a bounded exponential retry instead of an immediate global re-ping; present, retry and cast timers carry ownership generations (a stale one-shot can never mutate a replacement clock — no more manual calloop source removal); vblank completion picks the accounting mode that queued the frame, surviving a mid-flight config reload.
  • IPC watch subscriptions are deduplicated and capped (16/connection, 512 total); the active-window marquee timer is stopped when its widget model drops; the mkeys control socket got 0600 permissions, a message length cap and a read timeout.

Performance#

  • Repaints are scoped per output. Surface commits resolve to the smallest safe repaint scope (synchronized subsurfaces return early; popups resolve to their owner; hidden clients and suppressed layers never dirty KMS); animation ticks, cursor motion, arranges and focus changes repaint only the outputs they touch. A busy window on one monitor no longer wakes the other.
  • The shell's state-sync projection is indexed (per-tag counters and address lookups went from quadratic to linear in window count); image codecs trimmed to the five margo decodes; criterion no longer compiles into test builds; dev/test profiles use line-tables-only debug info.

Changed#

  • The theme catalogue was curated from 53 schemes down to 20 dark ones, with existing configs migrated (CONFIG_VERSION=3).

[1.1.6] – 2026-07-09#

A native login greeter, and a hardening pass over the whole workspace. mgreet is a new binary: margo's own GTK4 greeter, drawing a login card on every connected monitor. The greeter that shipped before could only ever appear on one screen — cage has no zwlr_layer_shell_v1, so a GUI can't get a per-output surface under it. The new one is hosted under a margo greeter-mode instance instead, which does. Alongside it, an audit pass closed a set of panics, unbounded inputs and non-durable writes across the compositor, the shell and the plugin host.

Drop-in upgrade. The greeter host defaults to gui, with automatic fallback to the previous cage host and then to the TTY greeter, so a login is never gated on the new path working.

Added#

  • mgreet — a native multi-monitor login greeter. Plain gtk4-rs (no relm4: a login gate should have as few layers as possible), one layer-shell surface per output, shared entry buffers so typing on any screen is reflected on all of them, and per-connector window reconcile on hotplug. Real PAM authentication, then the credentials are handed to the mlogind orchestrator over the existing one-shot file — the greeter never opens a session itself. A wrong password clears the field and stays up; it never tears the compositor down for a typo.
  • mlogind [display] host = "gui" selects the new greeter, and is now the default. It launches margo --config <generated> --startup-command "mgreet; mctl dispatch quit", so margo tears itself down when the greeter exits. No compositor changes were needed — --config, --startup-command and the quit dispatch already existed. Falls back to cage, then to the TTY greeter, if any of margo / mgreet / mctl is missing.
  • Greeter conveniences: an F-key power footer mirroring the TUI greeter's actions (inert under --preview, so a dry-run can never power the machine off), a battery indicator on laptops, a Caps Lock warning under the password field, the hostname under the clock, and last-login memory — the username and session are pre-filled from the same cache the TUI greeter writes.
  • mgreet --preview runs the card non-destructively under a live session: no PAM, no hand-off, power keys inert, Escape quits.
  • cargo-deny is a hard CI gate (bans / licenses / sources), not advisory.

Changed#

  • Settings subprocesses moved off the main thread. The region, date-time and about pages shelled out synchronously while building; the privacy poller parsed pw-dump on the GTK main loop. Both now run off-thread.
  • Clock and calendar menu tickers start lazily, on first reveal, instead of running from login.
  • Twilight menu buttons and the app-launcher preview swatches follow the shared menu design language; the swatch CssProvider is now scoped per output rather than installed display-wide.

Fixed#

  • Layout gap axes. right_tile and vertical_tile applied their gaps on the wrong axis, rectangles weren't floored, and the spring mass was never wired through.
  • Screencast session identity. Sessions were keyed by memory address, which a reallocating client could collide; they now carry a stable per-client id. The session vector is bounded.
  • mctl requests time out and exit non-zero on a malformed reply, instead of hanging on a silent socket.
  • Config and backup writes are atomic and durable (write-temp, fsync, rename), so a crash mid-write can no longer lose a profile.
  • A notification action button outliving its runtime no longer aborts mshell, and the launcher no longer aborts when its process is reaped.
  • mlogind honours a captured login even when the greeter host SIGSEGVs during DRM teardown — the hand-off is read before the host's exit code is judged.
  • mlogind starts seatd for the greeter host: Arch's libseat has the builtin backend compiled out, and the root orchestrator has no logind session.
  • Greeter keyboard layout comes from /etc/vconsole.conf rather than defaulting to US.

Security#

  • PAM and polkit plaintext passwords are zeroized from the heap after use.
  • WASM guest execution and plugin node-tree recursion are bounded, so a plugin can no longer hang or blow the stack of the shell that hosts it.
  • Plugin manifests can no longer inject into the managed binds file.
  • wayle-notification rejects crafted image-data hints — a malformed hint from any client on the bus could take the shell down.
  • The compositor is hardened against panics and hangs from driver and script input, and margo-config guards three parser crash paths reachable from a hand-edited config on reload.

[1.1.5] – 2026-07-07#

A professionalization pass over the mdash dashboard (mshellctl menu mdash) — the design language 1.1.4 brought to the bar menus, now applied to the dashboard itself: a stronger header hero, titled cards that share one caption rhythm, a section-headed action grid, and clearer grouping of the power actions. Drop-in upgrade — no feature removals.

Added#

  • Reusable SectionLabel menu widget — a small tonal caption that titles a run of tiles or actions in any panel. Wired into the dashboard as a "Quick Actions" heading above the bottom action grid, so the lower half reads with the same titled rhythm as the cards above.
  • Card titles on the dashboard's right column. Overview ("At a glance"), Connectivity ("Connected"), Compact Audio ("Sound"), and System ("System") now share one caption idiom (small, bold, tonal, letter-spaced) so every card in the column reads as a single titled set.

Changed#

  • Dashboard header hero. The greeting is larger and heavier, the leading glyph picks up the matugen accent, and the live date reads as a quiet pill — a wider hero treatment scoped to the dashboard, while the shared panel header stays deliberately calm in the narrow bar menus.
  • Overview "At a glance" card. The leading signal dots now carry each row's calm / warn / danger severity, so a row reads as one signal rather than a neutral dot beside a coloured label.
  • Action-grid grouping. The second action row (session / power) is set off as a distinct band — a top hairline, a container tint, and a clearer gap — so Shutdown / Reboot / Lock read as a deliberate zone rather than one more toggle in the strip.

[1.1.4] – 2026-07-07#

A professionalization pass across the shell's layer-shell menus, plus two XWayland input fixes. Every bar menu — Network, Bluetooth, Audio, VPN, UFW, Power, Control Center, Launcher, Clipboard — was reworked into one consistent, deliberate design language: surfaces that read as buttons, matugen-tonal accents instead of raw primary, equal-width actions, and tonal section headers in place of hard separators. Alongside it: a themeable MRU Super/Alt+Tab window switcher, real hot-corner controls, macOS-style animation presets, and fixes for two XWayland input bugs that made tiled X11 apps (vncviewer) misbehave. Drop-in upgrade — no feature removals.

Added#

  • MRU window switcher. A niri-style Super/Alt+Tab overlay (thumbnail strip, most-recently-used order, release-to-commit), themeable and configurable, with its own Settings → Window Switcher page.
  • Real hot-corner controls in Settings → Behaviour — per-corner action + dwell time — replacing the long-dead hotarea knobs, with a richer action list (not just Toggle Overview).
  • macOS-style animation presets in Settings → Animations (Silk / Swift / Satin / Breeze), with the active preset highlighted.
  • VPN indicator pill — a generic "VPN up" bar indicator with a per-tunnel detail menu, superseding the osc-mullvad-specific widget.
  • A shared keycap-chip component (.kbd-chip / .kbd-key / .kbd-caption) for consistent shortcut hints across menus.

Changed#

  • Layer-shell menu professionalization pass. Network, Bluetooth, Audio, VPN, UFW, Power, Control Center, Launcher, and Clipboard menus now share one design language: button surfaces raised off the panel so they read as buttons, scheme-fit (matugen-tonal) accents for the connected/active state instead of raw --primary, equal-width action buttons, tonal uppercase section labels in place of hard separators, and keycap footers / source badges where they help.
  • Audio Route folded into Settings → Sound plus a device-aware dashboard pill; the standalone Audio Route pill + picker were removed (mshellctl audio route-next kept).
  • smithay pin advanced to master HEAD (past the v0.7.0 tag).

Fixed#

  • XWayland cursor offset. A tiled X11 window (e.g. TigerVNC's vncviewer) no longer drifts its pointer / right-click-menu position: the compositor re-asserts the tiled geometry when the client tries to self-configure instead of echoing the client's request.
  • XWayland input-freeze after a tag switch. A tiled X11 window no longer goes input-dead — rendering the remote framebuffer live while forwarding no pointer/keyboard — after switching tags and back. X11 toplevels now skip the tiling slide animation that drove them into that state; native Wayland windows still animate.
  • Clipboard — removed the empty Files tab and the date-section list model that froze the panel and broke tab switching.
  • Launcher — Ctrl+R (resume last query) is now reliable.
  • VPN — the connect notification no longer reads "Tunnel is down" on a successful connect.
  • Calendar — event days render their dot again (GTK4 mark_day sets the :checked state, not :selected).
  • Control Center — flat, aligned tile icons.

[1.1.3] – 2026-07-03#

A native calendar and a much bigger control CLI. The headline is mcal — a GTK-free calendar that reads local .ics folders, remote ICS subscriptions, and (read-only) Google Calendar accounts over OAuth, surfaced in the clock menu, the dashboard, Settings, and a CLI. mshellctl grows into the single control surface for the whole shell and its companion tools, both tools ship shell completions, and a keyring backend bug that silently dropped stored secrets is fixed. Drop-in upgrade — no feature removals.

Added#

  • mcal — native, GTK-free calendar. ICS parsing + RRULE recurrence, a local-folder provider, remote-ICS subscriptions, and read-only Google Calendar accounts over OAuth (loopback + PKCE, BYO client id, refresh token in the keyring; --no-browser for adding a second account). One loader feeds every surface: an event-aware clock-menu calendar (month-grid marks + per-day agenda), a tabbed Month/Agenda card in the mdash dashboard, a Settings → Calendar page (local dir + subscriptions + connected accounts), and a mcal today|agenda|on CLI plus mcal account setup google|list|remove. Config lives under ~/.config/margo/mcal/.
  • mshellctl companion-tool proxies + top-level verbs. New commands proxy the sibling tools — calendar (mcal), vpn (mvpn), power (mpower), layout (mlayout), osk (mkeys), color (mpicker), play (mplay) — and the session/notification power-verbs are promoted from under menu to first-class session and notification commands. The --help overview is now grouped into headed sections.
  • Shell completions for mctl + mshellctl (bash/zsh/fish), clap-generated so they cover every subcommand/flag, installed system-wide and regenerated with just completions.
  • Bar pills: Countdown and Audio Route (native DMS-plugin ports). Audio Route is device-level — left-click cycles routable outputs, right-click opens an in-frame picker — with an icon-only pill and mshellctl audio verbs.

Changed#

  • mcal config moved from ~/.config/mcal to ~/.config/margo/mcal (auto-migrated).
  • mcal provider failures (dead token, offline, API error) are now surfaced on stderr instead of being silently swallowed, so an empty today is diagnosable.

Fixed#

  • Keyring secrets now persist. The workspace keyring backend is pinned to async-secret-service; the default sync-secret-service backend silently failed to persist against gnome-keyring (set_password returned Ok but the item was never retrievable), which dropped mcal OAuth tokens, AI keys, and plugin secrets.
  • Calendar event marks now render — GTK4 assigns marked days the :selected state, not a .marked class, so the previous styling was dead.
  • Shell services (notification, power-profiles, brightness, system-tray) now initialise best-effort, so a missing/absent service can't crash-loop the shell.
  • AI: the Gemini key is sent via the x-goog-api-key header, not the URL query.
  • Compositor IPC caps the inbound buffer and restricts the control socket to 0600.
  • mdots: the release binary no longer bakes in CARGO_MANIFEST_DIR, and prune restores the original file instead of orphaning it.
  • Config: char-safe truncation for animation_type / keymode.

Packaging / CI#

  • Ship mvpn; fix install.sh binary drift; add a cargo-deny advisories gate.

[1.1.2] – 2026-07-02#

A security & robustness hardening pass. This release closes three real weaknesses — an ungated WASM-plugin host, symlink-unsafe secret writes, and a compositor that could panic on malformed screencast input — brings the previously-untested auth/lock crates under test, and tightens the panic ratchet so more abort vectors are gated. No feature removals; it is a drop-in upgrade.

Added#

  • WASM plugin capabilities (deny-by-default). A plugin's manifest.toml now declares a [capabilities] table (process, network, clipboard, all false by default). The shell threads the granted set to the WASM host, which refuses any ungated run/process-start, http/http-start, or copy/clipboard-read call — returning a capability error to the guest and logging a denied … warning rather than performing the action. Previously only the filesystem path-sandbox was enforced, so any loaded plugin could spawn processes or reach the network. Covered by a new denies_network_without_capability integration test.
  • Tests for the auth/lock boundary. mlock, mshell-auth, and mshell-polkit (previously zero tests) gain unit coverage for their pure logic: config/sidecar parsers, battery classification, the power double-press confirmation window, fprintd→event mapping, interior-NUL input rejection, and the PAM-helper wire-protocol parser (including the trailing-space guard that stops a spoofed SUCCESSFUL from reading as completion).

Security#

  • Secrets writes are now symlink-safe (mdots). resolve_secret_target gained a canonicalize-based guard so a target reaching the config repo through a symlinked path component (e.g. ~/.config/mdots → the repo) is refused, not just the lexical case — decrypted plaintext can no longer slip into git that way. The atomic writer now creates its temp file with create_new (O_EXCL) after unlinking any stale temp, closing a TOCTOU where a pre-planted symlink at the temp path was written through.
  • Compositor no longer panics on malformed screencast input. The PipeWire format-negotiation and cast-render paths (pw_utils.rs) and the Mutter ScreenCast D-Bus session (mutter_screen_cast.rs) ran on the main event loop and unwrap()-ed external input; a bad SPA pod or bus error took down the whole desktop. They now degrade gracefully (stop_cast() / skip-frame / warn).

Fixed#

  • matugen no longer panics on a non-UTF8 wallpaper path. A wallpaper whose path isn't valid UTF-8 now returns a themed error instead of aborting the matugen worker.

Changed#

  • Panic ratchet hardened. scripts/panic-ratchet.sh now also counts unreachable!/todo!/unimplemented! and excludes test code by brace depth (so a mid-file #[cfg(test)] no longer hides the production code after it); baseline is 370. just check now runs the example-config parse (mctl check-config) that CI does, so a parser regression is caught locally.
  • Internal decomposition. control_center/tiles.rs (2039 → 1754) split its live-state probes into tile_probes.rs; mdots/src/lua/mod.rs (2510 → 2420) split its Lua error diagnostics into error_hints.rs. Pure lift-and-shift, no behavior change. Docs: retracted stale "gap" claims for overview and window grouping (both shipped) and closed the taglayout/tagrule unify item.

[1.1.1] – 2026-06-30#

mdots grows a real TUI. 1.1.0 shipped mdots as a CLI; this release makes its terminal UI fully interactive — you can now act from it, not just look. Every system-mutating action suspends the UI, runs the exact same commands::* code path the CLI uses, then restores the screen, so there is one implementation and one set of confirmations behind both front ends. Also new: a read-only doctor health check (CLI and in-TUI overlay), a diff drift summary, native file logging, and a hardening pass.

Added#

  • Interactive mdots TUI. Seven screens — Overview, Modules, Packages, Sync, Services, Secrets, and Hooks — reachable from a sidebar with keyboard navigation (Tab/Enter, j/k, / to filter), a ? help overlay, and per-screen footer hints. From the UI you can now enable/disable modules, run a sync, enable/disable service profiles, edit and sync SOPS/age secrets, and run a module's pre/post-install or disable hook — each behind a confirm dialog, each reusing the CLI command verbatim via a suspend → run → restore handoff.
  • Doctor. mdots doctor is a read-only environment health check — config, package backend, flatpak, secrets, Lua manifests, and nix/home-manager — reporting pass/warn/fail and exiting non-zero on any failure. The same report is available inside the TUI as a scrollable Shift+D overlay.
  • Diff. mdots diff summarises drift between the declared config and the installed system.
  • Mouse + matugen theming in the TUI. Wheel scrolling routes to whatever is focused (overlay, sidebar, or screen) and a left-click on the sidebar navigates; accent borders and the sidebar selection follow margo's matugen palette (read from ~/.cache/margo/mshell-colors.toml), with a built-in fallback when it is absent.
  • User guide for mdots covering the config model, secrets, nix, and the full command surface.

Changed#

  • mdots logs through margo-logging. Routine CLI runs now write to the shared durable file sink (per-start rotation, reloadable level) at warn level instead of going through env_logger.

Fixed#

  • Flatpak scope. The TUI sync preview now passes the canonical --user/--system scope argument to flatpak instead of a bare user/system, so its install/prune counts match a real sync.
  • Doctor PATH probe. A missing AUR helper is reported as a warning rather than silently passing.
  • No dcli left behind. Every dcli identity string, compat symlink, and migration path was removed; mdots reads its config solely from ~/.config/mdots.

[1.1.0] – 2026-06-27#

A deep performance and robustness pass over the whole stack — the compositor's state egress, the render hot path, the shell's menus, and the streaming chat and launcher surfaces — plus a batch of concrete correctness fixes surfaced by the same review. Nothing changes about how margo looks or how you drive it; it just does less work to get there, holds the desktop asleep when nothing's happening, and no longer panics on a few hotplug and poison races.

Added#

  • mdots — a declarative, NixOS-style package + dotfiles manager for Arch (pacman/AUR/flatpak, Lua-scriptable config, a TUI, services, SOPS/age secrets), now a first-party workspace binary reading its config from ~/.config/mdots. Built and packaged alongside the other m* tools (just dots, install.sh, PKGBUILD) with a generated man page and shell completions.

Changed#

  • The compositor does far less work per state change. Every IPC watch flush built the state snapshot once per subscriber and re-serialised it each time; it now builds the snapshot once per flush and serialises the shared state frame a single time, projecting the rest. Within the snapshot, the per-monitor occupied-tag mask and scratchpad counts were each a full scan of every client per monitor — folded into one pass — and the constant layout-name list is built once instead of every snapshot.
  • Idle desktops actually idle. A blurred always-on surface (a blurred bar, an acrylic window) reported full damage every frame, forcing a full-output repaint at refresh rate forever. The blur element now only self-damages when its own geometry changes; a static region behind it lets the frame be skipped entirely, while anything changing behind it still repaints correctly. Window shadows and blur also keep stable element ids across frames instead of defeating the damage tracker with a fresh id each frame, and the per-window client lookup on the render path is now O(1).
  • Closed menus stop polling. The CPU dashboard (its /proc + dual-hwmon walk), the media menu's two 30 ms marquee timers, the system-status temp poll, the overview-intel update probe, and the clock / calendar ticks all kept running while their menu was closed. They now stop on hide and resume on show — with an immediate refresh so nothing reads stale when you reopen.
  • Menus build when first opened, not eagerly per monitor. The Settings panel is now constructed on its first toggle rather than on every output at login; the active-monitor lookup that routes menu placement resolves from the shell's mirrored state store instead of a blocking socket round-trip; and an unchanged monitor layout no longer tears down and rebuilds menu surfaces.
  • Streaming chat and the launcher stay responsive. The AI assistant set the entire growing reply on its bubble every token (a full Pango relayout per token, O(n²) over a long answer) — repaints are now coalesced to ~30 Hz with the final text always flushed at the end. The launcher re-swept every provider and re-cloned every row on each keystroke; typing now debounces the recompute by 60 ms while keeping the text field in lock-step.
  • Window-rule matching got cheaper. Window-rule regexes are compiled once and cached instead of recompiled on every match, the "does any rule carry a title pattern" check is cached at config-load instead of recomputed on every title commit (frequent for browser tabs / terminal titles), and the render text-label cache no longer allocates a key string on every cache hit.
  • The packaged installer ships an optimised build. install.sh now builds with the dist profile (fat LTO + a single codegen unit) and installs from target/dist; the shipped binaries previously missed the optimisation that just / CI intentionally skip for dev-loop speed.
  • The Margo layout menu is keyboard-navigable. mshellctl menu margo-layout (and the layout bar pill) now walks its rows with Tab / Shift+Tab, the arrow keys, and Ctrl+N / Ctrl+P — the same focus-walk the session menu uses — with Enter / Space activating the focused layout.
  • Internal: the frame's menu-toggle plumbing is consolidated. The 34 uniform "open this menu" messages — each a data-less FrameInput variant plus an identical match arm, mirrored across the bar-output routing and the shell dispatcher — collapse into a single ToggleMenu(MenuId) path. No behaviour change (a routing cross-check pins every pill and IPC trigger to the same menu it opened before); it just sheds ~80 lines of parallel boilerplate and a class of "forgot to wire one of the four places" bugs.

Added#

  • Game Mode — one toggle that drops compositor effects (animations, blur, shadows, optionally screen tearing), turns on Do Not Disturb, and holds the idle inhibitor so the session stays awake while gaming. Effects are dropped via a managed conf.d/gamemode.conf fragment margo sources after config.conf (so it wins) while engaged, and cleared on exit — the original values come straight back, nothing in config.conf is rewritten. Toggle it live with mshellctl gamemode on|off|toggle (or status), or from Settings → Game Mode, which also picks what the mode affects. State is reconciled by a single reactive effect, so the CLI, the Settings switch, and a mid-game shell restart all share one apply path; the idle inhibitor remembers and restores its prior state so it never clobbers a manual hold. Port of the caelestia-dots game-mode toggle.
  • Lyrics widget — a native synced-lyrics bar pill + panel for the now-playing track, replacing the external musiclyrics WASM plugin. The pill scrolls the current line in the bar; the panel (mshellctl menu lyrics, or click the pill) renders the whole song as a scrolling column with the active line lit and auto-centred as it plays, topped by a source/state badge (Synced · lrclib.net, Instrumental, …) and a manual re-fetch button. Lyrics are resolved off-thread from lrclib.net (exact /api/get, then a fuzzy /api/search), parsed from LRC, and disk-cached under ~/.cache/mshell/lyrics/ — including a definitive "no lyrics", so a re-play is offline and instant while a transient network error still retries. The position is read from MPRIS, so any player works (Spotify, mpd, browsers, mpv). Fetching only runs when the pill is on a bar or the panel is open, so a user who never adds the widget never hits the network. Whether the current line shows in the bar (vs. icon-only, words only in the menu), the bar line's width in characters, and the menu's size / position all live under Settings → Widgets → Lyrics.
  • State-change toasts — a small opaque corner card that announces a system state change and auto-dismisses, distinct from app notifications (which carry history) and the volume/brightness OSD (a value pulse). A single headless producer (mshell-osd::toast_producer) subscribes to every source once — AC power plug/unplug, Caps/Num Lock, keyboard layout, default audio output/input device, VPN connect/disconnect, now-playing, primary network link connect/disconnect, Bluetooth device connect/disconnect, power-profile change, Do Not Disturb, idle inhibitor ("keep awake"), and Game Mode — tracks each previous value so it only fires on a change (and stays quiet at login), and broadcasts to a per-output toast surface; centralising it keeps the mvpn and lock-key pollers running once total rather than once per monitor. The network and Bluetooth sources reuse the same per-device watcher re-subscribe pattern as their bar pills, so a connected flip on an already-listed device still surfaces. Battery warnings became a ladder (warn at 20/10/5 %, danger at a configurable critical level) that re-arms on recharge, replacing the single low-battery threshold. Every event has its own switch under Settings → Toasts, which also picks where toasts dock (any screen edge or corner), the margin from it, and an optional fixed card width — the OSD-style geometry knobs. mshellctl toast "<title>" [body] [--icon NAME] [--severity calm|warn|danger|positive] is the notify-send equivalent for scripts and startup services. Port of the caelestia-dots toast framework.
  • start-margo is now an event-driven, hang-aware supervisor. The session watchdog was rebuilt around a single poll(2) over a signalfd, the child's pidfd, and margo's readiness pipe — it sleeps until something actually happens instead of waking 20×/second for the whole session (zero idle CPU, instant shutdown/readiness handling). On top of that:
  • systemd watchdog support. When the unit sets WatchdogSec=, start-margo forwards WATCHDOG=1 keep-alives driven by a heartbeat margo writes from its own event loop, so a hung (live-locked) compositor is now detected and recovered — previously only outright crashes were.
  • Race-free signal forwarding. SIGTERM/SIGINT/SIGHUP are blocked process-wide and drained from the signalfd, so a signal arriving in the window before the child is spawned is still delivered to it — with the original signal preserved — and margo's graceful teardown always runs instead of being skipped in favour of a SIGKILL.
  • Exponential restart backoff (250 ms → 5 s) so a config that crashes on every start can't hammer the GPU init path.
  • --safe-config <PATH> makes one last attempt with a known-good config when the crash budget is exhausted, instead of dropping straight to the display-manager.
  • A small readiness protocol over the pipe (READY=1, WATCHDOG=1, STATUS=…, FATAL=1) forwarded to systemd; FATAL=1 skips the restart loop for an unrecoverable init failure.
  • Durable logging through the shared margo-logging file sink, so the reason a session bounced back to the DM survives without a journal.
  • Distinct exit codes for the supervisor's own conditions (78 config preflight, 69 crash budget, 127 spawn failure).
  • margo readiness-pipe heartbeat. When launched with MARGO_HEARTBEAT_USEC (set by start-margo under a systemd watchdog), the compositor keeps the readiness pipe open and beats it from its calloop loop, proving the loop is still turning.
  • Adjustable frameless bar gap (Settings → Bar). With "Enable frame drawing" off, each bar floats as its own inset panel; the inset is now a setting (bars.frame.frameless_gap, px) with a live slider instead of a hardcoded SCSS value.

  • mlogind's baked default palette now matches the desktop. The greeter's in-code//etc/mlogind/variables.toml default was a generic Dracula baseline; it's now baked from the active matugen wallpaper theme, so a fresh install (or a config reset before mlogind sync-theme runs) shows a login screen that already matches margo's look instead of an off-palette fallback.

Fixed#

  • The shell could abort (SIGABRT) while typing fast in the app launcher. The search box debounces its recompute with a one-shot glib timeout. A one-shot source auto-destroys the instant it fires, but the launcher only cleared its stored handle when the queued ApplyFilter was later processed — so a keystroke landing in the window between "timer fired" and "ApplyFilter ran" called SourceId::remove() on an already-dead source, which panics in glib. From a main-loop callback that panic can't unwind, so it aborted the whole process (the still-live search Entry's changed handler then tried to input() into the now-shut-down component, compounding the abort). The one-shot now clears its own handle the moment it fires, and the search entry's changed/activate handlers use fallible sends — mirroring the hardening already in the media-player seek slider — so neither a stale timer nor a teardown-time signal can crash the shell.
  • The shell wouldn't start on a machine with no Bluetooth adapter. On a host with no BT hardware (e.g. a VM), bluez is installed but bluetooth.service is condition-skipped, so when mshell asked D-Bus to activate org.bluez it failed (NameHasNoOwner: … unit failed). Because every backend service is built in one try_join! and BluetoothService::new() registers a BlueZ agent that requires a live org.bluez, that one failure aborted the whole startup — the shell crash-looped to start-limit-hit and never came up (the compositor was fine, so you got a bar-less desktop). Bluetooth is now best-effort: it logs a warning and degrades to "no adapter" (bluetooth_service() is Option), so the bar, menus, and the rest of the shell start normally; the Bluetooth pill simply shows the hardware-missing state.
  • mshellctl audio switch fired a duplicate notification. Switching the default output/input raised a notify-send desktop notification and the toast producer's "Audio device" toast for the same change. The redundant notify-send is gone; the toast (gated under Settings → Toasts) is now the single source of feedback.
  • mshellctl bluetooth … did the same — notify-send on top of a toast. The auto-connect engine popped a notify-send on connect / disconnect / audio-routing, but the toast producer already toasts Bluetooth device connect/disconnect (Settings → Toasts → Bluetooth) and the default-audio change, so toggling gave you both. All notify-send is dropped; the reactive toasts are the single source, and an explicit toggle on a host with no adapter now toasts "Bluetooth — No adapter" from the IPC layer. The redundant per-widget Notifications switch under Settings → Bluetooth is removed — toasts.bluetooth is the one control.
  • The lyrics panel showed its header but no lyrics. The scrolling lyrics column lived in a GtkScrolledWindow, which reports ~0 natural height, so in a menu that sizes to its content the viewport collapsed and no lines rendered (the header, source badge and separator showed; the body was blank). The viewport now has an explicit height — the active line auto-centres in it and long songs scroll internally.
  • Menus baked into an older profile opened empty. A menu the profile was saved before it existed (e.g. the new Lyrics panel, or Media Player) was serialised as nothing, so it deserialised to an empty widget list and opened blank — the rich per-menu default was shadowed by the empty Menu::default(). An empty menu is never intended, so the builder now falls back to the per-menu default widget set when the configured list is empty.
  • l (lock-apply) keybinds were a silent no-op. Binds tagged l to keep working over the lock screen (volume / brightness / media keys) were never actually dispatched while locked — only force_unlock was, and everything else was forwarded to the lock surface. Tagged binds now run while locked and the key is swallowed; every other bind still forwards, so the lock screen stays secure.
  • Secondary / tertiary fonts were ignored. The matugen style builder read .primary() for all three font slots, so the configured secondary and tertiary fonts never took effect.
  • Bogus config warnings. mru_scope, mru_filter and mru_show_labels were validated as integers, so margo's own shipped string/bool defaults tripped a spurious "expected integer" warning. They're now validated as the enums / bool they are.
  • Asymmetric tiling gaps were wrong. The tile layout used the horizontal inner gap for the vertical spacing between stacked windows, and vertical_grid transposed the work area but not the gaps — so a setup with gappih ≠ gappiv tiled with the gaps on the wrong axes. (Symmetric gaps, the common case, were unaffected.)
  • Keybind/IPC menus could open on a random monitor. When the resolved monitor name didn't match (a stale or missing name), the shell fell back to an arbitrary hash-map entry, so a menu could open on a different output run to run. The fallback is now deterministic.
  • A stalled clipboard source could freeze clipboard history. Reading a paste offer looped on a blocking pipe read with no timeout, so a source app that requested the transfer then stopped writing wedged the single clipboard watcher thread forever. Reads now bail after a 2 s idle with no progress (a transfer that keeps flowing is never cut off).
  • A few hotplug / poison races could crash the compositor or shell. The per-keystroke xkb layout read, the clipboard watcher's Wayland read preparation, and the shell's per-output frame init each unwrap()-panicked on an unlikely-but-possible race; all three now recover gracefully.
  • Frameless bars overlapped windows until something else nudged the layout. Turning "Enable frame drawing" off makes each bar paint its own inset floating panel, but the layer-shell exclusive zone wasn't re-measured on the toggle, so windows kept their old tiling and the bar's inset overlapped the window's top until an unrelated event (a wallpaper rotation, any bar setting) happened to re-measure it. The toggle now re-measures both bars immediately, and the inset is applied as a GTK margin so it's reserved in the exclusive zone — windows tile below the floating bar right away. At gap 0 the bar squares off flush to the screen edges (no rounded corners or drop shadow), so its shadow can't paint past the reserved zone over the window's top.
  • Customised shell settings could be silently reset by a later update. The shell rewrote the entire resolved profile to active.yaml, baking every field at its then-current default; a field you never touched kept that stale baked value and shadowed a newer code default. Persistence now writes only the keys that actually differ from the current defaults, so untouched settings track the live default and your real customisations are preserved.

Tests#

  • First unit coverage for the tiling algorithms' gap semantics: tile, grid and vertical_grid are pinned against asymmetric gappih/gappiv.
  • Config persistence is pinned: a customised profile round-trips through the diff-against-default serialiser unchanged, and a field left at its default is confirmed omitted from the written active.yaml.
  • start-margo gains end-to-end integration tests that drive the real binary against a fake margo script — clean exit, crash-budget exhaustion, --no-restart propagation, --safe-config fallback, and race-free SIGTERM forwarding — plus unit coverage for the backoff curve, the readiness-line parser, and the poll-timeout computation.

[1.0.11] – 2026-06-26#

A performance + memory release. The clipboard and launcher no longer stutter on large histories or package searches, mshell's memory footprint is cut substantially, and the keep-awake-while-media-plays inhibitor no longer freezes the shell. Menus also open and close more smoothly, and on a multi-monitor desk they now open on the monitor you're actually working on.

Fixed#

  • Menus opened on the wrong monitor. Keybind/IPC-triggered menus (launcher, settings, every pill menu) opened on whichever monitor the mouse cursor happened to sit on, even when you were working on another output. The compositor's "active output" is now last-writer-wins between keyboard focus and the pointer, so a menu opens on the monitor of your most recent interaction: the focused monitor when you're working there (keybind or typing), the cursor's monitor when you just moved onto another output (including an empty one). Menu-open keybinds stay neutral, so the trigger itself never decides the target.
  • Keep-awake-while-media-plays could freeze the shell. The idle inhibitor added in 1.0.10 re-subscribed to the wayle playback Property from inside the very handler that subscription fired, looping forever the moment playback state changed — a hard freeze of the bar and menus. List-change (which needs a re-subscribe) is now split from item-change (reconcile only), so playback transitions no longer feed back on themselves.
  • Clipboard history was sluggish to open and search with a large history. The menu rebuilt its list from a full clone of every entry — including each entry's raw bytes, which images make tens of megabytes — on every open, every copy-while-open, and every search keystroke (e.g. a 1000-entry history with a handful of images cloned ~27 MB per keystroke). It now builds from lightweight preview-only views; the raw payloads are touched only by the persistence layer that actually needs them.
  • Launcher froze while searching packages. Typing p <query> ran pacman -Ss (or paru -Ss) synchronously on the GTK main thread — a 1–3 s subprocess per keystroke that locked up the whole UI. The lookup now runs on a worker thread (a brief "searching…" row shows while it's in flight), and a short settle window coalesces a fast typist's keystrokes so only the term they pause on hits pacman.

Changed#

  • Smoother menu open/close. The first reveal frame used to stall on a synchronous content build plus a broadcast that kicked off every widget's pollers (network/Bluetooth scans, launcher requery) — most visible as a laggy slide, worse on close. Those poller kick-offs are now deferred off the animation's critical frames, and edge + corner menus share one reveal timing so the whole set opens and closes consistently.
  • >start scripts are indexed off the main thread. The scripts provider used to walk every $PATH directory (thousands of entries under /usr/bin) synchronously on every launcher open. It now scans lazily on a worker thread, only the first time >start / the Run tab is actually used.
  • Substantially lower memory footprint. mshell's RSS was dominated by glibc malloc-arena fragmentation driven by its thread count (a CPU-sized async pool
  • per-service workers spun up ~150 × 64 MB arenas). Capping the glibc arenas (MALLOC_ARENA_MAX=2 in mshell.service) cut RSS by ~38 % (~1.25 GB → ~782 MB) and reserved virtual address space from ~13 GB → ~1.5 GB. A mimalloc global allocator (Rust-side allocations) and a right-sized tokio worker pool (4 workers instead of one per CPU) trim the remainder and keep RSS from ratcheting up over a long session.

[1.0.10] – 2026-06-24#

A small release: a >run launcher palette with $PATH completion, fzf-style search highlighting, a redesigned Podman menu with container shell/logs/ports, keyboard-typable Settings colour pickers, an XWayland window-geometry fix, and a dock icon-override fix, plus an internal field rename.

Added#

  • Keep awake while media plays. A new opt-in Settings → Idle → "Keep awake while media plays" holds the idle inhibitor while any MPRIS player is playing and restores the previous inhibitor state when playback stops — so a video or music keeps the screen on without toggling Keep Awake by hand. (Ported from the DMS audio-inhibit plugin.)
  • Keyboard shortcuts in the capture menus. The Screenshot and Screen-record menus now respond to keys while open: A all · M monitor · W window · R area (S too), Esc to close; in the recorder, S / Space stop. A dim hint line at the bottom of each menu lists them. (Inspired by the DMS ScreenCapture toolbar — margo already had photo + video capture as two menus; this adds the in-menu keyboard control they lacked.)
  • Settings → Display → Layout — quick display profiles + per-output power. On top of the existing named-mlayout catalogue + drag-to-arrange editor, the page gains four one-click profiles — Internal only · External only · Extend · Mirror — applied live via mctl (the laptop panel is detected by its eDP/LVDS connector; Mirror duplicates it onto an external monitor via wl-mirror). A per-output list adds a Toggle to flip any single display on/off. (Inspired by the niri display-settings plugin, built on margo's native mlayout + mctl dispatch.)
  • CPU dashboard menu — Storage section. The dashboard menu now shows a Storage group: a per-mount usage bar (used / total + %, calm/warn/danger) for each real filesystem, read from the shared system-stats service. Disk was the one system metric the shell didn't surface anywhere.
  • Podman menu — expandable container cards with shell, logs & ports. The mshellctl menu podman Containers tab is now the house revealer-row card: a state-tinted glyph + name + image + chevron, expanding to the human status line, published port mappings as chips (3000 → 8080), and a state-aware action list — Shell (interactive podman exec -it, bash with sh fallback, opened in your terminal), Logs (podman logs -f in a terminal that stays open), Pause/Unpause, plus Start/Stop/Restart/Remove. Containers sort running → paused → stopped, and an open row survives auto-refresh and post-action repolls instead of collapsing under you. Ported from the DMS Docker Manager plugin.

  • >run launcher palette — $PATH executable completion. The launcher's command runner is now reached via >run (renamed from >cmd) and, while you type a single bare token, completes against $PATH executables: >run fire surfaces firefox, firejail, … An exact name launches directly on Enter (spawned by absolute path); a multi-token line (>run a | b) still runs as a free-form sh -c command; frecency lifts your most-used binaries to the top.

  • fzf-style search highlighting. The launcher results and the clipboard menu's /-filter now accent-colour the characters your query matched in each row. The colour is read live from the matugen --primary, so it follows the theme; matched chars are coloured only (no bold), so row geometry doesn't shift.

Fixed#

  • Settings colour pickers can be typed into. A gtk::ColorDialogButton opens its chooser as a separate toplevel window, which can't take keyboard focus under the layer-shell Settings surface — so the bar frame fill / border / separator and the overview backdrop colours could only be set with the mouse (wheel / sliders), never by typing a hex code. Each picker now has an inline hex entry that lives in the Settings surface itself: type or paste #rrggbbaa (or 0xRRGGBBAA) and press Enter. The dialog button stays for mouse picking; both paths stay in sync, and an unparseable value flags the entry.
  • XWayland windows now honor their compositor-assigned geometry. Managed X11 (XWayland) toplevels are configured to their scene rect, so they fill their tile instead of opening at a too-small self-chosen size; and override-redirect popups track position changes after their initial map.
  • Dock icon overrides now resolve through the configured app_icon_theme. A dock.icon_overrides entry given as a bare icon name (e.g. brave-browser) is now looked up in the configured app icon theme — with that index's built-in hicolor/pixmaps fallback — instead of only the shell's default GTK theme. Theme-specific icons therefore render without hardcoding an absolute file path, and the lookup follows whatever app_icon_theme is set (no dependency on any one icon set). On a miss it still falls back to the default icon theme by name. Absolute / file:// / ~/ override values keep loading straight from disk as before.

Changed#

  • Bar system-stat widgets now share one poll pass. The CPU pill and the Network pill each ran their own /proc poll loops while the reactive wayle-sysinfo service (already built at startup) sat unused. Both pills now read from that single service — one poll feeds every consumer, the pill and the dashboard menu can't skew, and per-interface throughput + disk come from the same place. (No external stats helper: native in-process reads beat an extra subprocess.)
  • Collapsible Settings sidebar with friendlier group headers. The sidebar sections are now real accordion groups — each header is a clickable row (icon + Title-Case name + chevron) that expands/collapses its pages, with the open/closed state remembered across reopens and the active page's group auto-expanded. The headers are also styled to actually read as headers — full-strength --on-surface at bold weight with a --primary-accent icon, so a group clearly out-ranks the page rows beneath it instead of looking like a dim caption. Headers are also renamed from terse ALL-CAPS jargon to user-facing names: SHELL → Shell & Desktop, SYSTEM → System & Devices, INPUT → Input & Shortcuts, plus Appearance / Locale & Accounts / Advanced / About. Two page labels soften too — OSD → On-screen Displays, Keybinds → Keyboard Shortcuts. Search keywords are unchanged, so the old terms still find the pages.
  • Renamed the dock's pinned-app field hyprland_class → app_id (and the matching set_icon/resolve_icon_candidates parameter). The value is the Wayland app-id margo reports for a window (mctl clients APP-ID), matched against running clients to decide focus-vs-launch; the old name was a leftover from the noctalia/hyprland port. Internal-only — the on-disk pinned_apps.txt format (desktop_id<TAB>app_id) is unchanged.

[1.0.9] – 2026-06-20#

Tracks Smithay to its current HEAD — a large but internal port — alongside an audio-backend health watchdog, a touchpad-scroll fix, a few launcher fixes, and quieter startup logs.

Changed#

  • Migrated to Smithay HEAD (e1fb2496, was ff5fa7df). ~2 months / 61 upstream commits, with three breaking APIs adapted:
  • Dispatch2 rework. Smithay replaced every per-protocol delegate_*! macro with a single blanket delegate_dispatch2!. The ~40 delegate_X!(MargoState) calls collapse to one; margo's own protocol modules keep their hand-written wayland_server::delegate_dispatch! impls (their concrete user-data types don't implement Dispatch2, so there's no coherence overlap with the blanket).
  • Touch API. TouchTarget dropped the seq: Serial argument on down/up/motion/shape/orientation, frame/cancel now take a FrameMarker, and a new last_frame is required; the FocusTarget impl was updated to match.
  • PointerConstraintsHandler::remove_constraint is now a required method (a no-op for us — constraint enforcement reads the live state each motion), and XWayland::spawn gained an extra_args parameter.
  • Transitive bumps that came with it: winit 0.30 → 0.31.0-beta.2, smithay-client-toolkit 0.20, plus the new reis/EI input crates.
  • The launcher's result preview pane now defaults off. A fresh config opens with the result list at full width; re-enable the detail pane in Settings → Launcher or with the in-launcher toggle.

Fixed#

  • The shell no longer aborts when a media player disappears mid-seek. The media-player seek bar's value_changed handler and its 300 ms debounced seek timeout used relm4's panicking sender.input(...); when the player vanished (or the menu closed) those could fire after the component was dropped, hitting a dead receiver — and inside a glib callback that panic can't unwind, so it took the whole shell down with SIGABRT. They now use the fallible sender, so a send-after-shutdown is a silent no-op.
  • Two-finger touchpad scrolling works in Firefox / Zen again. The compositor forwarded touchpad finger scrolling but never sent the matching wl_pointer.axis_stop when the fingers lift (libinput's 0.0-amount finger event was passed through as a value(axis, 0.0)). Firefox/Gecko on Wayland ignores finger-source scrolling without that stop, so two-finger up/down did nothing there while the mouse wheel worked. We now emit axis_stop on a finger-source scroll-end, matching the reference compositors.
  • No more ~1-second freeze when switching to the launcher's Actions tab. The Bluetooth / audio / media providers ran blocking bluetoothctl / wpctl / playerctl subprocesses on the GTK main thread inside their search/browse — which the runtime calls on every tab switch — so tabbing onto Actions stalled the UI (worst on Bluetooth, one bluetoothctl info per paired device). They now refresh off-thread and serve the last snapshot instantly, repainting via a re-query when fresh data lands; the tab switch is immediate.
  • Typing a script's full name in the launcher runs it directly. An exact start-* script match now outranks the generic "Run: " shell row in the Actions tab, so pressing Enter spawns the script by absolute path instead of the sh -c <name> fallback shadowing it from the top slot (which was a silent no-op for some scripts, leaving only the "$ " toast).
  • No more spurious "failed to bind EGL Wayland display" warning on startup. EGL_WL_bind_wayland_display is the legacy wl_drm buffer-sharing path; modern Mesa drops it in favour of linux-dmabuf (which margo sets up right after), so its absence is expected and harmless. The missing-extension case is now logged at debug instead of warn — genuine bind failures still warn.
  • Quieter startup log: three benign dependency lines suppressed. The shared log-noise filter now also drops wayle_audio's startup @DEFAULT_SINK@/@DEFAULT_SOURCE@ "not found in store" warnings (PipeWire reports the symbolic default before a concrete one propagates) and wayle_bluetooth's br-connection-page-timeout error (a trusted device that was off / out of range at connect time). The bluetooth line is matched on its message, so genuine connect failures still log.

Added#

  • Audio-backend health watchdog. wayle-audio's PulseAudio backend has no reconnect logic, so when the PipeWire server restarts under it — dcli sync re-applying the pipewire user services, or a transient drop during Bluetooth churn at login — its connection dies for the rest of the session and the volume keys + OSD silently stop working (while wpctl still drives the sink fine). The shell now notices the audio server restarting (a systemd MainPID change) and raises a single critical notification with a one-click Restart shell action — the only in-session cure, since a fresh mshell re-establishes the connection.
  • The launcher ranks recently-run entries to the top. In browse mode — the category tabs (e.g. Actions) and the empty-query list — the things you ran most recently now lead, with a decaying recency boost (3-day half-life) on top of the existing use-count. Typed all-search is unchanged: it still ranks by fuzzy match, so recency never overrides a better match. (Pinned rows still win.)

[1.0.8] – 2026-06-18#

Two fixes: clicking right after a touchpad-gesture tag switch no longer bounces you back to the tag you came from, and a lone smartgaps tile no longer shows a thin strip of wallpaper between the bar and its top/bottom border.

Fixed#

  • No wallpaper sliver above/below a lone smartgaps window. With smartgaps and a single tiled window the outer gap was clamped to 2 × borderpx on both axes. Left/right needs that — the work area is flush with the monitor edge there, so the outset border would otherwise glue to (and read as clipped at) the physical screen edge. But top/bottom the work area is already inset by the bar(s), so the clamp left a borderpx-wide strip of wallpaper between the bar and the border: invisible left/right (it abuts the bezel) but an obvious gap against the dark bar. gappov now uses just borderpx so the outset border lands flush against the bar; gappoh keeps 2 × borderpx. Applied to the live arrange and the overview pre-arrange path.
  • Pointer focus is refreshed before a click is delivered. A programmatic tag switch (the 4-finger viewtoright / viewtoleft gesture, super+N, an overview close, …) hides the window under a stationary cursor and remaps the scene, but Smithay only re-evaluates pointer focus on real motion — so the pointer kept its stale focus on the now-hidden window. The next click was delivered to that off-tag window, and because browsers / Electron apps self-activate on every click via xdg-activation, the activation switched the view straight back to the hidden window's tag. Symptom: swipe to a tag, then click without moving the mouse, and you'd jump back to the previous tag. handle_pointer_button now re-sends a zero-distance pointer motion (reusing the press's serial so a click-initiated move/resize grab still validates) before the button event, so the click lands on whatever is actually under the cursor now.

[1.0.7] – 2026-06-15#

A performance + code-quality pass acting on a deep review. The headline wins are on the shell's config write path and the compositor's twilight tick — both were doing redundant work on hot/frequent paths — plus a fragile menu-placement data structure retired and a permanent idle wakeup removed.

Added#

  • Multiple, independently-addressable Hidden Bar drawers. The Hidden Bar used to be a single per-bar drawer (one !HiddenBar pill rendering that bar's one hidden_widgets list), so you couldn't place two drawers with different contents. You now can: define named drawers under bars.widgets.hidden_bars (each with its own widget list + behaviour) and drop as many as you like into any bar slot — they show up in the "Add widget" menu as Hidden Bar · <name> (the !HiddenBarNamed <name> reference pattern, mirroring !Custom). Settings → Widgets → Hidden Bar gains a Named drawers editor (add / rename / delete, each with its own add-remove-reorder widget list). The bare !HiddenBar pill is unchanged and keeps working as the bar's default drawer — zero config migration. mshellctl hidden-bar <verb> now takes an optional drawer name: mshellctl hidden-bar toggle media toggles just that drawer, while the name-less form still reaches every drawer.
  • mshellctl menu margo-layout toggles the Margo Layout Switcher menu from the CLI. The pill + menu already existed (bar widget + Settings → Widgets) but had no IPC verb, so it was the one bar menu that couldn't be opened from a keybind / script — now wired through the same com.mshell.Shell path as every other menu.
  • The OSD capsule is now configurable (Settings → OSD). The volume / brightness / mic / network pulse used hardcoded geometry (280 px wide, bottom-centre, 48 px up, fully-pill corners, no border). A new osd config section + Settings page (under Appearance) exposes width, position (top / bottom / left / right / centre), distance from the edge, corner radius and border thickness — the capsule now ships with a 2 px border in the matugen --outline colour (the same role the compositor paints idle window borders with, so the OSD ring matches the window border). Width / radius / border drive live --osd-* CSS vars (update instantly); position / distance anchor the layer-shell windows and apply on the next mshell restart (the screen-corner contract). Border colour stays matugen-driven, not a setting (DESIGN.md).

Changed#

  • Login no longer triggers a config-reload storm. The General settings profile DropDown fires connect_selected_notify on its own programmatic set_selected during init (and again from the active/available-profile effects), each time re-selecting the already-active profile. Every such call ran a full set_active_profile → reload_config: a fresh figment merge
  • migration pass + full deserialize, then a reactive-store patch that re-ran every config effect across both bars and all menus — and that cascade in turn re-triggered the theme effect (a second matugen palette subprocess) and a profile re-write that the file watcher bounced back as yet another reload. At login this fanned out to ~4 reloads + 2 watch-loop reloads + a redundant matugen run, all inside the startup CPU spike. set_active_profile now early-returns when the requested profile is already active, collapsing the whole cascade.
  • The config file-watcher no longer reloads on mshell's own writes. The profile dir is watched with notify, and persist_config_layer's atomic rename fires that watcher — so a Settings write (or a startup re-persist) bounced straight back as a full reload. After the set_active_profile storm fix this was the remaining two reload cascades at login. The watch loop now compares the reloaded config to what's already live and skips the reactive-store patch (and its effect cascade across both bars and all menus) when they're identical — exactly the self-write case. A genuine external edit still differs and reloads.
  • Config writes no longer round-trip through disk. Every Settings toggle / slider step called update_config, which persisted the profile and then reload_config'd it — rebuilding a default Config, re-reading and re-parsing the just-written YAML, running the migration pre-pass (another read), and re-deriving the plugin-widget layer. It now writes through: the in-memory config is patched straight into the reactive store (after re-adding the plugin layer), so a full synchronous reparse is gone from the UI thread on every change.
  • Twilight stops re-reading its presets every tick. In schedule mode tick_twilight re-read and re-parsed all seven preset files on every tick — as often as every 50 ms during a sunrise/sunset sweep. The parsed schedule is now cached on the compositor state (keyed by the schedule dir) and invalidated only on config reload and mctl twilight commands, so steady-state and sweep ticks do zero preset I/O.
  • Menu placement dropped its positional tuple. The per-output reposition signal carried a 17-field (Position, …) tuple that had to be hand-aligned across five call sites (adding or removing a menu meant editing all of them). It's now a unit signal; the placement code reads each menu's position from config — the pattern the dashboards already used.
  • New opt-in dist build profile (fat LTO + a single codegen unit) for a faster, smaller shipped binary, without slowing the release dev loop. Build with cargo build --profile dist.
  • Only the uwsm-managed Wayland session is offered at the login chooser. The packages previously shipped two session entries — margo.desktop (Exec=margo) and margo-uwsm.desktop. Picking the plain one from a display manager brought up a bare compositor with no shell: nothing activates the systemd graphical-session.target, so mshell.service (and the rest of the user session) was never pulled in, which read as "margo is broken". uwsm is a hard dependency and is what wires up that target, so the plain entry is now installed under doc/.../sessions/margo-bare.desktop (for manual / no-systemd launching) rather than as a session. install.sh also now installs the uwsm session + wrapper scripts + uwsm dependency, matching the Arch PKGBUILD.

Fixed#

  • No more surface missing from known popups ERROR spam in the logs. smithay adds a permanent per-wl_surface pre-commit hook for every xdg popup and never removes it, so when a client dismisses a menu (destroy the xdg_popup, then one last commit on the still-alive surface — the normal GTK/ Qt teardown) the orphaned hook fires, fails its known_popups lookup, and smithay logs an ERROR for a harmless race. These "missing from known {popups,toplevels}" lines are the only error!s in smithay::wayland::shell::xdg (confirmed against both the pinned rev and upstream HEAD — bumping smithay does not change them), so margo-logging now drops ERROR-level events from that module while keeping its warn/info/debug.
  • Active-window title marquee no longer wakes the main loop forever. It armed a 30 ms timer for the widget's whole life — ~33 wakeups/second per monitor — just to early-return whenever the title fit. It's now driven by the scroller's changed signal, so the timer exists only while the title actually overflows.
  • smartgaps no longer clips a lone tiled window's border on any edge. Window borders are drawn OUTSET — render_element_for_client grows the border rect by borderpx beyond the window geometry on every side — so the content must sit far enough inside the work area for the whole border ring to land within the monitor. smartgaps collapsed the outer gaps to 0, leaving the lone window flush against the work area and its outset border spilling past the screen. Clamping the gaps to borderpx (an earlier pass) made the border's outer edge land exactly on the work-area edge: fine top/bottom (the work area is already inset there by the bar + bottom slack) but on a full-width tile the left/right border then sat exactly on the flush monitor edge — no side bars — and read as clipped off-screen (confirmed against live geometry: a 2560-wide output with the lone window at x=2 w=2556, its 2 px border landing at 0–2 and 2558–2560, glued to the bezel). smartgaps now clamps the outer gaps to 2 * borderpx, so the outset border keeps a full borderpx of clearance inside the work area on all four sides — visibly inset on left/right too, matching top/bottom. (As a related tidy-up the drawn connected-frame band is also pinned to the bar's reserved height rather than its allocated container height, so the painted frame can't overhang work_area either.)

Removed#

  • Unused dependencies pruned from ten crates (verified with cargo-machete plus a per-crate source grep — e.g. an 84-line stub crate was carrying nine).

Tests#

  • Criterion benches for the two hottest pure-logic paths: layout arrange (margo-layouts) and config parse (margo-config), as regression shields.

[1.0.6] – 2026-06-13#

An internals pass — both compositor god-files (state.rs + udev/mod.rs) split back down, safe config migration, first-login bootstrap coverage, a justfile dev loop, fixed silent notification sounds — plus a batch of user-facing work: Kenp is the new default colour scheme, a mshellctl theme CLI, mctl doctor / mshellctl doctor health checks + shell completions, the Catwalk pill gains the RunCat cat + a CPU-% readout, the monly single-window-maximise knob, a redesigned CPU dashboard, a branded default wallpaper, and mdash — the dashboard, rebuilt with a greeting header and a menu-shortcut grid (it replaces the old dashboard outright).

Fixed#

  • mdash no longer reverts to a stale layout after a rebuild. Because the shell rewrites the entire resolved config to the active profile on any change, the serde-default menus.mdash_menu got baked into the profile on first write and that stale copy then shadowed every later code-default change (the button grid kept reappearing as an old 4-row layout). The field is now skip_serializing_if it still equals the code default, so it stays out of the written profile until actually customized — future default changes win, and user edits still persist.
  • About → Uptime no longer frozen at login. The About page is built eagerly at startup, so its system info (uptime in particular) was read once and never refreshed. It now re-reads every time the page is shown.
  • Launcher selected-row accent matches the clipboard. The launcher used a full --primary flood while the clipboard used a soft --primary-container; both now use the same soft container fill (DESIGN.md §14 restraint).
  • Notification sounds now actually play. rodio is built vorbis-only (features = ["vorbis", "playback"], no WAV decoder), but the two notification chimes shipped as .wav — so symphonia probed each to EOF, logged probe reach EOF at 61784 bytes, and the chime was silent. Converted notification.wav / notification-critical.wav to OGG/Vorbis (matching every other shipped clip) and added a mshell-sounds test that decodes every embedded clip with the configured features, so a future .wav fails CI instead of going quiet in production.

Changed#

  • Twilight's seeded preset curve reshaped. The bootstrap default was a near-flat warm band (2150–2500 K) whose coolest point landed at 07:30 and barely dimmed at night. It's now a proper day curve within the same warm range (2200–2650 K): a warm+dim deep-night trough (2200 K / 86 %), a cool+bright midday peak (2650 K / 100 % late-morning), easing back down through the evening. Schedule times unchanged; only new installs are affected — existing ~/.config/margo/twilight/presets/ files are user-owned and untouched.
  • Kenp is the new default colour scheme. The dusk palette that briefly shipped as "Eventide" — bioluminescent teal primary, twilight-violet / amber / rose accents over deep blue-violet surfaces — is promoted to the house theme and renamed Kenp; the previous Kenp / Kenp Light themes are removed. The Default alias now resolves to Kenp (the Margo brand palette stays selectable), so a fresh profile, an absent theme key, and theme: Kenp all land on Kenp. A config v1 → v2 migration auto-rewrites any profile still selecting the dead Eventide / KenpLight variants onto Kenp (an unknown enum value would otherwise reset the whole profile to defaults).
  • CPU dashboard redesigned. A per-core heat-grid (calm → warn → danger buckets), a gradient sparkline, and a refined hero readout.
  • state.rs split back under 3k (4045 → 2441 lines). The compositor's central file regrew past its Phase-2 <3000 target; the window/tag-rule + placement cluster, the tiling-arrange cluster (incl. the ~526-line arrange_monitor), keyboard-focus + pointer-monitor methods, and DPMS + monitor enable/disable moved into sibling impl MargoState modules (state/{window_rules,arrange,focus_methods,dpms}.rs), and apply_theme_preset sits beside its ThemeBaseline in state/theme.rs. Pure lift-and-shift — no behaviour change.
  • udev/mod.rs split (3868 → 1654 lines). The last compositor god-file: the entire render-element construction cluster (RenderTarget, build_render_elements[_inner], cursor / MRU-switcher / scroller-overview overlays, per-client/per-layer push helpers, screencast/image-copy frame drains — ~2200 lines) moved into backend/udev/render_elements.rs; mod.rs keeps backend setup, capture queueing, and serve_screencopies. Pure lift-and-shift.
  • Network change OSD moved to Settings → Network. The "Connection change OSD" toggle lived under Settings → General; it now sits with the other connection settings on the Network page. Config key unchanged (general.network_osd_enabled), so existing profiles and the OSD itself are unaffected — UI location only.
  • Settings + bar boilerplate consolidated. A build_pages! macro collapses the 47 hand-written settings-page controller builds into one declarative list (the page stack + sidebar were already table-driven); the three bar-slot rebuild guards fold into one BarModel::rebuild_slot helper. The mute wire-encoding (0/1/else = off/on/toggle) is now one mute_target helper shared by the output + mic IPC arms instead of two inline copies.

Removed#

  • The classic dashboard menu + pill are gone — mdash is the only dashboard now. Removed BarWidget::Dashboard, MenuType::Dashboard, the dashboard_menu config field, the MenuKind::Dashboard settings page, and the mshellctl menu dashboard verb. The bar pill widget moved to bars/bar_widgets/mdash.rs (MdashModel) so the bar surface is mdash-owned rather than borrowing the old dashboard pill. The default Super + G keybind and the shipped margo profile now point at mdash; a leftover dashboard_menu: block in an existing profile is harmlessly ignored. (cpu_dashboard / audio_dashboard are separate menus and unaffected.)

Added#

  • mshellctl theme — switch the colour scheme from the terminal. theme list [--names-only] / theme get / theme set <name>. set writes the same reactive config field the GUI picker does, so it applies live (no mctl reload, no restart); name matching ignores case and -/_/space separators. The top-level mshellctl --help command descriptions also got a consistency pass.
  • Catwalk: the RunCat cat + a CPU-% readout. A style toggle picks the bundled RunCat sprites (5 run frames + a sleeping pose) or the original noctalia cat; a display mode shows the cat, the CPU %, or both (the % severity-coloured); sprite size and CPU poll interval are configurable; the walk cadence uses the RunCat easing and the tooltip narrates the cat's mood. Ports the best of the Plasma / DMS CatWalk widgets.
  • monly — maximise the only tiled window of a tag (a native port of the niri tool oniri). When a tag holds exactly one tiled window, it's arranged as Monocle regardless of the active layout, so a lone window fills the work area even in column layouts like scroller. Off by default; pairs with smartgaps. Toggle in Settings → Appearance.
  • Profile schema versioning + stepped migration (mshell-config). A new config_version file-format meta key with a migrate_yaml load pre-pass that brings an older profile up to the current format and writes it back once (idempotent), and a save-side stamp. The framework makes the next config rename/reshape a one-step, round-trip-tested change instead of a silent "works-on-my-fresh-config" bug. v0→v1 is the versioning baseline; v1→v2 is the first real transform (the dead-theme-variant rewrite above). 9 round-trip tests.
  • justfile for the dev inner loop. One source for build+install+restart: just shell / just margo / just cli / just all, just reload (mctl reload — config only, never a binary swap), and just check (the exact pre-push CI gate). Complements ./install.sh (the packaged installer).
  • mctl doctor — one-shot compositor health check. Unifies the scattered diagnostics: control-socket reachability, on-disk config validation, the running compositor's rejected lines, margo↔mctl version sync (catches "installed a new margo but haven't re-logged in"), GPU render nodes, matugen, and the mshell / portal D-Bus services. Prints a ✓ / ⚠ / ✗ line per check and exits non-zero on error, so it doubles as a scriptable smoke test. The compositor snapshot now carries margo_version to feed the version check.
  • mshellctl doctor + mshellctl completions. The shell-side health check (session-bus reachability, com.mshell.Shell ownership, mshell↔mshellctl version sync — backed by a new Version D-Bus method), plus clap-generated shell completions (bash / zsh / fish / …) matching the existing mctl completions.
  • Branded default.jpg wallpaper. A clean dark desktop default mirroring margo-hero: the brand mark (coral bars + dark inner block) in the bottom-left corner with a soft halo and the "margo" wordmark beneath. This is the file the compositor + mlock fallbacks already resolve when no wallpaper is set.
  • mdash — the dashboard, rebuilt. Replaces the old dashboard menu entirely. Opened with mshellctl menu mdash, the default Super + G keybind, or the Mdash bar pill (a clock-style tempo pill — its own MdashModel widget). A fuller layout: a time-aware greeting header ("Good evening, " via a new PanelHeader { greeting } flag), the two-pane calendar/weather + intel/connectivity/audio/system/media body, and a two-row action grid packing the toggles, a 14-button menu-shortcut grid, and the power actions. The shortcut buttons are new QuickActionWidget menu-launchers (Network, Bluetooth, CPU/Audio dashboards, VPN, Control Center, Twilight, Keybinds, DNS, Power, Session, IP, Alarm Clock, System Update) — each opens the matching menu and closes mdash; they're also pickable for any menu's quick-actions in Settings. mdash is fully editable under Settings → Widgets → Mdash, including a Buttons editor — an add / remove / reorder list of every quick action that treats mdash's two physical button rows as one flat list and re-splits it evenly on change (leaving the header / calendar / weather widgets untouched). Backed by a serde-default menus.mdash_menu config.

Tests#

  • First-login config bootstrap is now covered: ensure_default_config writes a complete, parseable config.conf + starter binds.conf + conf.d/colors.conf on an empty dir and never clobbers existing files; seed_bundled_profile seeds the profile once and preserves a customised one.
  • mshell-core mute encoding unit-tested via the new mute_target helper.

Docs#

  • Roadmaps de-drifted: road_map.md / 1.0-readiness.md current-status bumped to v1.0.6; code-quality-roadmap.md marks the state.rs split, config versioning, and the settings/bar boilerplate items done/partial.
  • Roadmap accuracy: P12 (zwlr_output_power_management_v1, Wayland DPMS) marked shipped (1.0.2) rather than deferred; the Twilight in-Settings preset editor marked won't-do (Settings → Display → Twilight already covers it).

[1.0.5] – 2026-06-12#

The design-language release: one central geometry source, GNOME metrics everywhere.

Changed#

  • GNOME/libadwaita visual overhaul. The whole shell moved to Adwaita metrics, driven from a new central component-token layer (01-tokens/_components.scss) — buttons pull shape, size and colour roles from one place (--button-*, --row-*, --card-*, --entry-*). Radius scale 10/16/20/28/32 → 6/9/12/15/18 (Adwaita button/card/window); every button is now 9 px / ≥34 px with size equality across a row; the pill shape is reserved for switches, progress, chips, the clipboard panel search and prominent CTAs (action buttons are never pills anymore — power/session/DNS rows demoted). New shared boxed-list primitive (hairline-separated grouped rows, Adwaita ActionRow anatomy) adopted across ~45 Settings pages; compact searches (settings sidebar, keybinds, SSH) dropped from pill/xl to the Adwaita entry corner; control-center tiles, dashboard tiles and mdock take the card/window corners. DESIGN.md §0/§1/§5/§12/§21/§22 rewritten to the new language (incl. stale doc↔token drift), with the design-lint CI gate unchanged and green. Spec: docs/superpowers/specs/2026-06-12-gnome-visual-overhaul-design.md.
  • Standalone binaries follow the same metrics. mvpn's layer-shell panel (embedded CSS) and mkeys' key buttons align to the Adwaita scale (window 15 / card 12 / button 9 px · 34 px targets), and the theme/bar/menu/network-editor Settings sub-pages got the same boxed-list rows as the main pages.

[1.0.4] – 2026-06-12#

A notification-center deepening on top of housekeeping: notifications go from "informing" to "acting" — reply from the toast, hear it, watch it progress, find it later. Plus ledger sync, Settings polish, and two new CI gates.

Changed#

  • mvpn fav got pick numbers. fav list (and fav refresh) now number the favorites; fav connect 2 connects to the 2nd entry (relay names still work); bare fav connect on a terminal shows the numbered list and asks (Enter = fastest, q = cancel) — when piped or bound to a key it keeps the old "fastest favorite" behaviour.
  • Settings panel polish. Sidebar nav rows tightened (38 → 33 px, 2 px list spacing, group headers pulled in and aligned to the icon column; the search separator gained breathing room). The Widgets sub-sidebar now fits its longest names ("Screen Recording", "System Bluetooth") via a 216 px column + density overrides instead of ellipsizing. Page descriptions (label-small, action-row subtitles, hero subtitle) are re-toned with a palette-proof surface blend (color-mix 62–70 % toward the page surface) so section titles carry the contrast even on schemes where --on-surface-variant equals --on-surface; boxed-list cards gained a 1 px hairline edge for low-contrast palettes.

Added#

  • Inline reply (KDE-style). Notifications that carry an "inline-reply" action (chat apps, Valent SMS, KDE apps) now render a reply entry right on the popup toast; Enter or the send button emits the NotificationReplied(id, text) signal back to the app. The daemon advertises the inline-reply capability so clients light the feature up. Implemented by vendoring wayle-notification 0.1.3 with a contained, upstreamable extension (vendor/wayle-notification + [patch.crates-io]); the popup layer-shell surface switches to on-demand keyboard so the entry can type. Toggle: Settings → Widgets → Notifications → "Inline reply".
  • Notification sounds. A synthesized in-tree chime (gentle two-tone; brighter rising tone for critical) plays when a popup appears — per-urgency toggles, an "app-provided sounds" switch honouring the spec's sound-file hint, the suppress-sound hint always respected, and a quiet hours window (wraps past midnight). Off by default; everything under Settings → Widgets → Notifications. The daemon now advertises the sound capability.
  • Progress bars. Notifications carrying the spec's value hint (downloads, transfers, backups) render a real progress bar on the popup and in the history; replaces_id re-sends now update the live toast in place (text + bar) instead of freezing the first frame.
  • History search. The notification history menu gained a search box filtering app name + summary + body as you type, with a proper "No matches" empty state.
  • CI design-lint gate (W1.7). New scripts/design-lint.sh enforces DESIGN.md §15 L1–L7 as hard gates in ci.yml: no hardcoded hex, no raw ≥4 px spacing/radius/font, --radius-widget only in bar/frame styles, no literal transition durations, no gtk::Popover as a bar-widget primary surface, no add_css_class(""), no DragSource/DropTarget row reorder. The pre-flip audit's three real hits (privacy-menu rows on the bar-pill radius token; two deliberate 5 px launcher paddings) were fixed in the same commit.
  • Plugin-host path-sandbox tests. The WASM tier's only filesystem boundary (resolve_scoped behind the read-file/write-file capabilities) moved into an unconditionally-compiled sandbox module with 10 unit tests (traversal matrix incl. ../absolute/CurDir/ percent-encoding cases, scoped write/read round-trip, no-escape guarantees) — they run on every cargo test --workspace, even in builds that never link wasmtime. No behaviour change.
  • CI panic ratchet. New scripts/panic-ratchet.sh + scripts/panic-baseline.txt gate in ci.yml: the number of .unwrap() / .expect() / panic!() calls in non-test code (334 at seed) can only go down. A rise fails CI as a regression; a drop fails too until the baseline is lowered, so every cleanup is locked in.

Fixed#

  • Mic no longer jumps to 100 % after mshellctl audio switch-mic. With "Restore volume on startup" enabled, the configured default output/input level is now re-applied to the newly promoted device after every audio switch / switch-mic, not just at login — WirePlumber carries each device's own last volume, so a switch could land on a 100 % mic even though Settings → Sound pins 50 %.
  • Settings → Network: wired "Edit connection" button now works. It was a stub since the connection-editor task landed — it sent an empty UUID and just toasted "no connection UUID". It now resolves the ethernet profile (active one preferred, falling back to any saved ethernet profile) and opens it in the embedded connection editor; the toast only remains for the genuine "no wired profile exists" case.

Documentation#

  • Roadmaps + readiness ledgers synced to v1.0.3 reality. road_map.md header/TL;DR updated (1.0 shipped 2026-06-09; protocol score 14/17 → 15/17 with output_power; socket-IPC supersession note on the dwl-ipc section). road_map_mshell.md finally marks A2/A3/A4/A5/A8/A9/B4/B5/B9 and D3/D5/D6/D7/D9 as shipped, replaces dwl-ipc-v2 references with the Unix control socket, and ledgers the beyond-roadmap additions (control center, AI assistant, mvpn, mkeys, mplay, dock, …). docs/1.0-readiness.md got a "Resolved — 1.0 shipped" banner; docs/protocol-comparison.md re-audits margo's column at v1.0.3 (~60 globals); docs/code-quality-roadmap.md metrics refreshed (765 test fns; state.rs regrew to 4045 — flagged as the next ratchet).

[1.0.3] – 2026-06-09#

A VPN + window-switcher release: a native Mullvad VPN tool (CLI + GTK panel + bar pill + Settings page) and a niri-style most-recently-used Super/Alt+Tab window switcher with a live thumbnail overlay.

Added#

  • mvpn — native Mullvad VPN control. A new standalone binary: a full CLI (connect/toggle/<cc>/fastest/fav/obf/slot/timer/test/…) and a GTK4 layer-shell control panel (mvpn menu) themed from the matugen palette. Ports the favorites (ping-sorted), fastest-relay, obfuscation (anti-censorship), device-slot (multi-machine), blocky DNS-guard, timer and leak-test logic natively in Rust — file-compatible with the existing ~/.mullvad/{favorites,slot.state}. Honours the OSC_MULLVAD_* env overrides. Replaces the external mullvad WASM plugin.
  • Native "DNS / VPN" bar pill + in-shell menu + Settings → VPN page. One widget-picker-selectable BarWidget (status icon, accent-tinted when up, right-click → toggle). Left-click opens a native layer-shell menu (not a separate process): the full Mullvad control set — Connect/Disconnect, Random, Fastest, Add-favourite, Lockdown, Auto-connect, Quantum-resistant, anti-censorship, favourites list — plus a collapsible DNS section carrying the Blocky guard, system-default reset, and the DNS presets. The standalone DNS pill is folded into this one (old Dns bar configs migrate automatically). The Settings → VPN page mirrors the controls, reading live state via mvpn toggles. mshellctl menu vpn toggles the combined menu from a terminal, and Settings → Widgets lists separate VPN (the combined menu) and DNS (the standalone mshellctl menu dns menu) entries for position/size tuning.
  • Native AI assistant. The assistant-panel WASM plugin is now a first-class core feature: a new GTK-free mshell-ai engine (Gemini / OpenAI / Anthropic / Ollama / Custom, token-by-token streaming, API key in the keyring) with live model discovery — pick a provider and the model dropdown auto-offers its models (fetched from the provider's list-models endpoint, with a curated fallback), no hand-typing. A Settings → AI page (provider → model cascade + Refresh + endpoint / temperature / tokens / system prompt), an AI bar pill, a native streaming chat menu (bubbles, Stop / Retry / New / Copy, persisted history), and mshellctl menu ai.
  • MRU window switcher (niri-style Super/Alt+Tab). Hold the modifier, tap Tab to walk windows in most-recently-used order, release to commit. A live thumbnail-row overlay (scope title + per-thumb app-id labels), separate from the grid overview. The row is a carousel — the selected window's thumbnail is centred on the output and the strip scrolls as you cycle. Dispatch mru_next / mru_prev (arg.v = scope, arg.v2 = filter); knobs mru_thumb_height / mru_scope / mru_filter / mru_show_labels, also editable under Settings → Overview.

Fixed#

  • VPN menu redesigned (DESIGN.md): a segmented Mullvad / Blocky / Default mode selector on top (active mode accent-filled), with Favourites / Countries / DNS-presets as collapsible sections.
  • mvpn fastest now genuinely finds the fastest relay. It used to ping a random subset of 8 relays, so a ~10-relay country could miss the actual fastest (connecting to a 350 ms relay over a 60 ms one). It now pings every relay in the requested country (matching osc-mullvad), tries them fastest-first, and prints each result; fastest no longer touches favorites while fastest-fav saves the winner.
  • mvpn desktop notifications. connect / disconnect / toggle / random / fastest now raise a notify-send toast with the resulting relay + location (silenceable via MVPN_NO_NOTIFY).
  • The Mullvad pill opens the shell's own native layer-shell menu instead of spawning the standalone mvpn menu popup, so it matches the other menus' chrome and DESIGN.md exactly.
  • A standalone DNS bar pill is available again in the widget picker (alongside the combined VPN pill) — it opens the mshellctl menu dns menu.
  • The VPN pill shows the connected country beside the shield; the menu's favourites list marks the currently-connected relay active ("Connected" + accent); and a small account-expiry line sits under the DNS section.
  • The VPN menu has a collapsible Countries picker (every Mullvad country + relay count, from mvpn countries; per-row Connect runs mvpn <cc>), and the favourites' Connect buttons now connect to that relay (mvpn fav connect <relay>) instead of always the fastest favourite.
  • Changing a widget's position/config no longer flashes the clock menu over whatever menu is open: the per-region stacks' visible child is now preserved across the menu restack.
  • mvpn Protocol button repurposed to a working WireGuard quantum-resistance toggle (modern Mullvad removed relay set tunnel-protocol).
  • mvpn menu is a real layer-shell panel, not a floating "popup", and Esc closes it. Driven by a raw GLib main loop (no GtkApplication, whose window management forced an xdg-toplevel) with exclusive keyboard. The surface is translucent like the shell's native menus (matugen menu opacity).
  • MRU switcher: genuine most-recently-used ordering (cycling no longer rewrites the order) and the stuck-modifier bug (the Alt/Super release now reaches the focused window, so it isn't left "held"). Thumbnails are off-screen snapshots taken before render, so every window (even on other tags) shows a real preview from the first frame; the backing band is preview-sized and scrolls with the carousel.

[1.0.2] – 2026-06-09#

A compositor-effects + bar-consistency release: opt-in dual-Kawase blur, Hyprland-style tabbed window groups, a per-output frame clock, and a single bar-pill standard that finally makes every widget (and plugin) render with the same surface, hover, height, and shape.

Added#

  • Dual-Kawase background blur (opt-in, default off). blur / blur_layer blur behind translucent windows / layer-shell surfaces; strength + radius are tunable from Settings → Effects (blur_params_num_passes, blur_params_radius) and per-window/-layer noblur:1 rules.
  • Tabbed window groups (Hyprland-style, opt-in). togglegroup, changegroupactive, movegroupwindow, movewindowtogroup, lockgroups merge windows into one tile with a tab strip. The strip shows each member's app-name label (rendered with fontdue) on rounded, matugen-tinted chips; height/colours via group_bar_height / group_active_color / group_inactive_color.
  • Per-output frame clock (opt-in). per_output_frame_clock paces each monitor by its own refresh so a 60 Hz panel can't hold back a 144 Hz one.
  • mctl plugin subcommand. list / enable / disable for compositor Rhai plugins.
  • Manual frame colour override. Settings → Bar → Frame can pin the bar frame fill + border to fixed colours instead of the matugen palette.
  • Setup-wizard polish. Prominent nav + in-page action buttons, a default-shortcuts cheat-sheet on the Review step, and centred footer nav.
  • Configurable bar separator colour. Settings → Bar can override the Separator widget's colour (separator_color) instead of matugen --outline.
  • Panel power (DPMS) off/on. A dpms on|off|toggle [output] dispatch action truly powers monitors down (via smithay's DrmCompositor::clear()) and back up — real power saving, not just a dim. Recovery is guaranteed two ways: any input wakes a darkened panel, and a VT-switch round-trip always restores every output (both force the all-outputs render path so a stalled per-output clock can't keep a panel dark). The input that wakes a darkened panel is swallowed, so the keystroke that turns the screen back on no longer lands on the focused window (no stray newline in the terminal you ran mctl dispatch dpms off from).
  • External DPMS control (zwlr_output_power_management_v1). Idle daemons (swayidle) and wlr-randr --off/--on can now power outputs down/up; their set_mode maps onto the same recoverable request_dpms path, and mode events track every power change (whoever triggered it).
  • Example compositor plugin. Ships app-workspaces under margo/examples/plugins/ (app-id → home tag on open) and documents the full plugin.toml + init.rhai format + mctl plugin workflow.

Changed#

  • One central bar-pill standard (.bar-pill-std). build_widget tags every pill — native widgets AND plugin pills — with a single class + centre alignment, so they all share the same surface, matugen hover wash, height, and rounded shape (no more per-widget drift or full-width ballooning). Adjustable from one place.
  • Wizard's full starter profile renamed Nova → margo (lowercase, brand-consistent); the minimal one stays default.

Fixed#

  • Blur shimmer. Forcing a full redraw per frame while blur is on stops the stale-backdrop flicker from age-based damage tracking; the composite quad is now positioned via a deterministic viewport→NDC mapping.
  • Tabbed-group glitches. The tab strip reserves its height inside the tile (title no longer hides under the bar / eats the gap), and hidden members are pre-sized so changegroupactive doesn't flash the wallpaper.
  • Flatpak / sandboxed (security-context) clients no longer crash the compositor on launch.
  • External-monitor hotplug now allocates a scanout-capable buffer so a re-plugged display is detected.
  • Bar separator stays visible in every mode again (the new central pill standard had painted the Separator/Spacer helpers transparent); the frame Fill/Border colour pickers no longer drift apart in Settings → Bar.
  • mctl plugin list no longer errors when there's no ~/.config/margo/plugins/ directory — it explains the difference between packaged compositor plugins, auto-loaded init.rhai, and shell plugins.

[0.9.8] – 2026-06-04#

A shell-polish release: a walker-class app launcher, a much richer clipboard (CLI + smart typing), notification timeout bars, and Control Center additions — plus a second-generation config parser and an expanded setup wizard.

Added#

  • App launcher preview pane. mshellctl menu app-launcher gains a detail pane beside the results (text / monospace / colour swatch), per-provider rich rows + type classes, and comfortable density. Settings → Launcher → Appearance toggles cover the preview pane, compact rows, and large app icons.
  • mshellctl clipboard CLI. Headless access to the clipboard history — list [--json], copy, pin/unpin, delete, clear, wipe — driving the same store as menu clipboard.
  • Smart clipboard content typing. Entries are classified as URL / colour / code / email (plus text/image), with per-row type icons and a clipboard.image_max_kb cap; every knob is exposed under Settings → Widgets → Clipboard.
  • Notification timeout bar. Popup toasts show a shrinking bar counting down their on-screen time, paused while hovered. Toggle + duration in Settings → Notifications (show_timeout_bar, popup_duration_ms).
  • Control Center: power-profile control + battery chip. A Saver / Balanced / Performance segmented control above the sliders and a battery pill in the header, both toggleable from Settings → Widgets → Control Center → Sections.
  • Session widget menu controls. Settings → Widgets → Session gains the standard menu size & position section.
  • Second-generation config parser diagnostics. "Did you mean" suggestions for unknown keys, incomplete-bind (E004), modifier / enum validation (E005/W002), and scalar type-checking (W003).
  • Expanded setup wizard. Hardware-aware steps + Display / Power / Night-light pages, review-edit, keyboard navigation, and the margo logo on the Welcome step.

Changed#

  • Control Center SCSS moved fully onto tokens; volume / mic / brightness sliders gain a sensible scroll step.
  • ~/.config/margo reorganised toward ~/.cachy/modules/margo (dotfiles-managed) with binds.conf split out of config.conf.

Fixed#

  • App launcher Ctrl+N/K navigation no longer triggers a display-wide restyle per keystroke; dropped the unsupported GTK4 gap / cursor CSS that produced theme-parser errors.
  • Honour $MARGO_SOCKET in the compositor IPC socket path.
  • Dropped the removed mshelldash menu verb from the shell completions and docs.

[0.9.6] – 2026-06-03#

A Bluetooth + overview release: a native auto-connect engine that retires the external scripts, a configurable scroller-overview backdrop, and a fix so the scroller overview renders windows on never-visited tags correctly.

Added#

  • Native Bluetooth auto-connect + audio routing. Replaces the external bt-autoconnect.service + bt-autoconnect-once + bluetooth_toggle scripts (and the F10 binding) with an in-shell engine. Settings → Bluetooth gains an Auto-connect section: a master switch, a post-login delay, an ordered MAC device list (drag to reorder; first that connects wins), "use as audio output" / "use as microphone" routing toggles, and connect/disconnect notifications. At login the shell waits the delay then connects with a few retries and routes audio to the device; mshellctl bluetooth toggle | connect | disconnect (smart toggle: power-on + connect, or disconnect if already connected) backs a keybind and the Settings "Toggle now" button. The mic toggle is off by default — forcing a headset mic drops the codec to HSP/HFP and degrades playback.
  • Bluetooth menu redesign. The opened menu groups devices into Paired / Available, sorts connected devices first, shows a spinner while a connect/disconnect is in flight, and adds a ★ pin to add or remove a device from the login auto-connect list. Device name + battery text bumped to a larger, legible size.
  • Configurable scroller-overview backdrop. Settings → Overview gains a Backdrop section: pick a solid colour (with alpha) or an image (cover-fit) painted behind the tag cells. Both apply live; a missing image path falls back to the solid colour.
  • Per-script autostart trigger. Settings → Launcher startup scripts can run on every shell start or login only (once per session), set per script.
  • Per-bar enable + explicit auto-hide. The top and bottom bars each get their own enable switch and a configurable auto-hide delay.
  • Project-ethos note on the About page.

Fixed#

  • Scroller overview shows un-visited tags correctly. Windows on tags you hadn't switched to yet rendered crammed at their stale map-time position and size until the tag was visited once. The overview now pre-arranges every off-screen tag it will show (layout geometry + a resize configure) and feeds those off-space windows frame callbacks while it's open, so frame-throttled clients (GTK, Electron) repaint to their slot size from the first open.
  • SCSS edits always rebuild the baked stylesheet. mshell-style's build script now emits a per-file rerun-if-changed for every nested SCSS partial, so editing a component partial no longer bakes a stale stylesheet into the binary until an unrelated file change forces a rebuild.
  • Startup-scripts list height grows with content (capped, then scrolls) instead of cramming into a tiny box.

[0.9.5] – 2026-06-02#

A shell polish release: drag-to-reorder across Settings, a frameless "floating panels" bar mode, a batch of post-resume / log-noise fixes, and DESIGN.md turned into an enforceable quality gate.

Added#

  • Drag-to-reorder in Settings. Every reorderable list — bar-widget sections, menu-widget lists (incl. nested containers), quick actions, and Control-Center tiles — now has a ≡ grip handle you can grab and drag to reorder, alongside the existing ↑/↓ buttons (kept for keyboard/accessibility). A live .drop-target indicator highlights the landing row as you drag. Implemented with a shared GestureDrag helper (reorder_dnd), not GTK DnD — GtkListBox swallows drag-and-drop motion/drop before rows see it.
  • Frameless bar mode. Turning off Settings → Bar → "Enable frame drawing" no longer leaves the bars/menus transparent: each paints its own opaque, themed, rounded surface, so the shell reads as discrete floating panels (matugen-tracked).

Changed#

  • Islands bar toggle applies live — flipping Settings → Bar → Islands no longer needs a shell restart.
  • UFW bar poll no longer shells out to sudo. The pill/tile state comes from a privilege-free systemctl is-active ufw.service; the full rule list is fetched (sudo/pkexec) only when the menu is opened. Poll interval relaxed 120 s → 300 s. Eliminates the per-poll sudo/PAM journal spam (and the /usr/bin/ufw stdout leak from the which probe).
  • DESIGN.md is now an enforceable quality gate (§15 lint rules with grep recipes, §16 component state matrix, §17 async states, §18 reuse registry, §19 surface decision tree, reorderable-row + positioning standards). Doc only.

Fixed#

  • Shell froze for 1–2 min after suspend/resume. Synchronous state.json reads on the GTK main thread blocked while the just-resumed compositor drained its input backlog. Bounded with a 250 ms timeout so a busy compositor can never freeze the shell.
  • Window border lagged content on switch_proportion_preset resize — the resize snapshot is now held until the live buffer reaches the new slot size (or a grace ceiling), keeping border and content locked together on slow grows.
  • Stop a burst of harmless-but-noisy GTK assertions: control-center grid rebuild (gtk_grid_remove), menu scroller min/max content-size, and an empty-string CSS class on borderless menu-widget lists.
  • Setup wizard menu opens at a sensible fixed 640×720.

[0.9.4] – 2026-06-01#

Brings the user's external helper scripts in-house: a new first-party mplay binary, a synthetic-key sendkey action, and manual power-profile control.

Added#

  • mplay — native mpv companion (new binary). Replaces margo-mpv.sh:
  • Window control: start / toggle / play [URL] / download / snap (corner cycle) / pin (all-tags) / focus / stop, over mpv's JSON IPC socket + mctl.
  • Native video wallpaper: mplay wallpaper start <SRC> / stop — an in-tree mpvpaper port (wlr-layer-shell background surface + EGL + hand-written libmpv render-gl FFI), no external mpvpaper/uinput.
  • Smart media control: mplay media <toggle|play|pause|stop|next|prev| status> [player] — auto-detects the best active player across MPRIS (playerctl), MPD (mpc), and mpv; scoring + last-player memory + Spotify autostart + album-art notifications (osc-media.sh port).
  • Embedded yt-dlp shim (anti-bot fallback, cookies, browser UA) — no external yt-dlp-mpv script. optdepends: mpv, yt-dlp, playerctl, mpc.
  • sendkey dispatch action — inject a synthetic key combo into the focused window: sendkey,<combo>[,<appid-regex>][,<fallback>]. Forwards the keys via the seat keyboard (no ydotool/uinput/virtual-keyboard). Powers 3-finger touchpad browser tab-switching (ctrl+Tab / ctrl+shift+Tab, app-id gated, with a focusdir fallback) — replaces fusuma + fusuma-plugin-sendkey. Layout-independent keys only (Tab, Page_Up/Down, arrows, F-keys, …).
  • mpower cycle / mpower set <profile> — manual power-profile switching (e.g. on a keybind); the auto-profile daemon honours the manual change until the next AC transition.

Fixed#

  • mctl dispatch spawn multi-word args — spawn 'kitty -e htop' no longer drops everything past the first token over the socket.
  • IPC outbound back-pressure — a slow watch subscriber is buffered (bounded, with a WRITE source) instead of being dropped on the first partial write; the event loop never blocks or spins.
  • CI — drop debuginfo + incremental artifacts so the full-workspace build fits the runner disk.

Internal#

  • Large unit-test expansion (mplay controller/media/engine helpers, sendkey combo/regex parsing, IPC framing) — workspace suite well past 600 tests, clippy -D warnings clean.
  • Docs (site + README) refreshed for the socket IPC + the new tools; all config.conf comments translated to English.

[0.9.3] – 2026-06-01#

A from-scratch IPC rewrite: the legacy dwl-ipc-unstable-v2 Wayland protocol and the polled state.json snapshot file are gone, replaced by a single Unix-domain control socket.

Changed#

  • New socket IPC (replaces dwl-ipc-v2 + state.json). margo now serves a single newline-delimited control socket at $XDG_RUNTIME_DIR/margo/margo-ipc.sock (exported as MARGO_SOCKET) speaking get <topic>, watch <topic> (push-on-change), and dispatch <action> [args…]. mshell subscribes with watch state instead of inotify-watching a file, and mctl talks the same protocol — no Wayland client needed to script the compositor. Protocol documented in docs/ipc.md.
  • Breaking: the dwl-ipc-unstable-v2 protocol and the state.json file are removed entirely; external bars that read them need a socket adapter. The standard ext-workspace and foreign-toplevel-list protocols are unchanged.

Added#

  • mctl get <topic> / mctl watch <topic> — raw socket queries and change streams (topics: state, clients, client <id>, monitors, monitor <name>, tags <monitor>, focused, layouts, keyboard-layout, twilight, config-errors).
  • Dispatch actions settagset, setclienttags, setlayoutindex (plus the existing cyclekblayout), all reachable over the socket.
  • tag_carousel — wrapping a relative tag move past the first/last tag slides in the travel direction instead of reversing the long way.
  • edge_scroller_focus_allow_speed — in a scroller layout, a slow pointer resting at the leading/trailing edge shifts focus to the adjacent column (debounced; 0 disables).

Fixed#

  • Multi-word spawn over the socket — mctl dispatch spawn 'kitty -e htop' no longer drops everything past the first token.
  • Outbound back-pressure — a slow watch subscriber is now buffered (bounded to 4 MiB, with a level-triggered WRITE source) instead of being dropped on the first partial write; a subscriber past the cap is cut loose to reconnect. The event loop never blocks or spins on a stalled reader.
  • mctl get is now a real subcommand (was only used internally).
  • GTK4 CSS parser warnings — scrubbed the remaining unsupported properties (overflow, cursor, column-gap, @charset, -var() negation) from the shell stylesheet.

Internal#

  • Large unit-test expansion around the new IPC surface: request framing, outbound drain/back-pressure, topic projection (incl. error frames), dispatch arg mapping, tag-carousel + edge-scroller decisions, and the mctl socket client. Workspace suite at 565 tests, clippy -D warnings clean.

[0.9.2] – 2026-06-01#

Added#

  • Man pages for the core tools — margo(1), mctl(1), mshellctl(1) (hand-written roff under man/, installed to /usr/share/man/man1 by both the PKGBUILD and install.sh).

Changed#

  • CLI help polish — mshellctl now has a proper description + examples (was a bare "MShell CLI"), and margo --help lists all companion binaries with aligned columns plus a FILES section and man pointers.

[0.9.1] – 2026-06-01#

A wave of widgets ported from noctalia-shell v5, plus the removal of the mshelldash surface.

Added#

  • Keyboard Layout bar widget — shows the active xkb layout (e.g. US, TR); click cycles to the next configured layout. The compositor now tracks the active layout group (KeyboardHandle::with_xkb_state), publishes its name in state.json (keyboard_layout), and exposes a cyclekblayout dispatch action (mctl dispatch cyclekblayout). Configure multiple layouts with xkb_rules_layout = tr,us for cycling to do anything.
  • Audio Visualizer bar widget — a live spectrum strip driven by the cava CLI (raw/ascii mode); pulses with playback and shows a flat resting strip on silence. Degrades gracefully when cava isn't installed.
  • Keyword-aware Settings search — the Settings search now matches section keywords, not just page labels, so brightness finds Display, vpn finds Network, suspend finds Power, etc. (both Enter-to-jump and the live sidebar filter).

Removed#

  • mshelldash — the standalone tabbed dashboard surface and its mshellctl menu mshelldash verb were removed. The classic dashboard menu is unaffected. The Screen Time tracker that lived only inside it was removed with it.

[0.9.0] – 2026-06-01#

Added#

  • mpower — a native automatic power-profile manager (new binary + systemd --user service). Picks the power-profiles-daemon profile from live CPU load and AC/battery state (performance under load on AC, balanced / power-saver on battery), honours a manual override until the next AC change, and is fully configurable from Settings → Power → Automatic Power Profile and ~/.config/margo/mpower.toml. Replaces the external ppp-auto-profile; gated to margo sessions so it never fights another compositor's tool.
  • Hidden Bar bar widget — a collapsible drawer (native port of the DMS hidden-bar plugin) that hides a configurable group of pills behind a trigger: hover or click to reveal, right-click to pin, auto-collapse on leave. Settings → Widgets → Hidden Bar picks the contents + behaviour; mshellctl hidden-bar {toggle,expand,collapse,pin,unpin} drives it from keybindings.
  • Catwalk bar widget — a CPU-reactive animated cat (port of the noctalia plugin): idles below a CPU threshold, walks faster as load climbs; click opens the CPU dashboard. Configurable in Settings → Widgets → Catwalk.
  • Kenp colour scheme (dark + light) added to the static theme catalogue.
  • Power menu battery details — time remaining, power draw (W), health, capacity (Wh) and charge cycles, plus a charge-limit control (preset 60/80/100 + custom) via the kernel charge_control_end_threshold (ThinkPad/generic; set through pkexec). (Inspired by dms-framework-battery.)
  • Dock: middle-click launches a new instance, scrolling over an icon cycles that app's windows, and per-app icon overrides (dock.icon_overrides, class → icon name/path) fix apps started with a synthetic --class.
  • Media player: ± relative-seek buttons and an optional large album cover (the mplayerplus plugin folded into the native MediaPlayer widget); Settings → Widgets → Media Player.
  • Per-tag default tiling layout (taglayout / taglayout_force config + Settings → Tiling Layout page with a per-tag override editor); the compositor seeds them and re-applies on reload.
  • Plugins: plugin-declared keybinds with conflict resolution + Settings → Plugins → Keybinds, a panel-archetype design language with a family of plugin-* style classes, mshellctl plugin list, MPRIS player name exposure, and shell completions.
  • Docs: a complete configuration guide (all layouts + the full dispatch action catalogue), a README for every binary, and a GitHub wiki.

Changed#

  • Plugin install copies only runtime files (manifest / wasm / assets), never the plugin source tree.

Fixed#

  • Dock — focus the exact window from the right-click menu (the long-parked bug): the bare focuswindow address:… string was silently dropped; it now goes through the recognised dispatch focuswindow <idx> path.
  • Menus no longer flash a stale previous menu when opening a different menu at a shared screen position (instant stack-child switch; the revealer still animates).
  • Catwalk + Hidden-Bar triggers use the canonical bar-pill hover (.ok-bar-widget) instead of a non-existent class.
  • Tiling-layout precedence (taglayout > tagrule > default_layout) and live re-apply on mctl reload.

[0.8.5] – 2026-05-27#

Added#

  • Auto light/dark from wallpaper (Settings → Theme → Wallpaper Matugen) — an opt-in toggle that derives the Material You light/dark polarity from the wallpaper's average luminance on each wallpaper change (bright → Light, dark → Dark), overriding the manual Mode. Only affects the wallpaper-driven theme. (Inspired by VibePanel's wallpaper-adaptive theming.)

[0.8.4] – 2026-05-27#

Added#

  • Control Center — a noctalia/GNOME-style quick-settings menu (bar pill with a new margo-symbolic icon). A header (avatar + username + uptime + lock / power / settings / edit actions), volume / mic / brightness sliders, and a configurable tile grid. Tiles: Wi-Fi, Bluetooth, Audio Out, Mic, VPN, Valent, Battery, Keep Awake, DND, Airplane Mode, Dark Mode, Twilight, Color Picker, Disk, Firewall (UFW), Podman. Expandable tiles use GNOME-style inline-expand (a sliding gtk::Stack with a back arrow) and reuse the real detail components (Network, Bluetooth, Audio, Power, DNS, Valent, Twilight, Keep Awake, UFW, Podman menus). Left-click opens the detail page, right-click quick-toggles (Bluetooth / Twilight / Keep Awake / UFW power via pkexec / stop Podman machine). The active power profile shows on the Battery tile, the Twilight profile + temperature on Twilight, and the running machine name on Podman. Edit mode + Settings → Widgets → Control Center configure per-tile visibility, order, and wide (2-column) tiles.
  • Settings → Network + Bluetooth — GNOME-parity pages. Network: Wi-Fi scan / connect, wired status, VPN list + import, a per-connection editor (General / IPv4 / IPv6 / Security via nmcli), and a manual proxy section (config schema + environment.d applier). Bluetooth: adapter power, device list, pair / connect / trust.
  • Settings → Power + Default Apps + Privacy — GNOME-parity pages. Power: battery, power profiles, suspend, low-battery warning, and lid / power-button behaviour via a logind drop-in (pkexec, applies next login). Default Apps: per-category default handler via gio::AppInfo. Privacy: location toggle, camera / mic indicator, lock summary, file-history remember + clear (GtkRecentManager), and flatpak portal-permission list + revoke.
  • Audio — optional toggle to hide HDMI / DisplayPort outputs from the output list + switcher (Settings + mshellctl audio).

Changed#

  • Dashboard design system — a stabilization pass: a --space-* spacing scale, semantic --warning / --success colours, a 3-tier surface elevation model, and a shared card contract. The media widget was relaid out (cover / title / artist / progress + centred controls), the calendar gained a today / selected / hovered / inactive state model, and weather / clock / audio / system / CPU widgets were brought to token + elevation conformance, with consistent motion tokens for hover / focus / selection / reveal.
  • Bluetooth, Clipboard, and Notification menus — redesigned to the flat audio-dashboard design language (engine preserved). Bluetooth shows flat device rows with battery + a connected accent.
  • Bundled Nova profile — refreshed to mirror the current showcase (new bar widgets + menus, Control Center, alarm / proxy / power / privacy / audio sections), with machine-specific bits neutralized.

Performance#

  • Notification history — virtualized the list (GtkListView + factory + lightweight row model, mirroring the clipboard menu). A persisted history no longer rebuilds N heavy widgets per open / per incoming toast, fixing the lag that grew as notifications accumulated. Per-app grouping is preserved.
  • Control Center pollers — the shell-out probes (Twilight / UFW / Podman) and the Keep-Awake countdown only run while the panel is revealed.

Fixed#

  • Wallpaper menu — no longer freezes the GTK main loop on first open (blocking receive → async oneshot).
  • Bluetooth menu — device battery now shows (watch each device's battery / state property, not just the device list).
  • Dashboard — rounded the CalendarGrid card's bottom corners and made the media player fill its column height.

0.8.3 – 2026-05-26#

Added#

  • Alarm Clock — alarm + stopwatch widget. A bar pill (alarm-bell glyph; shows the running stopwatch time inline and pulses while ringing, right-click to silence) opens a tabbed menu: an Alarms tab (reactive list with per-alarm enable / time / repeat-day chips / delete, plus an add row) and a Stopwatch tab (start / pause / reset). Alarms persist in the shell profile and fire from a main-thread scheduler that plays a looping tone and pops a Stop / Snooze notification; one-shot alarms auto-disable after firing.
  • Settings → Users — GNOME-style account management replacing the old read-only list. Per-account expandable cards: change picture, edit full name, toggle Administrator, change password, and add / remove users. Every privileged action runs through pkexec so the polkit agent prompts, with guards against demoting or deleting the last administrator.
  • Daily Wallpaper (Settings → Wallpaper) — opt-in Bing or NASA image-of-the-day, fetched on login and refreshed daily.
  • Bundled default wallpaper — a margo-branded margo-hero.png ships as the default desktop wallpaper (shown when no wallpaper directory is set) and is always the first tile in the Wallpaper menu.
  • mshellctl media — MPRIS player control: toggle / next / prev / status / list, with an optional player-name fragment (spotify, browser, …) and a now-playing toast on each action.
  • mshellctl audio — full CLI audio control: list / status / set / switch for output + input devices.

Fixed#

  • Media targeting — a player-name fragment (e.g. browser) now matches on the D-Bus bus name + desktop entry, not just the MPRIS identity (so Chromium forks like Helium match), and picks the playing player when a fragment matches several.
  • Audio device list — stable ordering so switch cycles predictably, dead / monitor sinks filtered out, and friendly device names in the Sound page.
  • Cursor shapes — margo now honours named cursor shapes (pointer / text / grab / …).
  • mlogind — square (Plain) borders so corners join cleanly on a real TTY.
  • Settings → Menus — "Add widget" uses the same scrollable popover as the Bar editor.

0.8.2 – 2026-05-25#

Added#

  • Settings → Keybinds — a full editor for margo's keyboard shortcuts: read every existing bind, add new ones, edit and delete. The editor owns a dedicated binds.conf; on the first edit it migrates every inline bind* line out of config.conf into binds.conf (grouped by category), leaves a single source = binds.conf behind, and backs the original up to config.conf.bak. From then on each change is a clean full rewrite + live reload. The UI is a searchable list (modifier/key chips + humanised action) with an inline editor: modifier chips, a press-to-capture key field, a searchable action picker over the dispatch verbs with a contextual argument hint, and an optional description.
  • Settings → Animations — five curated motion presets (Smooth, Snappy, Bouncy, Cinematic, Glide), each a full coherent set of per-domain durations / bezier curves / clocks / open-close types. Pick a card, hit Apply, and the whole set is written to config.conf and reloaded live. Master on/off + layer-animation switches apply on the spot. Smooth is the recommended daily-driver (spring glide for moves, gentle zoom-in, snappy fade-out).
  • Settings → About / Date & Time / Region & Language — three new system pages closing the gap with a conventional desktop settings app. About shows read-only system info (OS, kernel, host, CPU / GPU / memory, desktop session, margo version, uptime). Date & Time wraps timedatectl (automatic time / NTP, searchable timezone, 24-hour clock). Region & Language wraps localectl (system LANG, searchable). The timedatectl / localectl writes authenticate through margo's polkit agent.
  • Settings → Users — a read-only roster of the system's human accounts (username, full name, administrator status from wheel/sudo, and ~/.face / AccountsService avatars), parsed from /etc/passwd + /etc/group. The current user is listed first.
  • Settings → Sound — output + input device selection, volume, and mute, backed by the same reactive wayle_audio service as the bar's Audio Dashboard. The page stays live (default-device, device-list, and per-device volume/mute watchers) without feeding programmatic refreshes back into a write loop.
  • Settings → Input — a full keyboard / touchpad / mouse page (replacing the narrower Gestures page). Keyboard: xkb layout / variant / options (e.g. ctrl:nocaps), repeat rate + delay, Num Lock on start. Touchpad: tap-to-click / tap-and-drag / drag-lock, natural scroll, disable-while- typing, left-handed, middle-button emulation, click + scroll method, scroll button, send-events mode. Mouse: natural scroll, acceleration profile + speed. Plus swipe sensitivity, and a gesture-binding editor — list / add / remove gesturebind swipe→action mappings (direction, fingers, action, argument, modifiers) right from the UI. Everything writes the compositor config.conf and applies live.
  • Bundled shell profiles + a starting-profile picker in the setup wizard. margo now ships two example mshell profiles — Default (clean, minimal) and Nova (the full-featured showcase) — installed to /usr/share/margo/mshell/profiles/ and baked into the binary. The setup wizard's Welcome step now offers them with descriptions; picking one seeds it into ~/.config/margo/mshell/profiles/ (only if absent — it never overwrites a profile you've customised) and activates it live. Both profiles are documented inline in English.
  • mshell now starts automatically on a margo session (packaged). The package ships a mshell.service systemd user unit and auto-enables it via a graphical-session.target.wants drop-in, so a fresh install brings up the bar / menus on login with no manual systemctl --user enable — and systemctl --user status mshell works out of the box. The unit is guarded (ConditionEnvironment=XDG_SESSION_DESKTOP=margo + WAYLAND_DISPLAY) so it never starts under another desktop, and a user's own ~/.config/systemd/user/mshell.service still overrides it.

Changed#

  • Settings → General decluttered. With dedicated pages now in place, General keeps only the account, config profile, and shell behaviour (Network OSD). The duplicate Clock toggle was dropped (Date & Time owns it), the Settings-panel font scale moved to Fonts, and rounded screen corners + radius moved to a new Screen sub-tab under Display.
  • Twilight menu — source-mode selector now uses power-profile-style tiles. The Auto / Manual / Static / Schedule buttons gained icons and the same vertical icon-over-label tile look as the Power menu's profile switcher (active mode filled primary), so the two quick-control panels feel like one family instead of plain text segments.
  • mlogind greeter redesigned to match the mlock lock screen. The login TUI now mirrors mlock's centred stack: a time-aware greeting, a big block clock (live, ticking every second), the full date, a single rounded accent-bordered card holding the session / username / password rows, a centred status line, and a centred row of power-control chips. Colours come from the same matugen palette as before (accent for the card, muted for secondary text), so the greeter is now visually of a piece with the locker. The layout is fully responsive to the terminal size — the bare VT can be much shorter or narrower than a terminal-emulator --preview: the power chips are pinned to the bottom and the clock + card always render, with the greeting / date / status dropping out (in that order) on a short console, and the chips collapse from [F1] Shutdown to a compact [F1] [F2] [F3] when the row is too narrow — so the F-keys can never be clipped off-screen. The selected session is drawn inline next to its label (Session Margo (UWSM) ›, with ‹ › arrows and graceful truncation) instead of a fixed-width carousel that vanished at some widths. The credential card is wider (room for full session names + a comfortable password field) and the power keys read as bracketed accent chips.

Fixed#

  • Settings pages used the wrong live-reload command. The Input, Animations and Keybinds pages spawned mctl config reload, which mctl rejects ("unrecognized subcommand 'config'") — so the file was rewritten correctly but the compositor never reloaded and edits looked like no-ops. The reload action is mctl reload; fixed all three.
  • General avatar pinned to a fixed 72×72. A ~/.face photo shown via a GtkPicture leaked the image's intrinsic size up through the box and ballooned the avatar; it's now centre-cropped to a square and drawn through a GtkImage at a fixed pixel size, so it stays 72×72 at any source resolution.
  • Setup wizard — theme / colour mode / font size now apply live. The appearance picks on the wizard's Theme step only took effect at the final "Apply & finish", so selecting a theme mid-wizard looked like it did nothing. They now apply on selection (via config_manager, the same path as Settings → Theme).
  • Lock-screen background control moved to the Lock page. It had been misfiled under Session; it now lives with the rest of the lock settings.

0.8.1 – 2026-05-24#

Added#

  • mlogind — margo's TTY login manager (forked from lemurs). A matugen-themed ratatui greeter that launches the margo session supervised through start-margo, syncs its colours from the active wallpaper palette (11 theme variables, active session visually distinct), and offers F3 Suspend alongside the usual power controls. Optional fingerprint login via PAM (pam_fprintd). Packaged like a display manager — binary plus config / PAM / systemd unit installed to /etc. The bare-VT greeter now matches --preview by reprogramming the console palette (no truecolor on a raw Linux VT). Documented in the README and project site, installable from the AUR (margo-git).
  • pass launcher provider — a password-store browser in the launcher, with a configurable store path (Settings → Launcher). Copy and type both honour PASSWORD_STORE_DIR.
  • Configurable lock-screen background — mlock can now use the desktop wallpaper (default), a flat solid colour, or a fixed custom image, set from Settings → Session and read from ~/.config/margo/mlock.conf.
  • Settings → Gestures — a dedicated section for touchpad and swipe gesture settings.
  • Dock settings page (under Widgets) — configurable icon size, plus the groundwork for click-to-focus.
  • Notification history-menu size controls — width and height are now adjustable in Settings.

Changed#

  • mlock UI refreshed — modern vector icons (replacing emoji) and power-action chips.
  • The Settings sidebar is now sorted alphabetically, with General kept first.

Fixed#

  • Clipboard performance — the history menu is virtualized (GtkListView, the copyq pattern), rebuilds and the active-search are gated on reveal + debounced, and image thumbnails are Arc-shared so preview clones are O(1). Menu width and height once again track the configured values (both had regressed to a fixed size).
  • pass copy/type honoured the wrong store, so nothing was copied despite the "Copied" toast — they now set PASSWORD_STORE_DIR explicitly (mshell runs as a systemd user service and doesn't inherit shell-rc env).
  • Notification history rebuilds and the SSH-sessions active poll are now gated on reveal (no idle CPU when the menus are closed).
  • The cellular icon recognizes 5G network-type variants (valent).
  • The session display name is capitalized (margo → Margo).

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_options such as ctrl:nocaps), Touchpad (tap-to-click, natural scroll, disable-while-typing), Wi-Fi (scan + connect via nmcli), Wallpaper (with a sensible directory fallback so rotation always has a source), Bar (top / bottom), and a Review summary. Apply writes everything live and runs mctl 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 the mwizard shim), and automatically on first launch when no profile is saved.
  • Microphone-mute key with an OSD — XF86AudioMicMute / mshellctl audio mic-mute toggles 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 (~/.face avatar + 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 --json live state (and hex colours in mlayout 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_model rebuilt the StringList every view pass, whose selected-notify fed 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 nmcli scan 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 on ok-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.desktop plus the margo-uwsm-session / margo-session wrappers now install to /usr/share/wayland-sessions and /usr/bin, and /etc/xdg/uwsm/env-margo restores the standard XDG user-bin dirs (~/.local/bin, ~/bin) onto the session PATH (uwsm rebuilds it from a POSIX login shell and would otherwise drop them). uwsm moves from optional to the default way to run margo; uwsm is now a runtime dependency.
  • Tracy profiling support — building with --features profile-with-tracy now starts a Tracy client and marks frames, so the existing span! 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.json writes 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_ID it 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.json is refreshed on every mctl twilight change, 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-btn chrome. 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 with mshellctl 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 scattered px / em font 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 CSS overflow on a GtkBox).
  • 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 mlock procs from wedging lock_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 LICENSE stays at the repo root; all upstream attributions (mango, dwl, dwm, OkShell, niri, noctalia) now live in a licenses/ directory. PKGBUILD, install.sh and 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-only ext-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 in road_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 repo PKGBUILD with makepkg and installs with pacman (uninstall = pacman -R margo-git). On Debian / Ubuntu it installs the apt build deps, bootstraps a current Rust via rustup when the system one is too old, builds gtk4-layer-shell from source when it isn't packaged, compiles the workspace, and installs to /usr — recording every path in a manifest so uninstall removes 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 upgrade won't change that for the LTS lifetime. Use Ubuntu 25.10+ / 26.04 LTS or any distro with GTK 4.20+. install.sh checks the GTK version up front and stops early with a clear message. README install section rewritten around the installer; design in docs/install-script.md.

[0.7.2] – 2026-05-21#

Added#

  • Native Screenshot portal. margo-portal now serves org.freedesktop.impl.portal.Screenshot in addition to ScreenCast, driving the compositor's org.gnome.Shell.Screenshot shim. The shim's screenshot path was previously a stub that always failed; it now captures the desktop via grim to a temp PNG (asynchronously, off the compositor event loop) and returns a file:// URI to the requesting app.

Changed#

  • gnome-portal-free sessions. margo-portals.conf no longer routes any interface to xdg-desktop-portal-gnome: ScreenCast=margo, Screenshot=margo, RemoteDesktop=none, default=gtk. Secret stays on gnome-keyring (the standalone freedesktop secret daemon — not the gnome portal). margo.portal advertises Screenshot with UseIn=margo, and the wayland-session entries declare DesktopNames=margo (dropping mango;wlroots) to keep portal config resolution unambiguous.
  • Packaging. xdg-desktop-portal and xdg-desktop-portal-gtk are now hard dependencies; the xdg-desktop-portal-gnome and polkit-gnome optdepends 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.conf bind = lines (following source includes), 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 mshellctl DND / 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.conf from the active scheme and mctl reloads on change. source it from config.conf to override the static border colours (rootcolor stays 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-path hints (and the -symbolic fallback) so notify-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.yaml exists 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 both default.yaml (full shell profile) and ~/.config/margo/config.conf (surgical xkb_rules_* line patch). mwizard --force re-runs even when a profile exists. No-flag mwizard is 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 in mshell-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 the margo-git package; mshell's launcher ColorPicker button calls mpicker directly 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 dashboard or 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_height config. 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 HiddenStore mirrors PinStore at ~/.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 a usage_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-tabs prints the known categories.

  • mscreenshot region selector via mshell IPC. mscreenshot area now bridges to mshell's rich in-shell selector (preview state, snap, aspect info) when mshell is running, drops to the bare slurp overlay otherwise. New IPC method SelectRegion returns 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 satty over swappy to match the rest of the workspace.

Changed#

  • menu_settings.rs collapsed 4041 → 394 LOC via a new MenuConfigPanel sub-component + extended MenuKind with 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/ColorPicker sweep; 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::paths module is now pub so external binaries (mwizard, future tools) can resolve the profile path without re-deriving the layout.

  • mshell + launcher caches honour $XDG_RUNTIME_DIR instead 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 in mshell-screenshot → mshell-services → wayle-* → zbus[tokio] via Cargo's per-invocation feature unification. The compositor linked against the tokio-enabled zbus and panicked at start_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 stays async-io-only.

  • Dashboard right column wasn't rendering card chrome. MenuModel's css_class field stored "quick-settings-menu dashboard-menu" as a single literal class name (set_css_classes was being passed the whole string as one entry), so .quick-settings-menu .network-menu-widget descendant selectors never matched. Split on whitespace post-view_output!.

  • matugen "no progress, hangs forever" bug. The output-log drainer thread used .flatten() on a BufReader::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 --workspace sweep — 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.rs documented its in-vec invariant via expect().

[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 via can_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 $TERMINAL with 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 to search(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.

  • DisplayItem wrapper — runtime-stamped decorations (pinned flag, quick-key digit) handed to the UI. Providers still emit raw LauncherItem; 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-variant border) — clear "well inside the panel" effect instead of the previous mid-grey wash that blended into the menu surface on the tight Margo palette.
  • Result rows: transparent default, hover picks up --surface-container-high, selected row flips to --primary with icon + label + quick-key + ★ all reflowing to --on-primary so contrast survives the tint swap.

  • Default browse pipeline — runtime tracks an active_category. Selecting a specific tab bypasses handles_search and calls browse(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) to input-keyboard-symbolic which 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 AppLauncher widget 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 libpam FFI extracted from mlock so mshell-lockscreen can actually unlock. The previous PAM stub always failed; mlock's libpam wiring now lives in a shared mshell_auth::pam module (avoids the bindgen/clang-sys problem the pam-sys crate 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 subsequent set() broadcasts (BehaviorSubject semantics). The old change-only stream missed the single startup set() for the workspaces vec (margo's tag_count = 9 is 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.json watch. 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.scss ships 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.css and 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_output is 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 ran focusmon) to an empty monitor. Mshell's IPC handler then routed Super+Space to the wrong frame. The field now follows the pointer monitor and refreshes on cursor crossings + focusmon dispatches.

  • 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 / chezmoi symlinks 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_idx null-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 null focused_idx. Root cause of "tag pills on the bar stay empty until the first window opens, every login". At session start margo writes focused_idx: null (no client focused yet); the old schema required i64 and rejected the whole document with invalid type: null, expected i64. apply_snapshot() never ran, service.workspaces stayed empty, and the snapshot-on-subscribe stream yielded an empty vec. Opening any window flipped focused_idx to a real integer → parse OK → 9 workspaces published → pills appeared. Now declared Option<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, win previously hit no provider and silently returned nothing. Every provider's handles_command() now participates in dispatch.

  • mshell-launcher: frecency boost applies in command-mode too. >start brave no longer always sorts alphabetically.

  • mshell-launcher: bt prefix works with bluez 5.65+. Upstream removed the bluetoothctl paired-devices subcommand; try devices Paired first, fall back to the old form.

  • mshell-launcher: lock actually locks. Session provider routes through mshellctl menu session lock (mshell's in-process session dispatcher); the old loginctl lock-session is 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 visible GtkEntry.

  • mshell-launcher: Settings deep navigation race. Activating settings:display no longer races CloseMenus against OpenSettingsAtSection.

  • 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_activated from 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/. Reads schedule.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 preset subcommand 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 dispatches reload_config so the change is live immediately.
  • mctl twilight set mode=geo|manual|static|schedule — the mode field 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 new mctl twilight preset family.
  • mscreenshot — three new options:
  • --delay N / -d N global flag: pop a notification, wait N seconds, then capture. Catches menus / tooltips that close when focus moves to a selector.
  • --output NAME / -o NAME global 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-handle helper that drives notify-send --wait --action, executes the click via xdg-open or fs::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). Legacy accel_* 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_small runtime — 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 via data.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:1 rule with no size hint → invisible 0×0 window. apply_window_rules only synthesised float_geom when the rule carried a width/height/offset hint, so isfloating:1 on its own flagged the client floating but left float_geom at (0,0,0,0). Arrange's if float_geom.width > 0 then skipped the apply and the toplevel got configured at 0×0 — listed in mctl clients, rendered in overview, but invisible on the output. Post-loop fallback now synthesises a default geometry (60 % of work_area centred) when is_floating ended true and float_geom is still empty.
  • windowrule typo class — silent drops. monitor_name: (the tagrule key) on a windowrule now aliases to rule.monitor; is_overlay and overlay now alias to isoverlay. 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 widget menu didn't scroll for users with > ~12 widgets; wrapped in a ScrolledWindow.
  • mshell-settings — Widgets → Layout renamed to Widgets → Menus for 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: EventControllerKey default + Capture phase, ShortcutController Local-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 $srcdir debug-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 in docs/protocol-comparison.md; work plan in road_map.md §15.10. Shipped in commits dc44818 + 74a0edb + c146aac:
  • zwp_keyboard_shortcuts_inhibit_v1 — VNC / RDP / VM clients can grab host shortcuts. input_handler.rs short-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 orphan TabletSeatHandler impl 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 protocol bindings. Their globals were later withdrawn pending a real FIFO/deadline barrier-release scheduler (advertising the managed Smithay states alone was not a complete implementation), then restored in 1.1.9 once state/pacing.rs drove the barriers.
  • 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 to keyboard_shortcuts_inhibit_v1 — same VNC / VM story via the X11 mechanism. Handler maps the XWayland-managed wl_surface to its MargoClient.window so the grab attaches to the correct toplevel FocusTarget.
  • xdg_toplevel_icon_v1 — toplevels ship inline PNG / SVG icons; smithay caches them on the surface as ToplevelIconCachedState. 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+delete keybind, and mshellctl menu session [action] IPC. Tab / Ctrl+N navigation works (focus delivered through a 160 ms post-reveal glib timeout to clear smithay's sync_keyboard_mode debounce).
  • mshellctl menu notifications {clears,read} — clears is destructive (history wipe); read marks 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 FocusTarget impl forwarded only via inner_wl_surface(), which returns None for X11-backed windows. As a result, smithay's KeyboardTarget for X11Surface (the path that calls XSetInputFocus + sends WM_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. Now FocusTarget::Window variants with an X11 underlying surface forward keyboard events to the X11Surface directly. 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. Affects profiles/, styles/, and icons/ lookup paths.
  • Session menu keyboard nav was unreachable. The session menu's EventControllerKey was wired but the focus path was broken — RevealChanged was not forwarded into the session widget, and the bar's hardcoded broadcast list omitted it. Both fixed, plus a 160 ms post-reveal glib::timeout_add_local_once so first.grab_focus() lands after smithay's layer-shell focus debounce.
  • Settings → Session entries typed right-to-left. The gtk::Entry widgets had #[watch] set_text bindings that fed back into themselves on every keystroke, resetting the cursor to position 0. Entries now seed text once at init and write via connect_changed only — no reactive read-back loop.

Changed#

  • KeyboardTarget for FocusTarget dispatches X11 vs Wayland through a new inner_x11_surface() accessor. Wayland-native variants (Window/Wayland, LayerSurface, SessionLock, Popup) keep the existing WlSurface forwarding.

[0.5.0] – 2026-05-14#

Fixed#

  • Menu content widgets recreated ~once per second. The menu's SetWidget handler 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 to ufw / nmcli / podman / resolvectl / mullvad on 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 GridView factory 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 mshell desktop 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 under mshell-crates/. Replaces the need for a separate third-party panel.
  • Margo-native widgets. MargoTags (single-row capsule workspace pills with occupancy dots), MargoLayoutSwitcher (driven by mctl 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/random to 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#

  • npower and nnetwork are now reactive over D-Bus. Both widget pairs previously ran per-monitor poll loops that shelled out to powerprofilesctl (a Python script) and nmcli — 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 twilight instead of mshell-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 cargo invocations — a single --workspace build unified zbus's tokio feature into the compositor, which then panicked at startup.

Fixed#

  • margo bar flicker — ported niri's render + frame-callback pacing, paced frame_done to VBlank, dropped the wp_linux_drm_syncobj_v1 global, disabled DRM overlay-plane scanout (Intel MTL quirk), and fixed margo-client Arc identity churn.
  • margo startup panic — zbus was pulled in with its tokio feature via workspace feature unification; the compositor drives zbus over async-io and has no Tokio runtime, so it panicked before the session came up.
  • Settings crash from unsanitised GAction names 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:
    1. ~/.local/share/margo/wallpapers/default.jpg (user override)
    2. /usr/share/margo/wallpapers/default.jpg (package default)
  • wallpaper_fit = cover|contain|fill|center — only cover is 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.jpg and lands at /usr/share/margo/wallpapers/default.jpg after install, so a fresh margo session never falls through to a flat dark lock backdrop just because the user's external shell hasn't populated state.json yet.

Changed#

  • mlock wallpaper resolution is now tiered. Previous behaviour was state.json or nothing; new chain:
  • state.json active output's wallpaper field (margo tagrule passthrough — unchanged primary path).
  • ~/.local/share/margo/wallpapers/default.jpg — user override.
  • /usr/share/margo/wallpapers/default.jpg — package default (shipped by margo-git). Every layer is metadata().is_file()-checked, so a stale path in state.json no longer wins against a real fallback. The candidate that lands is logged via tracing::info! so the source of the current lock wallpaper is one log line away from diagnosis.

[0.4.6] – 2026-05-13#

Added#

  • start-margo watchdog supervisor. New Rust binary in the workspace — wraps margo with a rolling crash budget (--max-restarts 3 --restart-window-secs 60 by default), emits sd_notify READY=1 after spawn and STOPPING=1 on graceful shutdown, preserves the incoming signal when forwarding SIGTERM / SIGINT / SIGHUP to the compositor, and sets PR_SET_PDEATHSIG(SIGKILL) so a kill -9 start-margo can never leave an orphaned margo. Single source file (start-margo/src/main.rs, ~230 lines), depends only on anyhow / clap / tracing / tracing-subscriber / libc. Three concrete improvements over Hyprland's start-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 prefers start-margo, falls back to bare margo.
  • systemd/user/wayland-wm@margo-session.service.d/10-session-lifecycle.conf — drop-in that sets MARGO_LOG, fires the session target, bumps Nice / CPUWeight. See contrib/sessions/README.md for 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 mode CLAUDE.md warns against ("mesa abort inside the render path on overview trigger" coredumps were resolving to ?? ??:0 for every margo frame). options=(!lto !strip) now matches the strip = "none" setting that's been in the Cargo release profile all along. The next time margo trips an ABRT, coredumpctl info / addr2line will name the exact Rust source line instead of a hex offset.

Changed#

  • README binary table + install loop. start-margo is now in the table (between margo and mctl), 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.conf now passes mctl check-config cleanly. 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/l was bound twice (the setmfact pair moved to super+alt,h/l, hjkl muscle memory preserved); focuslast action 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-once block modernised. Bar / notifications / launcher recommendations updated to reflect the external-shell-first architecture: qs -c noctalia-shell --no-duplicate or waybar side by side, with fnott / mako notification-daemon alternatives broken out.
  • unreachable!() panic messages. Eight bare unreachable!() sites across protocols/screencopy.rs, protocols/gamma_control.rs, mctl/bin/mctl.rs, mlayout/main.rs, mscreenshot/main.rs, and layout/snapshot_tests.rs now carry a one-line why string so post-mortems read context instead of the generic "entered unreachable code" line. The ok_or_else(|| unreachable!()) pattern in mctl's output-index resolver rewrote to plain unwrap_or(0) — the original .or(Some(0)) already guaranteed Some.
  • mlock wl_globals binding tightened. if self.X.is_none() guards inside match g.interface.as_str() collapsed into match-arm guards, and three min().max() clamp chains rewritten with .clamp(lo, hi).

Removed#

  • Stale #[allow(dead_code)] attributes.
  • margo/src/screencasting/pw_utils.rs lost its crate-level #![allow(dead_code)] — the niri-port scaffolding was fully wired up over Phases C / D / F.
  • mlock/src/state.rs field conn: the allow was a holdover; Connection is read every iteration via state.conn.flush() and state.conn.backend().poll_fd() in main.rs.
  • margo/src/state.rs: orphaned attribute above DmabufImportHook (blank line in between) moved onto the type alias so empty_line_after_outer_attribute stops firing.
  • Unused dependencies pruned. Manual audit confirmed zero source-level use sites:
  • margo: keyframe, nix, log (the codebase standardised on tracing).
  • 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 in mlock/src/state.rs, 1 in margo/src/state.rs).
  • cargo test --workspace: 244 tests, 0 failures.

[0.4.4] – 2026-05-13#

Removed#

  • mshell crate. 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 any dwl-ipc-v2 client (noctalia, waybar-dwl, fnott, …). The compositor side of dwl-ipc-v2 is unchanged.
  • midle crate. Idle management moves out of the workspace. Any ext-idle-notify-v1 client (swayidle, hypridle, stasis, …) works as a drop-in.
  • Matugen integration. mshell matugen, the ~/.cache/margo/margo-colors.conf source = 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.toml wallpaper fallback. Wallpaper resolution inside mlock now reads exactly one source — state.json's active output wallpaper field — and falls through to the solid dark backdrop on miss. toml is dropped from mlock's Cargo.toml.

Changed#

  • README rewrite. Intro, binary table, install paths, file-layout block, At a glance recipe list, scripting example, and acknowledgements are now consistent with the six-binary scope. The dwl-ipc-v2 bullet was promoted to call out external-shell integration explicitly.
  • PKGBUILD overhaul. depends= lost the panel-only runtime pulls (libpulse, pipewire) and gained the previously-implicit pam / 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 gained noctalia-shell-git + fnott as 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) and network_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's animated_size wrapper 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::MONOSPACE on every numeric bar value — equal advance per digit, so two-digit values are pixel-stable.
  • Length::Shrink (text widget hugs its content) instead of Length::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_item now skips the animated_size wrap 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, so CompositorState.active_window collapsed to None. WindowTitle::recalculate_value was overwriting its cached string with that None, blanking the bar item, and the resulting Length::Shrink content collapse rippled across every neighbour capsule. recalculate_value now early-returns whenever active_window is None or 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 → ToggleMenu on that monitor's bar surface. With two monitors, if active_output shifted between presses the handler picked the other monitor as the target, opened a fresh menu there, and toggle_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 restart subcommand. Scans /proc for sibling mshell processes, 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 via setsid(). Replaces the pkill mshell && setsid -f mshell … shell incantation.
  • midle D-Bus inhibit monitor. Eavesdrops the session bus for org.freedesktop.ScreenSaver, org.gnome.SessionManager and org.freedesktop.portal.Inhibit traffic, correlates method-call serials with their cookie / handle returns per sender, and drops sender rows on NameOwnerChanged disconnects. Settings::enable_dbus_inhibit (default true) gates it. midle info now reports an inhibitors breakdown (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_indicator wraps Warning / Danger states in a 1px tinted border + 10% accent background; BatteryData::get_indicator_state gains 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_bar resolves the open menu once per render and threads it through to ModuleItem::is_active.
  • Tempo rich composite. Opt-in [tempo] secondary_format = "%a %d %b" renders a 2-line Column: primary clock in semibold at bar_font, secondary string beneath at font_size.xs and 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 open Exclusive; the compositor moves focus onto the menu surface as soon as it appears, ESC reaches mshell's listen_with Escape handler, and the menu closes.
  • No more blank flash on menu open. A new MENU_OPEN_PREROLL_MS = 30 constant backdates open_at so the first paint lands at ~42% opacity (after ease-out-cubic) instead of α=0; the animation finishes ~150ms later with no perceptual flash. When theme.animations_enabled = false, open_at / closing_at are 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 to theme.bar_font_size, drop the pointless wrapping container, tint the row with palette.primary when there are pending updates.
  • midle daemon no longer panics margo at startup. The tokio feature on midle's zbus dependency 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 to default-features = false, features = ["async-io"] — margo stays on async-io, midle's own tokio runtime can still .await zbus 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 Changed below).
  • Cold-path structured-logging migration complete (Q5) — every tracing call in state.rs, dispatch/mod.rs, scripting.rs, plugin.rs now emits structured fields.

Added#

  • Screenshot region selector geometry tests (roadmap T6). 14 new tests lock ActiveRegionSelector::selection_rect normalisation across all four drag directions (TL→BR, BR→TL, TR→BL, BL→TR), degeneracy handling (zero area, sub-pixel, vertical/horizontal line), grim -g geom-string format, drag-lifecycle (begin_drag snaps anchor, update_drag no-ops without begin, end_drag preserves rect), and half-pixel rounding edge cases.

  • Theme preset tests (roadmap T8). 13 new tests cover apply_theme_preset for default / 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.
  • default is idempotent under repeated calls.
  • Baseline survives intermediate manual config tweaks (doesn't refresh from post-tweak state).
  • Unknown preset returns Err with a clear "try default, 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_id and exclude_title veto 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$ → [invalid substring). 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.0 spring 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 than MAX_TAGS both deserialise cleanly.
  • unknown_layout_name_in_snapshot_does_not_break_serde — snapshots survive a future layout-name renaming (the loader's LayoutId::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 so session.json stays human-diff-able.
  • captured_at_round_trips_through_serde — belt-and-braces on the hand-rolled chrono_like_now string. 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 in state.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 | jq slices cleanly: e.g. ... | jq 'select(.fields.error)' for every error record, or select(.fields.cmd | test("nautilus")) for every spawn of a specific command.
  • FocusTarget::enter / FocusTarget::leave demoted 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. The target field keeps full pretty-debug detail for users who actively want to trace focus routing.
  • Hot-path callers (backend/udev/{frame,hotplug}, input_handler keybind + 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 --workspace handled the mechanical temp-lifetime rewrites across 9 files; the rest of this commit is the modern-idiom follow-up:
  • 7 collapsible nested if let blocks rewritten as if 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 = …; foo blocks collapsed to direct return (margo::input_handler, margo::state).
  • theme_baseline_tests rewrote Config::default() + 9 reassignments into the Config { borderpx: 3, ..Config::default() } struct-init spread idiom.
  • gesture_tests mod moved from mid-file to end-of-file (clippy::items_after_test_module).
  • gamma_lut::extreme_inputs_clamp_safely test's tautological assert!(v <= u16::MAX) (always true for u16) replaced with std::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, no sunrise or chrono deps), 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 test sweep.
  • Reuses the existing wlr_gamma_control_v1 plumbing — pending_gamma is fed from the tick, the udev frame handler pushes ramps to GAMMA_LUT on 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 TwilightMode enum. All clamped at parse time; parser::OPTION_KEYS extended so the validator picks them up automatically.
  • Live control via mctl twilight {status, preview, test, set, reset}. status reads state.json (no IPC roundtrip); the rest dispatch through the compositor.
  • Disabled by default — flip twilight = 1 to 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 structured ConfigDiagnostics 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, unresolved source/include paths, and unknown top-level keys. The allowlist is sourced from parser::OPTION_KEYS — adding a new option to the parser automatically expands what the validator accepts.
  • mctl check-config rewrite — 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. --force keeps the old "fire and see what happens" behaviour.
  • Compositor fail-soft on reload — reload_config runs the validator before parsing; if there are errors it keeps the previous config, sets last_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 for last_reload_diagnostics via state.json (Hyprland's hyprctl configerrors analogue). 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 SolidColorRenderElement path (no new shader, no font rasterizer), sits above windows + layer surfaces but below the cursor. Lives in render::config_error_overlay. The banner is a visual cue only; the actual error list comes from notify-send, mctl check-config, and mctl 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 modifiers from the release-event filter callback and checked whether the snapshot still overlapped. Problem: xkbcommon updates its modifier state after the filter runs, so on the Alt_L release event the callback still sees modifiers.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 its margo_config::Modifiers bit 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_step called open_overview() first — and open_overview reset overview_cycle_pending + overview_cycle_modifier_mask to 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 read cycle_pending = false, did nothing, and overview stayed open after the user let go of Alt. Fix: drop the defensive reset from open_overview. close_overview and overview_activate already handle the flag's lifetime on the way out; opens reached through overview_focus_step carry 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 is None on the very first press while overview is freshly open. The None fallback landed at index 0, and in MRU mode index 0 is the currently-focused window (most-recent entry in focus_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 = +1 then moves to index 1, which in MRU is the previously-used window. Same fix benefits tag / mixed modes: the user's first cycle step moves away from where they already are, not onto it.

Added#

  • overview_cycle_order config — 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 in overview_visible_clients:
  • mru (default, preserves 0.1.9 behaviour) — focus_history first (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/prev keybind 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_next binding would commit on Super release.
  • alt+shift+Tab walks backwards: releasing Shift alone won't commit (Alt is still held); releasing both Alt and Shift will.
  • Two new MargoState fields: overview_cycle_pending and overview_cycle_modifier_mask. Cleared by open_overview, close_overview, and overview_activate. alt+Return still 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 (default 1.6, range [1.0, 4.0]) — multiplies the normal border width on the keyboard / hover-selected thumbnail. Border already paints focuscolor on selection; the multiplier makes the pick read even at small thumbnail sizes without a separate render path.
  • overview_dim_alpha (default 0.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 on render_elements_from_surface_tree (Wayland live surface) and the X11 AsRenderElements path, so no new render element type is needed — one f32 per window per frame. Set either to 1.0 to opt out individually.

  • Overview alt+Tab now MRU-ordered. overview_visible_clients walks the per-monitor focus_history first (most-recent first), then appends any remaining visible clients in clients-vec order for completeness. Result: alt+Tab steps 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-slide arrange_monitor after every Tab press to push the new selection through the layout pipeline. Even at 1 ms duration, the arrange-time border::refresh ran 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 flips is_overview_hovered, calls border::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_grid helper (~95 LOC including doc) and its is_overview branch in arrange_monitor.
  • The is_overview setup at the top of arrange_monitor (layout = Grid + tagset = !0 + is_tiled filter relaxed) is now sufficient — a single layout::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 focuscolor border on the new selection instantly, no animation kaos.
  • spatial_overview module + design doc + 7 dispatch actions + OverviewMode/overview_mode config + MargoState::spatial/ spatial_panning fields + 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_delta so 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/down step ¼ of the panel each, overview_zoom_in/out × 1.2 / × 1/1.2, overview_zoom_reset snaps to active tag at config zoom. Bind any of them inside overview for accessibility / no-mouse flows.
  • Frame tick: the same tick_animations hop that drives window animations now also ticks MargoState::spatial. While momentum is non-zero or the camera is interpolating toward a target, arrange_all runs and the next frame schedules — so the camera keeps coasting until friction settles it (FRICTION = 0.92 per frame, VELOCITY_FLOOR = 0.5 px/frame snap-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 with overview_mode = grid). On open, arrange_monitor branches into the new arrange_spatial_overview_geometries helper:
  • 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_screen to land its geom on screen. Render, border, hit-test paths all read client.geom unchanged — they don't know spatial mode is on.
  • open_overview snaps MargoState::spatial to the active tag's world centre at overview_zoom so 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 at docs/design/spatial-overview.md covers 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_corner now early-exits when session_locked is 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/_prev border highlight tracks the selection. The previous attempt called focus_surface on 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 the is_overview_hovered path that border::refresh already paints with focuscolor (border.rs:64), with no crossfade kick. Border, cursor, and hover flag move together on every Tab; commit goes through overview_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, and OverviewDrag all 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.0 thumbnails 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. New MargoState::compute_overview_grid_layout helper is the single source of truth for the cell list; arrange_overview_per_tag_grid, overview_cell_rect, and overview_cell_at_cursor all 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 focuscolor border; 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. New MargoState::overview_drag: Option<OverviewDrag> state, plus overview_cell_at_cursor / overview_cell_rect / overview_client_at_cursor hit-test helpers (kept in math lock-step with arrange_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, before upper_layers so 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_overview is the single dispatch name. The toggleoverview / toggle-overview / overview aliases 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. The mctl actions catalogue entry now reads toggle_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 from Pertag::ltidxs flow 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). New MargoState::arrange_overview_per_tag_grid helper drives the cell-by-cell arrange; arrange_monitor branches into it when is_overview is 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 by arrange_monitor: while overview is open, the work area shrinks to zoom × work_area centered 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_ms config is now honoured via a new overview_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:

bind = super,grave,toggle_overview
gesture = swipe, 4, up, toggle_overview

Changed#

  • MargoState gains hot_corner_dwelling: Option<HotCorner> + hot_corner_armed_at: Option<Instant> to drive the dwell timer. update_hot_corner() runs at the tail of every pointer_motion handler — 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_exclusive dispatch action. True fullscreen: window resizes to monitor_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.
bind = super,f,togglefullscreen
bind = super+shift,f,togglefullscreen_exclusive

Changed#

  • togglefullscreen now respects work_area. The default fullscreen action used to size the window to the full monitor_area even though the layer-shell bar kept rendering on top — the window's top region was permanently covered. Now the window resizes to monitors[].work_area (after layer-shell exclusion zones), so the bar stays visible and the window covers every other pixel below it. Standard F11 feel.
  • MargoClient gains a fullscreen_mode: FullscreenMode { Off, WorkArea, Exclusive } field alongside the existing is_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 and set_client_fullscreen(idx, bool) shims to WorkArea. xdg_toplevel size 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#

  • mvisual window no longer flashes-and-quits. GApplication registers itself on the session bus by default; if a stale dev.margo.visual name was still claimed (most commonly: a previous cargo run session whose dbus name hadn't been released), the second start registered as remote, forwarded the activate signal 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 passing gio::ApplicationFlags::NON_UNIQUE on 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's dispatch request takes 5 string slots; margo maps them as arg1 → arg.i (numeric parse), arg2 → arg.i2, arg3 → arg.f, arg4 → arg.v (string), arg5 → arg.v2. The 0.1.4 Theme { preset } clap variant was stuffing the preset into slot 1 — the i32 parse silently failed, arg.v stayed None, and theme gaudy quietly resolved to the default preset. Now lands in slot 4 alongside the convention every other string-payload dispatch follows. session-save / session-load don't take args so they were already correct; the latent mctl run <file> bug (path stuffed into slot 1, run_script handler reads arg.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-load subcommands. Three new Command variants in the mctl clap 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 --help now lists all three; session-save/session-load accept the underscore alias too for symmetry with the action name.

Changed#

  • Hot-path logging migrated to tracing structured fields. backend/udev/frame.rs, backend/udev/hotplug.rs, and the gesture + keybinding-match log lines in input_handler.rs now emit per-event fields (output = %name, reason = …, queued = …, error = ?e) instead of pre-formatted strings. Run with tracing-subscriber's JSON formatter and journalctl -u margo --output=json | jq slices 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 a theme_baseline snapshot so default always reverts to "what the config said"; mctl reload invalidates the baseline so the next default lands 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_bindings dispatch. True touch events (TouchDown/Motion/Up) are now distilled into the same (fingers, motion, mods) → action lookup the touchpad swipe path uses. A binding written as gesture = swipe, 3, right, view_tag fires regardless of input surface. (feat(input))
  • presentation-time real per-output VBlank seq. The seq field in wp_presentation_feedback.presented was hardcoded to 0; it's now a monotonic OutputDevice::vblank_seq bumped at the head of every DrmEvent::VBlank handler. Frame-pacing-sensitive consumers (mpv --vo=gpu-next, kitty render loop, gnome-shell's getRefreshRate polling) now see the contract the protocol promises. (feat(presentation-time))

Changed#

  • Window-rule reapply unified via WindowRuleReason enum. Three trigger sites (finalize_initial_map, late app_id settle, mctl reload) previously called apply_window_rules_to_client with no shared signal of why a rule was firing. New WindowRuleReason::{InitialMap, AppIdSettled, Reload} is passed to a single reapply_rules(idx, reason) path; the debug log records the trigger so a RUST_LOG=margo::state::windowrule=debug trace tells you which call site landed. Roadmap §16 #4 do-over wishlist item. (refactor(state))
  • RenderTarget enum replaces (include_cursor, for_screencast) bool pair. build_render_elements_inner callsites now read RenderTarget::Display / DisplayNoCursor / Screencast { .. } instead of two anonymous booleans the reader had to remember the meaning of. Internal flags() 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#

  • mvisual design 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's Pertag so 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 than niri-visual-tests on 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-layouts workspace crate. Pure layout arithmetic (~1040 LOC, no smithay/wlroots deps) extracted from margo/src/layout/{mod,algorithms}.rs so the compositor binary and mvisual consume the exact same arrange(). 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). colord D-Bus client (org.freedesktop.ColorManager + Device + Profile proxies) resolves a DRM connector name → assigned ICC path; lcms2-backed bake_lut runs an identity 33³ grid through sRGB → display-profile transform; to_atlas_rgba32f re-lays the cube as a 1089 × 33 RGB texture so the GLES2 path can sample it without a sampler3D. CPU-side trilinear sampler doubles as the GLSL reference for the ICC_LUT_FRAG shader (ships as const). MARGO_HDR_ICC=1 env gate. Runtime activation upstream-blocked on smithay's compile_custom_texture_shader exposing 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 via DrmCompositor::use_mode), hotplug.rs (405, rescan + setup_connector + migrate-clients-off-output), frame.rs (331, render dispatch + presentation feedback + scanout flags). Type visibility for OutputDevice / BackendData / GammaProps lifted to pub(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-abstracting MargoState (~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.configure for 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=x11 worked because XWayland takes a different protocol path. The commit handler now mirrors smithay anvil's pattern: find the popup via PopupManager, and if is_initial_configure_sent() is false, call send_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 because handle_pointer_button and apply_sloppy_focus called state.focus_surface(...) before forwarding the click. The toplevel-level focus_under() lookup can't see popups (popups aren't in state.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 when pointer.is_grabbed() or keyboard.is_grabbed() — smithay's active grab owns focus routing for the duration, and dismissal re-establishes focus through the normal motion path.
  • xdg_popup.grab now 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 requests xdg_popup.grab could 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_popup validates the serial, ensures the popup is the topmost in its chain, and returns a PopupGrab; 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 trivial From impls — From<PopupKind> for FocusTarget and From<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 via Pertag, per-tag wallpaper hint surfaced through state.json for 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 of ArrangeCtx → Vec<(idx, Rect)> so every algorithm gets snapshot-tested against a committed text fixture.
  • Adaptive layout engine — per-tag user_picked_layout sticky bit + window-count / aspect-ratio heuristic; user setlayout pins the choice, heuristic never overrides.
  • Spatial canvas — PaperWM-style per-tag pan via canvas_pan / canvas_reset actions, 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, runtime wlr-output-management-v1 (mode + scale + position changes apply live, kanshi compatible), pointer_constraints_v1 + relative_pointer_v1 (FPS games / Blender), xdg_activation_v1 with strict-by-default anti-focus-steal policy, VBlank-accurate presentation-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 live windows_changed updates 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. Late app_id / title reapply so Qt clients don't flicker.
  • Scratchpad system — anonymous + named scratchpads, cross-monitor support, single_scratchpad mode, recovery via unscratchpad_focused and super+ctrl+Escape reset.
  • 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 bundled Super+Ctrl+R keybind) re-applies window rules, key binds, monitor topology, animation curves, and gestures without a logout. mctl check-config is 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_v1 global 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_METADATA blob writer + EdidHdrBlock parser (Phase 3, gated on smithay's set_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, MRU focus_history, per-tag wallpaper.

Companion tools#

  • mctl — IPC + dispatch CLI. Subcommands: status / clients / outputs / focused / watch (live JSON / table inspection), dispatch (40+ typed actions; mirrors bind = … 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 with version: 1.
  • mlayout — named monitor-topology profiles for laptops with frequent dock changes. mlayout suggest / list / set / save / edit. Wraps wlr-randr against margo's wlr-output-management-v1 handler so changes apply live without logout.
  • mscreenshot — region / window / output capture. Wraps grim + slurp + wl-copy + an optional editor (swappy / satty if 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 — MargoState lives in margo/src/state.rs (~6,100 LOC after the W4.2 split, down from 7,651). 15 protocol-handler impls extracted into state/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 revision ff5fa7df; Rust 1.85+.
  • Cargo features — dbus (default; gates D-Bus + async-io), xdp-gnome-screencast (default; requires dbus; gates pipewire), a11y (off by default; gates AccessKit), profile-with-tracy (off by default; flips tracy-client to its full backend so a live Tracy GUI can connect). Three build configurations verified.
  • AccessKit a11y — accesskit_unix adapter 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-xwayland disables X11 entirely. Default path stays in-tree (smithay XWayland::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 .snap text 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-client Client + Fixture harness (port of niri's src/tests/{fixture,server,client}.rs). All 15 W4.2-extracted protocol handlers have at least one integration test; 41 integration tests across xdg_shell, layer_shell, idle, xdg_decoration, session_lock, xdg_activation, pointer_constraints, gamma_control, screencopy, output_management, selection, globals, plus negative-invariant pinning for dmabuf / 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.toml documents 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 annotated config.example.conf rendered inline via pymdownx.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) and margo-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.nix exposes packages.default, devShells.default with rust-analyzer + clippy, plus nixosModules.margo and hmModules.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).