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 the discrete stack instead: waybar, Vicinae, SwayOSD, mako, hyprlock, and hyprpolkitagent.

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.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,center,right} list of { id; settings?; } Widget entries for that bar section. 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).
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.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 marchyo.services.tailscale.enable is set, 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.

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.

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 marchyo.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.

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 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.

The shell ships a compatibility layer (shell/compat, exposed as the QML modules qs.compat.Ui / qs.compat.Commons) that provides omarchy’s “Quattro” plugin host API — BarWidget, Panel, KeyboardPanel, PluginBarApi, the Style/Color/Border tokens, and the omarchy-shell / omarchy-bar CLIs — mapped onto marchyo’s own bar, theme, and services. This lets an upstream omarchy bar widget run near-unmodified: when a 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. The compat CLIs and the runtime tools omarchy plugins call by bare name (python3, notify-send, curl) are added to the shell’s PATH only when plugins are declared, so plugin-less hosts keep a lean closure.

Two omarchy plugins are packaged in this repo and can be imported directly:

marchyo.shell.plugins = [
pkgs.marchyo-shell-plugins.sbb # vvkycodevv.sbb — Swiss transport
pkgs.marchyo-shell-plugins.agent-activity # agf.agent-activity — agent dashboard
];
marchyo.shell.settings.bar.layout.right = [
{ id = "vvkycodevv.sbb"; settings = { homeStation = "Zürich HB"; }; }
{ id = "agf.agent-activity"; }
# … first-party widgets …
];

A plugin popup that persists a setting via omarchy-bar set writes marchyo’s runtime overlay ($XDG_STATE_HOME/marchyo/shell/plugin-settings.json), which is merged over the widget’s inline settings the next time it loads. Hyprland global shortcuts an omarchy plugin registers via hyprctl eval degrade gracefully: marchyo’s Hyprland config has no omarchy hl Lua helper, so the widget stays usable by click while its shortcut registration no-ops.