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.
Options
Section titled “Options”| 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 |
Runtime configuration
Section titled “Runtime configuration”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
Section titled “Bar style”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";};Per-monitor overrides
Section titled “Per-monitor overrides”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.
Control Center
Section titled “Control Center”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.
Polkit authentication
Section titled “Polkit authentication”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.
Audio routing
Section titled “Audio routing”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.
Plugins
Section titled “Plugins”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.
Adding plugins with the CLI
Section titled “Adding plugins with the CLI”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.
marchyo plugin add https://example.com/acme-weather.git # pin + bake on rebuildmarchyo plugin add https://example.com/acme-weather.git --rev <sha>marchyo plugin list # every plugin baked into the shellmarchyo plugin update acme.weather # re-pin at the remote HEAD (or --rev)marchyo plugin remove acme.weatheradd 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.
omarchy plugin compatibility
Section titled “omarchy plugin compatibility”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.