Skip to content

Desktop Shell

The unified Quickshell shell is the default desktop shell: one long-running process that draws the top bar, OSD, panels, notifications, lock, launcher, and polkit password dialog. marchyo.shell.enable defaults to on with marchyo.desktop.enable. Setting it to false keeps only hyprlock and hyprpolkitagent, with no bar, OSD or notification daemon.

Option Default Description
marchyo.shell.enable true with desktop Enable the unified Quickshell shell (false keeps the discrete stack)
marchyo.shell.settings {} Runtime config serialized to ~/.config/marchyo/shell.json
marchyo.shell.plugins [] Shell plugins baked into the store shell (build-time)
marchyo.shell.extraPlugins [] Additive plugin specs (pinned git), managed by marchyo plugin
marchyo.shell.diskWarnPercent 10 Free-space percentage of the Nix store filesystem at or below which marchyo.disk appears

marchyo.shell.settings is serialized to a generated, read-only ~/.config/marchyo/shell.json that the shell reads live — a rebuild’s new layout applies without restarting the shell (it is watched, with a short poll to catch atomic replacements). This is the build-time model: the file is generated from your configuration and stays reproducible, but is re-read at runtime.

Recognised keys are typed options; any other key passes through to shell.json unchanged, at every level. Typed keys left unset are omitted from the file, so the shell’s own defaults apply.

Key Type Description
bar.layout.{left,centerBegin,center,centerEnd,right} list of { id; settings?; } Widget entries for that bar section. center stays at the middle of the bar, centerBegin grows leftwards from it and centerEnd rightwards. A section left unset falls back to the shipped default layout.
bar.style "flat" or "segmented" Bar look. segmented fills each cluster of widgets with a two-tone background (the theme’s subtle-background colour and an accent-tinted surface colour) joined by curved edges. Layout is unchanged. Shell default "flat".
idle.screensaver positive int Seconds of idle before the screensaver (shell default 150).
idle.lock positive int Seconds of idle before the lock (shell default 300).
clock.seconds bool Live seconds in the bar clock (shell default false); right-click the clock flips it for the session.
battery.percentage bool Charge number beside the battery glyph (shell default true); right-click the widget flips it for the session.
caffeine.autoVideo bool Hold an idle inhibitor while a video plays (shell default true).
monitors.<output> per-output overrides See Per-monitor overrides.
marchyo.shell.settings = {
bar.layout = {
left = [ { id = "marchyo.session"; } { id = "marchyo.workspaces"; } ];
center = [ { id = "marchyo.clock"; } ];
right = [ { id = "marchyo.network"; } { id = "marchyo.audio"; } { id = "marchyo.battery"; } ];
};
idle = { screensaver = 150; lock = 300; };
};

First-party widget ids: marchyo.session, marchyo.workspaces, marchyo.activeWindow, marchyo.clock, marchyo.screenRecording, marchyo.reminders, marchyo.nightLight, marchyo.tray, marchyo.media, marchyo.dictation, marchyo.caffeine, marchyo.theme, marchyo.dnd, marchyo.keyboardLayout, marchyo.bluetooth, marchyo.network, marchyo.camera, marchyo.mic, marchyo.audio, marchyo.cpu, marchyo.powerProfile, marchyo.peripherals, marchyo.battery, marchyo.tailscale, marchyo.weather, marchyo.lockKeys, marchyo.update, marchyo.disk, marchyo.health, marchyo.controlCenter.

marchyo.screenRecording (visible only while a recording runs) and marchyo.reminders (visible only while marchyo reminder timers are pending) are self-hiding and ship in the default layout; marchyo.nightLight is a warm/neutral toggle, registered but not placed by default — add it to a section to show it. marchyo.tailscale (a VPN status indicator) is also registered but not placed by default; it only works — and only appears — when services.tailscale.enable is on (marchyo’s default), which is what bakes the tailscale CLI into the shell. Clicking it opens a read-only detail panel (this device, exit-node status, and online tailnet peers with their addresses).

marchyo.camera is the camera privacy indicator next to marchyo.mic: it is self-hiding and ships in the default layout. It shows while an app captures from a camera, through PipeWire (a link out of a Video/Source node) or by opening /dev/video* directly (a /proc fd scan every 3 s, run only while PipeWire reports a camera node); the tooltip names the apps. marchyo.lockKeys shows CAPS / NUM while Caps Lock or Num Lock is on, read from the keyboard LEDs in /sys/class/leds. It is registered but not placed by default: it polls those LEDs every 500 ms while placed, since lock-key toggles raise no compositor or sysfs event.

The Network panel gains a Share (QR) button on Wi-Fi networks: it opens an overlay with a scannable QR of the current network’s credentials (rendered as a UTF8 text grid by qrencode; the PSK is read via nmcli -s, falling back to an open-network QR when it is unavailable). marchyo.weather (current conditions from wttr.in, IP-geolocated) is registered but not placed by default; placing it is what starts the periodic outbound fetch, so a bar without it makes no network request.

Three self-hiding indicators are registered but not placed by default. Add the ones you want to a bar section; each starts its own slow probe only when placed.

  • marchyo.update: shows when the nixpkgs revision pinned in the flake lock differs from the running system (marchyo update --check, local, hourly). Run marchyo upgrade or marchyo rebuild to apply it.
  • marchyo.disk: shows when free space on the filesystem holding /nix is at or below marchyo.shell.diskWarnPercent (default 10), checked every 5 minutes.
  • marchyo.health: shows when marchyo doctor reports failing checks (every 15 minutes). The tooltip lists them; a click runs marchyo doctor in a terminal.

The Power panel has a rollback button that runs marchyo rollback in a terminal.

bar.style = "segmented" gives each cluster of widgets (the groups between separators, or a whole section without them) its own filled background. The backgrounds alternate between the theme’s subtle-background colour and its surface colour tinted with the accent, so a theme change recolours them, and neighbouring segments meet in a curved join where the separator rule was. Hovering a clickable widget highlights it with a stronger accent tint. Widget widths and positions are the same as in the default "flat" style.

marchyo.shell.settings = {
bar.style = "segmented";
# Keep the plain look on the TV.
monitors."HDMI-A-1".bar.style = "flat";
};

monitors.<output> overrides the global settings for one output, keyed by the output name hyprctl monitors reports (DP-1, HDMI-A-1, eDP-1, …). The override is merged over the global values for that output only: objects merge key by key, and lists replace the global value whole, so a per-monitor bar.layout.right is that output’s complete right section. Sections the override leaves unset keep the global (or default) layout.

marchyo.shell.settings = {
bar.layout.right = [ { id = "marchyo.network"; } { id = "marchyo.audio"; } { id = "marchyo.battery"; } ];
# A slim bar on the TV: clock only on the right, global left and center.
monitors."HDMI-A-1".bar.layout.right = [ { id = "marchyo.clock"; } ];
};

These keys are always global; a per-monitor value for them is ignored:

  • idle: the seat has one idle timer.
  • caffeine: the seat has one idle inhibitor.
  • monitors: overrides do not nest.

The overrides are part of the same generated shell.json, so a rebuild applies them live like the rest of the file.

marchyo bar edits the layout without a rebuild. It records ops (put, move, remove, set) in $XDG_STATE_HOME/marchyo/shell/bar-layout.json; the shell watches that file and applies the ops over the layout resolved from shell.json (or the default) for every output, so shell.json stays generated and read-only. A moved widget keeps its settings. marchyo bar reset deletes the file, and marchyo runtime restore rewrites it after a lost state directory. marchyo bar background and marchyo bar position are the bar-related runtime switches (marchyo theme bar-background and bar-position). Double-clicking empty bar space steps the background through opaque, glassy and clear.

The centre of the bar has three sections: center (the clock) stays at the middle, and centerBegin and centerEnd grow outwards from its edges.

The marchyo.tray entry takes settings.pinned, a list of tray ids or titles that stay on the bar; every other icon folds behind the · drawer button.

The Control Center is a quick-settings panel: output-volume and microphone sliders with a live microphone input meter (active only while the panel is open and the mic is unmuted), plus tiles for Wi-Fi, Bluetooth, Tailscale, do-not-disturb, caffeine, night light, and the power profile. Each tile shows the live state and flips it on click; its chevron opens the matching detail panel (network, Tailscale, notifications, power), with a back button to return. Tiles the host cannot offer (no Wi-Fi device, no Bluetooth adapter, Tailscale off) are hidden.

Open it with Super + Shift + Q, marchyo shell toggle controlcenter, or the marchyo.controlCenter bar widget at the right end of the default layout. The Tailscale tile needs services.tailscale.enable, and tailscaled accepts connect/disconnect only from root or the tailnet operator. With the shell on, marchyo sets the operator to the first enabled marchyo.users entry (by attribute name) through services.tailscale.extraSetFlags. Tailscale allows one operator; pick another with marchyo.services.tailscale.operator = "alice"; or leave it unmanaged with null. Without an operator the tile reports the refusal.

An Appearance section under the tiles holds a window-gaps toggle, the bar position (top or bottom), the bar background (opaque, glassy or clear), a reduce-motion toggle, and the Themes… and Wallpapers… buttons. They run marchyo theme gaps, marchyo theme bar-position, marchyo theme bar-background, marchyo toggle reduce-motion and open the pickers below. The shell reads the choices from $XDG_STATE_HOME/marchyo/shell/style.json (reduceMotion, uiFont, barPosition, barBackground); a missing key means the declarative default. With the bar at the bottom, the panel cards, tooltips, toasts and tray menus move to the bottom edge; the volume overlay and the window overview keep their top-bar layout.

Super + Ctrl + T opens the theme picker and Super + Ctrl + Shift + W the wallpaper picker (marchyo shell toggle themes and backgrounds). Each shows a filter field, a grid and a large preview of the selected item. The arrow keys move, Enter applies (marchyo theme set or marchyo bg set) and Esc closes. A theme without a preview.png shows a plain tile with its name. Wallpaper thumbnails are cached in $XDG_CACHE_HOME/marchyo/thumbs, keyed by size and modification time; the full image shows until its thumbnail exists. In the launcher, >font lists fonts: start with mono or ui to pick one kind.

A panel opened by a key chord or marchyo shell open <panel> takes keyboard focus; one opened by clicking a bar widget does not. Esc closes the panel, Up, Down and Tab move between its controls, and Enter or Space activates the focused control. Hover highlights are ignored while the keyboard drives and return when the pointer moves. marchyo shell gallery [section] opens a dev panel that shows every shell control in every state.

Elsewhen is a built-in panel: a coastline map (Natural Earth 110m, public domain) with your cities, and one row per city showing local time, weekday, UTC offset, the difference to home and the current temperature. Open it with Super + Ctrl + Alt + E or marchyo shell toggle elsewhen.

  • Click a row to make that city home (click again to return home to the local zone). Offsets are relative to home.
  • Type in the search field (or press /) to look up a place through Open-Meteo geocoding; Enter adds the first result.
  • The people button on a row edits a comma-separated list of names shown under the city.
  • t flips 12 and 24 hour time, Alt + T flips Celsius and Fahrenheit, Esc closes.

Zone offsets come from date with TZ= (the shell’s JavaScript engine has no Intl) and refresh every 5 minutes while the panel is open. The list, home, people and the two toggles persist in the shell’s state directory (elsewhen.json). The panel makes network requests to Open-Meteo only while it is open.

With the shell on, the shell is the session’s polkit agent and hyprpolkitagent is not started. When an app asks for elevated rights (pkexec, a systemd action, a 1Password unlock), a centered dialog shows the request, the account it authenticates as, and a password field on the focused monitor. Enter authenticates and Escape cancels. A wrong password shakes the dialog and lets you retry. With the shell off, hyprpolkitagent stays the agent.

Moving an app to another output (in wiremix, Super + Ctrl + A, or pavucontrol) persists across app and session restarts: WirePlumber remembers each app’s target and restores it when the app plays again. The Audio panel’s device picker sets the default output, which WirePlumber also remembers.

The shell supports third-party plugins using the same manifest.json schema as upstream omarchy, but on a build-time (declarative) model: plugins are Nix-declared, validated, and baked into the store shell. There is no runtime plugin directory and no hot-reload — changing the plugin set is a rebuild.

A plugin is a directory with a manifest.json (schemaVersion: 1, an id outside the reserved marchyo.* namespace, kinds, and entryPoints) plus its QML. Declare one with pkgs.mkMarchyoShellPlugin, which validates the manifest at build time (matching id, kinds, entry points and prefix, safe relative entry points, no symlinks):

marchyo.shell.plugins = [
(pkgs.mkMarchyoShellPlugin {
src = pkgs.fetchgit {
url = "https://example.com/acme-weather.git";
rev = "…";
hash = "…";
};
id = "acme.weather";
kinds = [ "bar-widget" ];
entryPoints = { barWidget = "Panel.qml"; };
})
];

kinds is a closed set, and entryPoints carries exactly one QML file per declared kind:

Kind Entry point key What it does
bar-widget barWidget A bar item, placed from marchyo.shell.settings.bar.layout by its manifest id (e.g. acme.weather)
launcher launcher A launcher provider whose root is a qs.Launcher Provider; typing its prefix in the apps palette routes the query to it
daemon daemon A non-visual Item or QtObject, created once when the shell starts and kept for its lifetime

A launcher plugin sets prefix in both its manifest and the mkMarchyoShellPlugin call. The prefix is required, non-empty, has no whitespace, and must not start with =, >, # or !, which belong to the built-in launcher providers. Plugin ids match [A-Za-z0-9][A-Za-z0-9._-]* without .., and the marchyo.* namespace is reserved. Plugin ids and prefixes must be unique. A launcher or daemon plugin that fails to load is logged and skipped, so the rest of the shell keeps running.

(pkgs.mkMarchyoShellPlugin {
src = ./echo;
id = "acme.echo";
kinds = [ "launcher" ];
prefix = "?";
entryPoints = { launcher = "Provider.qml"; };
})

Every shell build records its plugins in share/marchyo/shell/plugins.lock.json inside the shell package: each plugin’s id, name, version, kinds, entry points, prefix, store path, and source ({ url, rev } for CLI-added plugins, null for flake-declared ones). The file exists with an empty plugins list when no plugins are declared.

marchyo plugin is the CLI leg of the same build-time model. It pins a git source and persists a raw spec into the marchyoCliState sidecar (marchyo.shell.extraPlugins), which is appended to your flake-declared marchyo.shell.plugins at mkDefault priority — so a hand-written plugins list always wins. Every change is baked in by a rebuild; there is no runtime plugin directory.

Terminal window
marchyo plugin add https://example.com/acme-weather.git # pin + bake on rebuild
marchyo plugin add https://example.com/acme-weather.git --rev <sha>
marchyo plugin list # every plugin baked into the shell
marchyo plugin update acme.weather # re-pin at the remote HEAD (or --rev)
marchyo plugin remove acme.weather

add resolves the revision and hash with nix-prefetch-git, reads and validates the repo’s manifest.json (schema version, non-reserved id, entry points), then writes the pinned spec and rebuilds. Pass -n/--dry-run to print the state change without writing or rebuilding.

marchyo plugin validate <dir> runs the build’s manifest checks on a checkout.

The shell ships a compatibility layer (shell/compat, exposed as the QML modules qs.compat.Ui / qs.compat.Commons) that provides the “Quattro” bar-widget host API — BarWidget, Panel, KeyboardPanel, PluginBarApi and the Style/Color/Border tokens — mapped onto marchyo’s own bar, theme, and services. When a bar-widget plugin is baked in, its import qs.Ui / import qs.Commons lines are rewritten to the compat namespace, and the shell injects a marchyo-backed bar facade.