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