Switch to running quickshell as a systemd service that is auto-restarted if it crashes

This commit is contained in:
David Heinemeier Hansson
2026-05-18 13:58:07 +02:00
parent 8424718d1b
commit b0f5d941ba
38 changed files with 735 additions and 244 deletions
+6 -7
View File
@@ -107,9 +107,9 @@ When testing layer-shell UI, capture the reference and candidate states as separ
# Omarchy shell
The Quickshell desktop runs as a single long-running process out of
`default/quickshell/omarchy-shell/`. Hyprland's autostart launches it; do
not start additional standalone `quickshell -p` instances for individual
components.
`default/quickshell/omarchy-shell/`. The `omarchy-shell.service` user unit
keeps it running for the graphical session; do not start additional
standalone `quickshell -p` instances for individual components.
Run `omarchy-restart-shell` after making changes to QML files.
@@ -129,10 +129,9 @@ Plugin contract:
IPC:
- `bin/omarchy-shell-ipc` is the canonical IPC entry point. It starts
the shell on first call, then forwards to `quickshell ipc call`.
Prefer it over re-implementing the wait-for-instance dance in every
CLI.
- `bin/omarchy-shell` is the canonical IPC entry point. It forwards to
the running shell service and does not start it. Prefer it over
re-implementing direct Quickshell socket calls in every CLI.
- The `shell` IPC target exposes `ping`, `summon`, `hide`, `toggle`,
`rescanPlugins`, `setPluginEnabled`, and `listPlugins`. Individual
plugins can register additional IPC targets (the bar registers `bar`,
+1
View File
@@ -63,6 +63,7 @@ GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders"
GROUP_DESCRIPTIONS[remove]="Removal workflows"
GROUP_DESCRIPTIONS[restart]="Restart Omarchy components"
GROUP_DESCRIPTIONS[setup]="Interactive setup wizards"
GROUP_DESCRIPTIONS[shell]="Omarchy shell IPC helpers"
GROUP_DESCRIPTIONS[screensaver]="Screensaver branding and animation"
GROUP_DESCRIPTIONS[snapshot]="System snapshots"
GROUP_DESCRIPTIONS[style]="Global UI style controls"
+1 -1
View File
@@ -255,7 +255,7 @@ stop_screenrecording() {
}
toggle_screenrecording_indicator() {
omarchy-shell-ipc --if-running bar refreshScreenRecording >/dev/null 2>&1 || true
omarchy-shell bar refreshScreenRecording >/dev/null 2>&1 || true
}
screenrecording_active() {
+1
View File
@@ -12,6 +12,7 @@ if [[ -f $FIRST_RUN_MODE ]]; then
bash "$OMARCHY_PATH/install/first-run/battery-monitor.sh"
bash "$OMARCHY_PATH/install/first-run/recover-internal-monitor.sh"
bash "$OMARCHY_PATH/install/first-run/omarchy-shell.sh"
bash "$OMARCHY_PATH/install/first-run/cleanup-reboot-sudoers.sh"
bash "$OMARCHY_PATH/install/first-run/firewall.sh"
bash "$OMARCHY_PATH/install/first-run/dns-resolver.sh"
+1 -1
View File
@@ -2,4 +2,4 @@
# omarchy:summary=Launch the Omarchy bar settings panel
exec omarchy-shell-ipc shell summon omarchy.settings "{}"
exec omarchy-shell shell summon omarchy.settings "{}"
+4 -4
View File
@@ -4,8 +4,8 @@
# omarchy:args=[toggle|summon|close|refresh|ping] [route]
# omarchy:examples=omarchy menu | omarchy menu toggle system | omarchy menu summon style.theme | omarchy menu refresh
# Thin wrapper around `omarchy-shell-ipc menu`. Keybinds and the bar icon
# call omarchy-shell-ipc directly to skip the omarchy-CLI dispatch hop;
# Thin wrapper around `omarchy-shell menu`. Keybinds and the bar icon
# call omarchy-shell directly to skip the omarchy-CLI dispatch hop;
# this exists so humans get `omarchy menu toggle X` etc.
verb="${1-toggle}"
@@ -13,10 +13,10 @@ route="${2-root}"
case "$verb" in
toggle | summon)
exec omarchy-shell-ipc menu "$verb" "$route"
exec omarchy-shell menu "$verb" "$route"
;;
close | refresh | ping)
exec omarchy-shell-ipc menu "$verb"
exec omarchy-shell menu "$verb"
;;
-h | --help | help)
cat <<USAGE
+1 -1
View File
@@ -3,4 +3,4 @@
# omarchy:group=menu
# omarchy:examples=omarchy menu clipboard
omarchy-shell-ipc shell toggle omarchy.clipboard-picker "{}"
omarchy-shell shell toggle omarchy.clipboard-picker "{}"
+1 -1
View File
@@ -3,4 +3,4 @@
# omarchy:group=menu
# omarchy:examples=omarchy menu emoji
omarchy-shell-ipc shell toggle omarchy.emoji-picker "{}"
omarchy-shell shell toggle omarchy.emoji-picker "{}"
+5 -55
View File
@@ -223,67 +223,17 @@ if [[ $cache_only == true || $prepare_only == true ]]; then
exit 0
fi
# Image rows can contain newlines and tabs, which don't survive a positional
# bash argv into `quickshell ipc call`. Base64-encode for transit; the
# ImagePicker plugin Qt.atob()s on the other side.
# Image rows can contain newlines and tabs, which don't survive positional
# shell IPC arguments. Base64-encode for transit; the ImagePicker plugin
# Qt.atob()s on the other side.
rows_b64=$(printf '%s' "$rows" | base64 -w 0)
if [[ $preload == true ]]; then
omarchy-shell-ipc image-selector preload "$rows_b64" "$selected_list_image" "$show_labels" "$filterable" >/dev/null || true
omarchy-shell image-selector preload "$rows_b64" "$selected_list_image" "$show_labels" "$filterable" >/dev/null || true
exit 0
fi
send_builtin_ipc_request() {
perl -MCwd=abs_path -MEncode=encode,decode -MSocket \
-e '
sub read_qstring {
my ($data, $offset) = @_;
return ("", $offset) if $offset + 4 > length($data);
my $length = unpack("N", substr($data, $offset, 4));
$offset += 4;
return ("", $offset) if $length == 0xffffffff || $offset + $length > length($data);
return (decode("UTF-16BE", substr($data, $offset, $length)), $offset + $length);
}
sub qstring {
my $encoded = encode("UTF-16BE", $_[0] // "");
return pack("N", length($encoded)) . $encoded;
}
my $shell_qml = abs_path(shift @ARGV);
my @open_args = @ARGV;
my $runtime_dir = $ENV{"XDG_RUNTIME_DIR"} || "/run/user/$<";
my $payload = chr(3) . qstring("image-selector") . qstring("open") . pack("N", scalar @open_args) . join("", map { qstring($_) } @open_args);
my @candidates;
for my $lock_path (glob("$runtime_dir/quickshell/by-id/*/instance.lock")) {
my $data;
next unless open(my $lock, "<:raw", $lock_path);
{ local $/; $data = <$lock>; }
close($lock);
my (undef, $offset) = read_qstring($data, 0);
my ($path) = read_qstring($data, $offset);
next unless $path && abs_path($path) eq $shell_qml;
(my $socket_path = $lock_path) =~ s{/instance\.lock$}{/ipc.sock};
push @candidates, [(stat($lock_path))[9] || 0, $socket_path];
}
for my $candidate (sort { $b->[0] <=> $a->[0] } @candidates) {
socket(my $client, AF_UNIX, SOCK_STREAM, 0) || next;
if (connect($client, sockaddr_un($candidate->[1]))) {
syswrite($client, $payload);
close($client);
exit 0;
}
close($client);
}
exit 1;
' "$OMARCHY_PATH/default/quickshell/omarchy-shell/shell.qml" \
"" "$rows_b64" "$selected_list_image" "$selection_file" "$done_file" "$show_labels" "$filterable"
}
if ! send_builtin_ipc_request && ! omarchy-shell-ipc image-selector open \
if ! omarchy-shell image-selector open \
"" \
"$rows_b64" \
"$selected_list_image" \
+1 -1
View File
@@ -8,4 +8,4 @@ if (($# == 0)); then
exit 1
fi
omarchy-shell-ipc notifications dismiss "$1" >/dev/null 2>&1 || true
omarchy-shell notifications dismiss "$1" >/dev/null 2>&1 || true
+1 -1
View File
@@ -2,4 +2,4 @@
# omarchy:summary=Show the current weather notification
omarchy-shell-ipc weatherFlyout show
omarchy-shell weatherFlyout show
+1 -1
View File
@@ -34,4 +34,4 @@ payload=$(jq -cn \
--arg max "$max" \
'{icon:$icon,message:$message,value:$value,progressText:$progressText,max:$max}')
omarchy-shell-ipc osd show "$payload" >/dev/null 2>&1 || true
omarchy-shell osd show "$payload" >/dev/null 2>&1 || true
+2 -10
View File
@@ -3,13 +3,5 @@
# omarchy:summary=Restart the Omarchy shell
# omarchy:examples=omarchy restart shell
CONFIG_DIR="$OMARCHY_PATH/default/quickshell/omarchy-shell"
# Skip `quickshell kill` (graceful IPC exit) — it currently trips an upstream
# crash in IpcKillCommand::exec (Quickshell #539), which leaves the Wayland
# surface lingering and produces a duplicate bar when the new instance starts.
# A SIGKILL drops the client connection and the compositor cleans up surfaces
# immediately, with no state to flush since the shell holds nothing persistent.
pkill -KILL -f "quickshell -p $CONFIG_DIR" 2>/dev/null
setsid uwsm-app -- quickshell -p "$CONFIG_DIR" >/dev/null 2>&1 &
systemctl --user enable omarchy-shell.service >/dev/null
systemctl --user restart omarchy-shell.service
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
# omarchy:summary=Send an IPC call to the running Omarchy shell
# omarchy:args=<target> <method> [args...]
# omarchy:examples=omarchy shell shell ping | omarchy-shell menu toggle root
if [[ $# -eq 0 || $1 == "-h" || $1 == "--help" ]]; then
cat <<USAGE
Usage: omarchy-shell <target> <method> [args...]
Forwards an IPC call to the supervised omarchy-shell service. The shell is
expected to already be running; this command does not start it.
Examples:
omarchy-shell shell ping
omarchy-shell shell summon omarchy.settings "{}"
omarchy-shell shell listPlugins
omarchy-shell menu toggle root
USAGE
exit 0
fi
if (( $# < 2 )); then
echo "Usage: omarchy-shell <target> <method> [args...]" >&2
exit 1
fi
if [[ -z ${OMARCHY_PATH:-} ]]; then
echo "OMARCHY_PATH is not set" >&2
exit 1
fi
SHELL_QML="$OMARCHY_PATH/default/quickshell/omarchy-shell/shell.qml"
perl -MCwd=abs_path -MEncode=encode,decode -MSocket -MIO::Handle \
-e '
binmode STDOUT, ":encoding(UTF-8)";
binmode STDERR, ":encoding(UTF-8)";
$SIG{PIPE} = "IGNORE";
sub read_qstring {
my ($data, $offset) = @_;
return (undef, $offset) if $offset + 4 > length($data);
my $length = unpack("N", substr($data, $offset, 4));
$offset += 4;
return (undef, $offset) if $length == 0xffffffff || $offset + $length > length($data);
return (decode("UTF-16BE", substr($data, $offset, $length)), $offset + $length);
}
sub qstring {
my $encoded = encode("UTF-16BE", $_[0] // "");
return pack("N", length($encoded)) . $encoded;
}
sub write_all {
my ($client, $payload) = @_;
my $written = 0;
while ($written < length($payload)) {
my $n = syswrite($client, $payload, length($payload) - $written, $written);
return 0 unless defined $n;
$written += $n;
}
return 1;
}
sub print_success {
my ($response) = @_;
my ($message) = read_qstring($response, 2);
return 0 unless defined $message && length($message);
print $message;
print "\n" unless substr($message, -1) eq "\n";
return 0;
}
my $shell_qml_arg = shift @ARGV;
my $shell_qml = abs_path($shell_qml_arg);
my $target = shift @ARGV;
my $function = shift @ARGV;
my @args = @ARGV;
my $runtime_dir = $ENV{"XDG_RUNTIME_DIR"} || "/run/user/$<";
my $payload = chr(3) . qstring($target) . qstring($function) . pack("N", scalar @args) . join("", map { qstring($_) } @args);
my @candidates;
if (!defined $shell_qml) {
print STDERR "omarchy-shell config not found: $shell_qml_arg\n";
exit 1;
}
for my $lock_path (glob("$runtime_dir/quickshell/by-id/*/instance.lock")) {
my $data;
next unless open(my $lock, "<:raw", $lock_path);
{ local $/; $data = <$lock>; }
close($lock);
my (undef, $offset) = read_qstring($data, 0);
my ($path) = read_qstring($data, $offset);
next unless $path;
my $abs_path = abs_path($path);
next unless defined $abs_path && $abs_path eq $shell_qml;
(my $socket_path = $lock_path) =~ s{/instance\.lock$}{/ipc.sock};
push @candidates, [(stat($lock_path))[9] || 0, $socket_path];
}
for my $candidate (sort { $b->[0] <=> $a->[0] } @candidates) {
socket(my $client, AF_UNIX, SOCK_STREAM, 0) || next;
if (connect($client, sockaddr_un($candidate->[1]))) {
unless (write_all($client, $payload)) {
close($client);
next;
}
my $response = "";
while (1) {
my $buffer = "";
my $n = sysread($client, $buffer, 65536);
last unless defined $n && $n > 0;
$response .= $buffer;
}
close($client);
next unless length($response);
my $code = ord(substr($response, 0, 1));
if ($code == 5) {
print_success($response);
exit 0;
} elsif ($code == 2) {
print STDERR "Target not found.\n";
} elsif ($code == 3) {
print STDERR "Function not found.\n";
} elsif ($code == 4) {
print STDERR "Invalid arguments.\n";
} else {
print STDERR "Unexpected IPC response: $code\n";
}
exit 1;
}
close($client);
}
print STDERR "omarchy-shell is not running\n";
exit 1;
' "$SHELL_QML" "$@"
-61
View File
@@ -1,61 +0,0 @@
#!/bin/bash
# omarchy:summary=Send an IPC call to omarchy-shell, starting it if not running
# omarchy:hidden=true
if [[ $# -eq 0 || $1 == "-h" || $1 == "--help" ]]; then
cat <<USAGE
Usage: omarchy-shell-ipc [--if-running] <target> <method> [args...]
Forwards a quickshell ipc call to omarchy-shell. Starts the shell if it
isn't already running, unless --if-running is passed (in which case the
call is silently skipped when no shell instance exists).
Examples:
omarchy-shell-ipc shell ping
omarchy-shell-ipc shell summon omarchy.settings "{}"
omarchy-shell-ipc shell hide omarchy.image-picker
omarchy-shell-ipc shell listPlugins
omarchy-shell-ipc shell listShellConfig
omarchy-shell-ipc shell rescanPlugins
omarchy-shell-ipc --if-running bar refreshIndicators
omarchy-shell-ipc image-selector ping
omarchy-shell-ipc image-selector cancel ""
USAGE
exit 0
fi
if_running=0
if [[ $1 == "--if-running" ]]; then
if_running=1
shift
fi
SHELL_DIR="$OMARCHY_PATH/default/quickshell/omarchy-shell"
shell_running() {
quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance '
}
if (( if_running )); then
shell_running || exit 0
else
# Serialize concurrent invocations so two CLI callers don't both spawn the shell
# when no instance is running yet.
lockfile="${XDG_RUNTIME_DIR:-/tmp}/omarchy-shell-ipc.lock"
{
flock 9
if ! shell_running; then
# 9<&- so the spawned shell does not inherit (and keep) the lock fd; the
# spawned process is long-lived and would otherwise hold the lock for the
# remainder of the session, deadlocking every subsequent helper invocation.
setsid uwsm-app -- quickshell -p "$SHELL_DIR" >/dev/null 2>&1 9<&- &
for _ in 1 2 3 4 5 6 7 8 9 10; do
sleep 0.2
shell_running && break
done
fi
} 9>"$lockfile"
fi
exec quickshell ipc -p "$SHELL_DIR" call -- "$@"
-60
View File
@@ -1,60 +0,0 @@
#!/bin/bash
# omarchy:summary=Send a fire-and-forget IPC call to a running omarchy-shell
# omarchy:hidden=true
SHELL_QML="$OMARCHY_PATH/default/quickshell/omarchy-shell/shell.qml"
if (( $# < 2 )); then
echo "Usage: omarchy-shell-ipc-fast <target> <function> [args...]" >&2
exit 1
fi
perl -MCwd=abs_path -MEncode=encode,decode -MSocket \
-e '
sub read_qstring {
my ($data, $offset) = @_;
return ("", $offset) if $offset + 4 > length($data);
my $length = unpack("N", substr($data, $offset, 4));
$offset += 4;
return ("", $offset) if $length == 0xffffffff || $offset + $length > length($data);
return (decode("UTF-16BE", substr($data, $offset, $length)), $offset + $length);
}
sub qstring {
my $encoded = encode("UTF-16BE", $_[0] // "");
return pack("N", length($encoded)) . $encoded;
}
my $shell_qml = abs_path(shift @ARGV);
my $target = shift @ARGV;
my $function = shift @ARGV;
my @args = @ARGV;
my $runtime_dir = $ENV{"XDG_RUNTIME_DIR"} || "/run/user/$<";
my $payload = chr(3) . qstring($target) . qstring($function) . pack("N", scalar @args) . join("", map { qstring($_) } @args);
my @candidates;
for my $lock_path (glob("$runtime_dir/quickshell/by-id/*/instance.lock")) {
my $data;
next unless open(my $lock, "<:raw", $lock_path);
{ local $/; $data = <$lock>; }
close($lock);
my (undef, $offset) = read_qstring($data, 0);
my ($path) = read_qstring($data, $offset);
next unless $path && abs_path($path) eq $shell_qml;
(my $socket_path = $lock_path) =~ s{/instance\.lock$}{/ipc.sock};
push @candidates, [(stat($lock_path))[9] || 0, $socket_path];
}
for my $candidate (sort { $b->[0] <=> $a->[0] } @candidates) {
socket(my $client, AF_UNIX, SOCK_STREAM, 0) || next;
if (connect($client, sockaddr_un($candidate->[1]))) {
syswrite($client, $payload);
close($client);
exit 0;
}
close($client);
}
exit 1;
' "$SHELL_QML" "$@"
+1 -1
View File
@@ -22,4 +22,4 @@ ln -nsf "$BACKGROUND" "$CURRENT_BACKGROUND_LINK"
# Update the live shell background immediately when it is running. The
# background plugin also polls this symlink, but IPC avoids the visible delay.
omarchy-shell-ipc-fast background set "$BACKGROUND" >/dev/null 2>&1 || true
omarchy-shell background set "$BACKGROUND" >/dev/null 2>&1 || true
+1 -1
View File
@@ -32,7 +32,7 @@ run_parallel() {
}
shell_ipc() {
timeout 2 omarchy-shell-ipc --if-running "$@" >/dev/null 2>&1
timeout 2 omarchy-shell "$@" >/dev/null 2>&1
}
snapshot_background_path() {
+1 -1
View File
@@ -10,4 +10,4 @@ else
omarchy-notification-send -g "󰾪" "Idle behavior on" "System can now sleep / lock on idle"
fi
omarchy-shell-ipc --if-running bar refreshIndicators >/dev/null 2>&1 || true
omarchy-shell bar refreshIndicators >/dev/null 2>&1 || true
+1 -1
View File
@@ -3,4 +3,4 @@
# omarchy:summary=Toggle notification do-not-disturb mode
state=$(omarchy-shell notifications toggleDnd 2>/dev/null || echo "")
omarchy-shell --if-running bar refreshIndicators >/dev/null 2>&1 || true
omarchy-shell bar refreshIndicators >/dev/null 2>&1 || true
+1 -1
View File
@@ -2,5 +2,5 @@
# omarchy:summary=Ensure the status bar icon offering the available update is removed
omarchy-shell-ipc --if-running bar refreshUpdate >/dev/null 2>&1 || true
omarchy-shell bar refreshUpdate >/dev/null 2>&1 || true
exit 0
+2 -2
View File
@@ -32,9 +32,9 @@ hl.bind("SUPER + SHIFT + ALT + X", hl.dsp.exec_cmd([[omarchy-launch-webapp "http
-- Overwrite existing bindings with hl.unbind() first if needed.
-- hl.unbind("SUPER + SPACE")
-- hl.bind("SUPER + SPACE", hl.dsp.exec_cmd("omarchy-shell-ipc menu toggle root"), { description = "Omarchy menu" })
-- hl.bind("SUPER + SPACE", hl.dsp.exec_cmd("omarchy-shell menu toggle root"), { description = "Omarchy menu" })
-- Logitech MX Keys examples:
-- hl.bind("SUPER + SHIFT + S", hl.dsp.exec_cmd("omarchy-capture-screenshot"))
-- hl.bind("SUPER + H", hl.dsp.exec_cmd("voxtype record toggle"))
-- hl.bind("SUPER + PERIOD", hl.dsp.exec_cmd("omarchy-shell-ipc shell toggle omarchy.emoji-picker \"{}\""))
-- hl.bind("SUPER + PERIOD", hl.dsp.exec_cmd("omarchy-shell shell toggle omarchy.emoji-picker \"{}\""))
+1 -1
View File
@@ -11,7 +11,7 @@
// action Shell command to run. If omitted, the row is a submenu.
// target Existing submenu id to open. Use for links/aliases.
// provider Runtime provider function/command returning JSON rows.
// aliases alternate `omarchy-shell-ipc menu summon <name>` routes; also searchable.
// aliases alternate `omarchy-shell menu summon <name>` routes; also searchable.
// keywords Extra search terms beyond id/label/aliases.
// description Optional subtitle and extra search text.
// when Shell condition; hide row when it fails.
+17
View File
@@ -0,0 +1,17 @@
[Unit]
Description=Omarchy Shell
PartOf=graphical-session.target
After=graphical-session.target
StartLimitIntervalSec=30
StartLimitBurst=5
[Service]
Type=exec
Environment=OMARCHY_PATH=%h/.local/share/omarchy
ExecStart=/usr/bin/quickshell -p %h/.local/share/omarchy/default/quickshell/omarchy-shell
Restart=always
RestartSec=2
Slice=session-graphical.slice
[Install]
WantedBy=graphical-session.target
+5 -5
View File
@@ -1,15 +1,15 @@
hl.on("hyprland.start", function()
hl.exec_cmd("omarchy-restart-shell")
-- Slow app launch fix -- set systemd vars before starting session services.
hl.exec_cmd("systemctl --user import-environment $(env | cut -d'=' -f 1)")
hl.exec_cmd("dbus-update-activation-environment --systemd --all")
hl.exec_cmd("systemctl --user start omarchy-shell.service")
hl.exec_cmd("uwsm-app -- hypridle")
hl.exec_cmd("uwsm-app -- fcitx5 --disable notificationitem")
hl.exec_cmd("omarchy-first-run")
hl.exec_cmd("omarchy-powerprofiles-init")
hl.exec_cmd("uwsm-app -- omarchy-hyprland-monitor-watch")
-- Slow app launch fix -- set systemd vars.
hl.exec_cmd("systemctl --user import-environment $(env | cut -d'=' -f 1)")
hl.exec_cmd("dbus-update-activation-environment --systemd --all")
-- Run post-boot hooks after startup config has loaded.
hl.exec_cmd("sleep 2 && omarchy-hook post-boot")
end)
+1 -1
View File
@@ -13,4 +13,4 @@ end
o.bind("SUPER + C", "Universal copy", send_shortcut_once("CTRL", "Insert"))
o.bind("SUPER + V", "Universal paste", send_shortcut_once("SHIFT", "Insert"))
o.bind("SUPER + X", "Universal cut", send_shortcut_once("CTRL", "X"))
o.bind("SUPER + CTRL + V", "Clipboard manager", "omarchy-shell-ipc shell toggle omarchy.clipboard-picker \"{}\"")
o.bind("SUPER + CTRL + V", "Clipboard manager", "omarchy-shell shell toggle omarchy.clipboard-picker \"{}\"")
+17 -17
View File
@@ -1,12 +1,12 @@
hl.bind("SUPER + SPACE", hl.dsp.exec_cmd("omarchy-launch-walker"), { description = "Launch apps" })
hl.bind("SUPER + CTRL + E", hl.dsp.exec_cmd("omarchy-shell-ipc shell toggle omarchy.emoji-picker \"{}\""), { description = "Emoji picker" })
hl.bind("SUPER + CTRL + C", hl.dsp.exec_cmd("omarchy-shell-ipc menu toggle capture"), { description = "Capture menu" })
hl.bind("SUPER + CTRL + O", hl.dsp.exec_cmd("omarchy-shell-ipc menu toggle toggle"), { description = "Toggle menu" })
hl.bind("SUPER + CTRL + H", hl.dsp.exec_cmd("omarchy-shell-ipc menu toggle hardware"), { description = "Hardware menu" })
hl.bind("SUPER + ALT + SPACE", hl.dsp.exec_cmd("omarchy-shell-ipc menu toggle root"), { description = "Omarchy menu" })
hl.bind("SUPER + SHIFT + code:201", hl.dsp.exec_cmd("omarchy-shell-ipc menu toggle root"), { description = "Omarchy menu" })
hl.bind("SUPER + ESCAPE", hl.dsp.exec_cmd("omarchy-shell-ipc menu toggle system"), { description = "System menu" })
hl.bind("XF86PowerOff", hl.dsp.exec_cmd("omarchy-shell-ipc menu toggle system"), { locked = true, description = "Power menu" })
hl.bind("SUPER + CTRL + E", hl.dsp.exec_cmd("omarchy-shell shell toggle omarchy.emoji-picker \"{}\""), { description = "Emoji picker" })
hl.bind("SUPER + CTRL + C", hl.dsp.exec_cmd("omarchy-shell menu toggle capture"), { description = "Capture menu" })
hl.bind("SUPER + CTRL + O", hl.dsp.exec_cmd("omarchy-shell menu toggle toggle"), { description = "Toggle menu" })
hl.bind("SUPER + CTRL + H", hl.dsp.exec_cmd("omarchy-shell menu toggle hardware"), { description = "Hardware menu" })
hl.bind("SUPER + ALT + SPACE", hl.dsp.exec_cmd("omarchy-shell menu toggle root"), { description = "Omarchy menu" })
hl.bind("SUPER + SHIFT + code:201", hl.dsp.exec_cmd("omarchy-shell menu toggle root"), { description = "Omarchy menu" })
hl.bind("SUPER + ESCAPE", hl.dsp.exec_cmd("omarchy-shell menu toggle system"), { description = "System menu" })
hl.bind("XF86PowerOff", hl.dsp.exec_cmd("omarchy-shell menu toggle system"), { locked = true, description = "Power menu" })
hl.bind("SUPER + K", hl.dsp.exec_cmd("omarchy-menu-keybindings"), { description = "Show key bindings" })
hl.bind("SUPER + ALT + K", hl.dsp.exec_cmd("omarchy-menu-tmux-keybindings"), { description = "Show Tmux key bindings" })
hl.bind("XF86Calculator", hl.dsp.exec_cmd("gnome-calculator"), { description = "Calculator" })
@@ -16,18 +16,18 @@ hl.bind("SUPER + SHIFT + CTRL + UP", hl.dsp.exec_cmd("omarchy-style-bar-position
hl.bind("SUPER + SHIFT + CTRL + DOWN", hl.dsp.exec_cmd("omarchy-style-bar-position bottom"), { description = "Move bar to bottom" })
hl.bind("SUPER + SHIFT + CTRL + LEFT", hl.dsp.exec_cmd("omarchy-style-bar-position left"), { description = "Move bar to left" })
hl.bind("SUPER + SHIFT + CTRL + RIGHT", hl.dsp.exec_cmd("omarchy-style-bar-position right"), { description = "Move bar to right" })
hl.bind("SUPER + CTRL + SPACE", hl.dsp.exec_cmd("omarchy-shell-ipc menu toggle background"), { description = "Background switcher" })
hl.bind("SUPER + SHIFT + CTRL + SPACE", hl.dsp.exec_cmd("omarchy-shell-ipc menu toggle theme"), { description = "Theme menu" })
hl.bind("SUPER + CTRL + SPACE", hl.dsp.exec_cmd("omarchy-shell menu toggle background"), { description = "Background switcher" })
hl.bind("SUPER + SHIFT + CTRL + SPACE", hl.dsp.exec_cmd("omarchy-shell menu toggle theme"), { description = "Theme menu" })
hl.bind("SUPER + BACKSPACE", hl.dsp.exec_cmd("omarchy-hyprland-window-transparency-toggle"), { description = "Toggle window transparency" })
hl.bind("SUPER + SHIFT + BACKSPACE", hl.dsp.exec_cmd("omarchy-hyprland-window-gaps-toggle"), { description = "Toggle window gaps" })
hl.bind("SUPER + CTRL + BACKSPACE", hl.dsp.exec_cmd("omarchy-hyprland-window-single-square-aspect-toggle"), { description = "Toggle single-window square aspect" })
-- xkbcommon names the comma keysym "comma"; the upper-case "COMMA" does not match.
hl.bind("SUPER + comma", hl.dsp.exec_cmd("omarchy-shell-ipc notifications dismissOne"), { description = "Dismiss last notification" })
hl.bind("SUPER + SHIFT + comma", hl.dsp.exec_cmd("omarchy-shell-ipc notifications dismissAll"), { description = "Dismiss all notifications" })
hl.bind("SUPER + comma", hl.dsp.exec_cmd("omarchy-shell notifications dismissOne"), { description = "Dismiss last notification" })
hl.bind("SUPER + SHIFT + comma", hl.dsp.exec_cmd("omarchy-shell notifications dismissAll"), { description = "Dismiss all notifications" })
hl.bind("SUPER + CTRL + comma", hl.dsp.exec_cmd("omarchy-toggle-notification-silencing"), { description = "Toggle silencing notifications" })
hl.bind("SUPER + ALT + comma", hl.dsp.exec_cmd("omarchy-shell-ipc notifications invokeLast"), { description = "Invoke last notification" })
hl.bind("SUPER + SHIFT + ALT + comma", hl.dsp.exec_cmd("omarchy-shell-ipc notifications showHistory"), { description = "Open notification history" })
hl.bind("SUPER + ALT + comma", hl.dsp.exec_cmd("omarchy-shell notifications invokeLast"), { description = "Invoke last notification" })
hl.bind("SUPER + SHIFT + ALT + comma", hl.dsp.exec_cmd("omarchy-shell notifications showHistory"), { description = "Open notification history" })
hl.bind("SUPER + CTRL + I", hl.dsp.exec_cmd("omarchy-toggle-idle"), { description = "Toggle locking on idle" })
hl.bind("SUPER + CTRL + N", hl.dsp.exec_cmd("omarchy-toggle-nightlight"), { description = "Toggle nightlight" })
@@ -37,15 +37,15 @@ hl.bind("switch:on:Lid Switch", hl.dsp.exec_cmd("omarchy-hw-external-monitors &&
hl.bind("switch:off:Lid Switch", hl.dsp.exec_cmd("omarchy-hyprland-monitor-internal on"), { locked = true })
hl.bind("PRINT", hl.dsp.exec_cmd("omarchy-capture-screenshot"), { description = "Screenshot" })
hl.bind("ALT + PRINT", hl.dsp.exec_cmd("omarchy-capture-screenrecording --stop-recording || omarchy-shell-ipc menu toggle trigger.capture.screenrecord"), { description = "Screenrecording" })
hl.bind("ALT + PRINT", hl.dsp.exec_cmd("omarchy-capture-screenrecording --stop-recording || omarchy-shell menu toggle trigger.capture.screenrecord"), { description = "Screenrecording" })
hl.bind("SUPER + PRINT", hl.dsp.exec_cmd("pkill hyprpicker || hyprpicker -a"), { description = "Color picker" })
hl.bind("SUPER + CTRL + PRINT", hl.dsp.exec_cmd("omarchy-capture-text-extraction"), { description = "Extract text (OCR) from screenshot" })
hl.bind("SUPER + CTRL + S", hl.dsp.exec_cmd("omarchy-shell-ipc menu toggle share"), { description = "Share" })
hl.bind("SUPER + CTRL + S", hl.dsp.exec_cmd("omarchy-shell menu toggle share"), { description = "Share" })
hl.bind("SUPER + CTRL + PERIOD", hl.dsp.exec_cmd("omarchy-transcode"), { description = "Transcode" })
hl.bind("SUPER + CTRL + R", hl.dsp.exec_cmd("omarchy-shell-ipc menu toggle reminder-set"), { description = "Set reminder" })
hl.bind("SUPER + CTRL + R", hl.dsp.exec_cmd("omarchy-shell menu toggle reminder-set"), { description = "Set reminder" })
hl.bind("SUPER + CTRL + ALT + R", hl.dsp.exec_cmd("omarchy-reminder show"), { description = "Show reminders" })
hl.bind("SUPER + SHIFT + CTRL + R", hl.dsp.exec_cmd("omarchy-reminder clear"), { description = "Clear reminders" })
+1 -1
View File
@@ -7,7 +7,7 @@
// Dotted IDs define the tree. Use provider:"name" only when the submenu
// calls provider_name() or a command named "name" to return JSON rows.
// Optional fields:
// aliases alternate `omarchy-shell-ipc menu summon <name>` routes; also searchable
// aliases alternate `omarchy-shell menu summon <name>` routes; also searchable
// keywords extra search terms beyond id/label/aliases
// when shell condition; hide row when it fails
// checked shell condition; append ✓ when it succeeds
+218
View File
@@ -0,0 +1,218 @@
# Omarchy shell
`omarchy-shell` is a single long-running [Quickshell](https://quickshell.org/)
instance that hosts the Omarchy desktop. A supervised user systemd service
keeps one shell running per graphical session; everything else — the bar,
the bar settings UI, the background switcher, future panels and overlays —
runs **inside** the shell as a plugin.
Hosting everything inside one shell means:
- shared services and singletons live once, not once per process
- summoning a panel is an IPC call into a process that is already running,
not a fresh `quickshell -p ...` cold start
- third-party plugins can be loaded from disk without changing any source
code in Omarchy itself
The runtime layout in this branch:
```
default/quickshell/omarchy-shell/
shell.qml entry point (ShellRoot)
shell-defaults.json canonical out-of-the-box config
services/
PluginRegistry.qml discovers, validates plugins, looks up enabled state in shell.json
BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p)
ui/
settings/
DynamicSettingsForm.qml renders plugin-declared schemas
plugins/
bar/ first-party plugins (see plugins/README.md)
settings/
image-picker/
menu/
notifications/
osd/
polkit/
```
The plugin discovery path is documented in [plugins/README.md](plugins/README.md).
## Plugin manifest
Every plugin ships a `manifest.json` describing what it is and how the
shell should load it. Minimal example:
```json
{
"schemaVersion": 1,
"id": "my.org.cool-clock",
"name": "Cool clock",
"version": "1.0.0",
"author": "You",
"description": "A clock that does cool things",
"kinds": ["bar-widget"],
"activation": "on-demand",
"entryPoints": { "barWidget": "Widget.qml" },
"barWidget": {
"displayName": "Cool clock",
"category": "Time",
"allowMultiple": false,
"defaults": { "format": "HH:mm" },
"schema": [
{ "key": "format", "type": "string", "label": "Format" }
]
}
}
```
Supported `kinds`:
| Kind | What it is |
|--------------|--------------------------------------------------------------|
| `bar-widget` | A component that the bar can drop into a section |
| `panel` | A persistent or summoned floating window (e.g. bar settings) |
| `overlay` | A fullscreen overlay (e.g. background switcher) |
| `menu` | A summoned menu surface |
| `service` | A headless singleton, no UI |
| `bar` | Reserved for the first-party bar host (`omarchy.bar`). Third-party plugins should ship `bar-widget`s; they do not replace the host bar. |
`activation` is either `persistent` (loaded on startup, never unloaded) or
`on-demand` (loaded by `shell summon <id>` and unloaded by `shell hide`).
Plugins that need to outlive a single summon can set `keepLoaded: true`
(e.g. the image picker keeps its overlay window mounted between
summons).
The full schema lives in `services/PluginRegistry.qml`.
## Installing a third-party plugin
1. Drop the plugin into `~/.config/omarchy/plugins/<plugin-id>/`.
The directory must contain a `manifest.json` plus the QML files
referenced from its `entryPoints`.
2. `omarchy-shell shell rescanPlugins`.
3. Enable the plugin with `omarchy-shell shell setPluginEnabled <id> true`.
4. If it's a `bar-widget`, add it to a layout section from the bar editor.
First-party plugins under `default/quickshell/omarchy-shell/plugins/`
are discovered the same way and cannot be disabled.
## IPC contract
The shell exposes a single `shell` IPC target plus whatever extra targets
individual plugins register (e.g. the bar's `bar` target for refresh
hooks, the image picker's `image-selector` target). `omarchy-menu` uses the
shell target to summon the first-party `omarchy.menu` plugin instead of
running a separate Quickshell instance.
| Method | Returns | Effect |
|------------------------------------------|---------|-------------------------------------------------------|
| `ping` | `ok` | health check |
| `summon <id> <payloadJson>` | `ok` / `unknown` | load + open a panel/overlay plugin |
| `hide <id>` | — | close a previously-summoned plugin |
| `toggle <id> <payloadJson>` | — | summon if closed, hide if open |
| `rescanPlugins` | — | re-walk plugin dirs and pick up new/changed manifests |
| `setPluginEnabled <id> <enabled>` | — | flip the persisted enabled bit (see note) |
| `listPlugins` | JSON | every discovered plugin (id, name, kinds, enabled) |
Direct invocation:
```
quickshell ipc -p $OMARCHY_PATH/default/quickshell/omarchy-shell call shell ping
```
The `omarchy-shell.service` user unit starts the shell for the graphical
session and restarts it if it exits. Use `omarchy-restart-shell` to reload
the long-running shell process.
A convenience wrapper, [`omarchy-shell`](../../../bin/omarchy-shell),
forwards IPC calls to the running service. It does not start the shell; the
systemd unit owns the shell lifecycle.
```
omarchy-shell shell ping
omarchy-shell shell summon omarchy.settings "{}"
omarchy-shell shell listPlugins
omarchy-shell shell rescanPlugins
```
**Note on `setPluginEnabled`:** the `enabled` argument is a string. Only the
literal `"true"` enables the plugin; every other value (including `"True"`,
`"1"`, `"yes"`, or omitted) disables it. This keeps the IPC surface
type-stable across QML's `string`-only IPC arguments.
## Persisted state
There is one user config file. Everything that distinguishes your
customization from the shipped defaults lives in it.
| Path | Owner | Purpose |
|-----------------------------------|----------------|--------------------------------------------------------|
| `~/.config/omarchy/shell.json` | the shell | full layout + per-entry settings + enabled plugin list |
| `~/.config/omarchy/plugins/<id>/` | user | drop-in third-party plugin source files |
The `shell-defaults.json` bundled with the shell describes the
fresh-install state. When the user has no `shell.json`, the shell uses
the defaults verbatim. Once the user customizes anything, `shell.json`
becomes the authoritative file — we do **not** deep-merge defaults back
in. Pressing **Reset bar to defaults** in `omarchy launch bar settings`
rewrites the `bar` subtree from the current `shell-defaults.json`.
### shell.json shape
```json
{
"version": 1,
"bar": {
"position": "top",
"transparent": false,
"centerAnchor": "calendar",
"fontFamily": "JetBrainsMono Nerd Font",
"layout": {
"left": [ { "id": "omarchy" }, { "id": "workspaces" } ],
"center": [ { "id": "calendar", "format": "HH:mm" } ],
"right": [
{ "id": "audioPanel" }
]
}
},
"plugins": [
{ "id": "omarchy.settings" },
{ "id": "omarchy.image-picker" }
]
}
```
### Storage rules
1. **Every plugin instance is one entry.** Either in `bar.layout.<section>`
for bar widgets, or in `plugins[]` for panels, overlays, services,
menus, and anything else non-bar.
2. **Settings are inline on the entry.** No `config:` sub-object, no
separate per-plugin settings file, no merge layers. The fields on each
entry are the values the plugin sees.
3. **Enabled ⇔ present.** A plugin is enabled iff its id appears somewhere
in shell.json. For bar widgets, the bar settings UI adds/removes layout
entries; other plugin kinds are enabled with the shell IPC.
4. **Multiple instances** are allowed when a manifest sets
`allowMultiple: true`. Each instance is independent — e.g. two clocks
in different timezones are just two `{"id":"calendar", "timezone": ...}`
entries with their own values.
5. **`version: 1` is required** at the top level. The shell will fall back
to defaults rather than load an unknown version.
## Implementation history
Built up in phases on this branch:
- Phase 1 — `omarchy-shell phase 1: host the existing bar in a single shell`
- Phase 2 — `omarchy-shell phase 2: plugin registry and bar widget registry`
- Phase 3 — `omarchy-shell phase 3: fold bar-settings into the shell as a panel plugin`
- Phase 4 — `omarchy-shell phase 4: absorb background-switcher as a plugin`
- Phase 5 — `omarchy-shell phase 5: docs, cleanup, and migration crumbs`
- Phase 6 — `omarchy-shell phase 6: reviewer cleanup (path traversal, collision, races)`
- Phase 7 — `omarchy-shell phase 7: replace socket with IpcHandler, rename to image-picker`
- Phase 8a — `omarchy-shell phase 8a: unified shell.json with inline plugin settings`
Shared services and Pipewire/UPower/Hyprland consolidation are explicitly
out of scope here and deferred to a follow-up after a review pass.
@@ -0,0 +1,99 @@
# First-party plugins
These plugins ship with Omarchy and are loaded by the shell at startup.
They use the same `manifest.json` contract as third-party plugins; the
only difference is that the shell flags them with `__isFirstParty: true`
so they cannot be disabled.
User-installed plugins live alongside these conceptually but on disk under
`~/.config/omarchy/plugins/<plugin-id>/` rather than in this directory.
| Plugin | id | kinds | activation | entry point |
|---------------|-------------------------|-----------|------------|-------------------------------------|
| Bar | `omarchy.bar` | `bar` | persistent | `bar/Bar.qml` |
| Bar settings | `omarchy.settings` | `panel` | on-demand | `settings/SettingsPanel.qml` |
| Image picker | `omarchy.image-picker` | `overlay` | on-demand | `image-picker/ImagePicker.qml` |
| Emoji picker | `omarchy.emoji-picker` | `overlay` | on-demand | `emoji-picker/EmojiPicker.qml` |
| Clipboard mgr | `omarchy.clipboard-picker`| `overlay` | on-demand | `clipboard-picker/ClipboardPicker.qml`|
| Omarchy menu | `omarchy.menu` | `menu` | on-demand | `menu/Menu.qml` |
| Notifications | `omarchy.notifications` | `service` | persistent | `notifications/Service.qml` |
| OSD | `omarchy.osd` | `panel` | persistent | `osd/Osd.qml` |
| Polkit agent | `omarchy.polkit` | `service` | persistent | `polkit/PolkitAgent.qml` |
## Bar
The status bar. Mounted at startup, lives forever. Layout lives in the
top-level `bar:` subtree of `~/.config/omarchy/shell.json` (with the shell
providing [`shell-defaults.json`](../shell-defaults.json) when the user has
no file). Owns the `bar` IPC target for refresh hooks fired by indicator
scripts. See [`bar/README.md`](bar/README.md) for the widget catalogue
and customization schema.
## Bar settings
Visual editor for the bar layout. Summoned by
`omarchy-shell shell summon omarchy.settings "{}"` (which is what
`omarchy launch bar settings` ultimately calls). Provides:
- bar position and center-anchor controls
- per-section add/move/remove/edit of bar widget entries
- dynamic per-widget settings forms that write inline back to the
corresponding shell.json entry
## Image picker
Fullscreen image-grid selector overlay. Used by `omarchy-menu-images`
(wallpaper picker) and `omarchy-theme-switcher` (theme picker) and any
other caller that wants to present a directory of images with previews.
Two ways to drive it:
- Shell-level summon: `omarchy-shell shell summon omarchy.image-picker '<jsonPayload>'`.
The payload can carry `imageDirs`, `imageRows`, `selectedImage`,
`selectionFile`, `doneFile`, `showLabels`, `filterable`. Best for
in-shell callers that already speak JSON.
- Direct IPC target: `omarchy-shell image-selector open <imageDirs> <imageRowsB64> <selectedImage> <selectionFile> <doneFile> <showLabels> <filterable>`.
Positional args; `imageRowsB64` is base64-encoded so embedded newlines /
tabs survive the bash argv handoff. This is what `omarchy-menu-images`
uses. Colors come from the central shell theme singleton; there is no
per-call override surface.
The selection round-trip remains file-based: callers create a
`selection_file` and `done_file` (both `mktemp`), pass the paths, and
poll `done_file` for existence. The plugin writes the chosen path into
`selection_file` and touches `done_file` when it's done. `cancel` IPC
clears it without writing a selection.
The plugin has `keepLoaded: true` so the layer-shell window survives
between summons within a single shell session.
## Polkit agent
Theme-aware authentication dialog for privileged actions. It uses
Quickshell's native `Quickshell.Services.Polkit.PolkitAgent` backend and
runs inside the long-lived `omarchy-shell` process, replacing the old
`polkit-gnome-authentication-agent-1` autostart.
## Omarchy menu
Quickshell-powered replacement for the legacy Walker-driven `omarchy-menu`.
The menu UI lives in `menu/Menu.qml` as a first-party `menu` plugin and is
summoned through the shell (`omarchy-shell shell summon omarchy.menu ...`),
so it shares the long-running `omarchy-shell` process instead of starting a
second Quickshell instance.
The menu definition lives outside the shell host code:
- defaults: `default/omarchy/omarchy-menu.jsonc`
- user extensions: `~/.config/omarchy/extensions/omarchy-menu.jsonc`
The shell parses both JSONC files at startup (with `watchChanges: true`
so edits take effect without a restart), evaluates `when:` / `checked:`
bash expressions in a single batched subprocess, and executes the
selected `action:` string directly via `Quickshell.execDetached`. The
long-running shell process keeps the parsed menu in memory, so the
keybind → IPC → visible path costs ~30ms cold.
## Coming soon
- `omarchy.theme-switcher` — folds theme switching into the shell.
@@ -1126,7 +1126,7 @@ Item {
horizontalMargin: 7.5
onPressed: function(button) {
if (button === Qt.RightButton) root.run("xdg-terminal-exec")
else root.run("omarchy-shell-ipc menu toggle root")
else root.run("omarchy-shell menu toggle root")
}
}
@@ -1632,7 +1632,7 @@ Item {
tooltipText: root.batteryTooltip()
onPressed: function(button) {
if (button === Qt.RightButton) root.run("omarchy-notification-send \"$(omarchy-battery-status)\"")
else root.run("omarchy-shell-ipc menu toggle power")
else root.run("omarchy-shell menu toggle power")
}
}
}
@@ -0,0 +1,174 @@
# Omarchy bar
This is the Quickshell implementation of the Omarchy status bar. It is
shipped as a first-party plugin of [`omarchy-shell`](../../README.md), the
long-running shell host. The bar is mounted at startup and lives inside
the shell for its whole session.
- `manifest.json` declares the plugin (`id: omarchy.bar`, `kind: bar`, `activation: persistent`) and points at `Bar.qml` as the entry point.
- `Bar.qml` is Omarchy-owned bar engine code, loaded by the omarchy-shell host. Users should not edit it directly.
- `widgets/` holds first-party widgets — modular, interactive components shipped with Omarchy.
- `common/` holds shared QML helpers (buttons, sliders, popup cards).
- The bar receives its config from the host shell as a `barConfig` property; the host loads it from `~/.config/omarchy/shell.json` (or `shell-defaults.json` when the user has no file).
- `omarchy-style-bar-position` updates only the user shell.json file.
## Customizing
The bar config lives under the `bar:` key of [`~/.config/omarchy/shell.json`](../../README.md#shelljson-shape). Out of the box the shell uses [`shell-defaults.json`](../../shell-defaults.json). Once you customize anything via `omarchy launch bar settings` or by editing shell.json directly, your file is canonical — there is no deep-merge.
Launch the visual editor with `omarchy launch bar settings` (or run `omarchy-launch-bar-settings`) to reorder widgets, add/remove them, and tweak per-widget options without editing JSON by hand. You can also right-click empty space to the left or right of the centered clock to open it; double-left-click the same empty space to toggle bar transparency.
Example `shell.json` (bar subtree only shown):
```json
{
"version": 1,
"bar": {
"position": "top",
"transparent": false,
"centerAnchor": "calendar",
"layout": {
"left": [
{ "id": "omarchy" },
{ "id": "spacer", "size": 12 },
{ "id": "workspaces" }
],
"center": [
{ "id": "media" },
{ "id": "calendar", "format": "HH:mm" }
],
"right": [
{ "id": "audioPanel" },
{ "id": "battery" }
]
}
}
}
```
`centerAnchor` pins one center module to the exact horizontal/vertical center and flanks others around it. Set to an empty string to disable anchoring (the center list is centered as a group).
## Module catalogue
### First-party interactive widgets (in `widgets/`)
| Name | What it does | Interactions |
|---|---|---|
| `media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup |
| `audioPanel` | Volume icon + popup with master slider, output-device picker, per-app mixer | left = popup · right = mute · middle = audio TUI · scroll = volume |
| `networkPanel` | Wi-Fi/Ethernet icon + popup with Wi-Fi scan, signal, connect, DNS provider selection | left = popup · right = nmtui |
| `bluetoothPanel` | Bluetooth icon + popup with device list, connect/disconnect, battery | left = popup · right = toggle radio · middle = bluetoothctl TUI |
| `calendar` | Clock + popup with month-grid calendar | left = popup · right = tz selector |
| `notificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND |
| `systemStats` | Inline CPU + memory sparklines, popup with detail | left = popup · right = terminal |
| `weatherFlyout` | Weather icon + popup with forecast | left = popup · right = full notification |
| `idleInhibitor` | Coffee-cup that toggles `omarchy-toggle-idle` | left = toggle |
| `microphone` | Mic icon + scroll volume | left = mute toggle · middle = audio TUI · scroll = source volume |
### Built-in legacy modules (in `shell.qml`)
`omarchy`, `workspaces`, `clock`, `weather`, `update`, `voxtype`, `screenRecording`, `idle`, `notifications`, `tray`, `bluetooth`, `network`, `audio`, `cpu`, `battery`.
These remain available — set them in `layout` to use them instead of the richer widget versions.
## Orientation
All widgets work in `top`, `bottom`, `left`, and `right` positions. Popups anchor on the side opposite the bar edge, sliding into the workspace. Vertical bars use 28px width; widgets that show text fall back to compact icon-only forms (e.g. `media` hides its scrolling label).
## Custom user modules
The schema accepts arbitrary module ids that you provide. Set `type` to `command` for shell-driven output or `qml` for a custom QML widget. Both still go under `bar.layout.<section>` in `shell.json`.
Command module:
```json
{
"version": 1,
"bar": {
"layout": {
"right": [
{ "id": "tray" },
{ "id": "vpn", "type": "command", "exec": "~/.config/omarchy/bar/scripts/vpn-status", "interval": 5, "tooltip": "VPN", "onClick": "nm-connection-editor" },
{ "id": "audioPanel" }
]
}
}
}
```
The command may print plain text or Waybar-style JSON, for example:
```json
{"text":"󰌆","tooltip":"Work VPN","class":"active"}
```
QML module:
```json
{
"version": 1,
"bar": {
"layout": {
"right": [
{ "id": "gpu", "type": "qml" },
{ "id": "audioPanel" }
]
}
}
}
```
Then create `~/.config/omarchy/bar/modules/gpu.qml`. If you want to store it elsewhere, add a `source` path.
Custom QML modules should be an `Item` with `implicitWidth` and `implicitHeight`. They may optionally define these properties, which the bar fills after loading:
```qml
import QtQuick
Item {
property var bar
property string moduleName
property var settings
implicitWidth: 28
implicitHeight: bar ? bar.barSize : 26
Text {
anchors.centerIn: parent
text: "GPU"
color: bar ? bar.foreground : "white"
font.family: bar ? bar.fontFamily : "monospace"
font.pixelSize: 12
}
MouseArea {
anchors.fill: parent
onClicked: if (bar) bar.run("omarchy-launch-or-focus-tui btop")
}
}
```
## Bar properties available to widgets
Widgets receive `bar` (the shell root), `moduleName` (string), and `settings` (object) injected at load time. The bar exposes:
- `bar.foreground`, `bar.background`, `bar.urgent` — theme colors (live-updated)
- `bar.fontFamily` — current monospace family
- `bar.position``"top" | "bottom" | "left" | "right"`
- `bar.vertical` — boolean shortcut
- `bar.barSize` — 26 horizontal / 28 vertical
- `bar.run(command)` — fire-and-forget bash exec
- `bar.shellQuote(value)` — safe shell-quote a string
- `bar.showTooltip(target, text)` / `bar.hideTooltip(target)` — shared tooltip popup
- `bar.requestPopout(owner)` / `bar.releasePopout(owner)` — one-popup-at-a-time coordinator
First-party widgets live in `widgets/<name>.qml` and are picked up by the
shell's `BarWidgetRegistry` at startup; reference one by `id` in any
layout list.
Third-party widgets ship as separate plugins under
`~/.config/omarchy/plugins/<plugin-id>/` with their own `manifest.json`
declaring `kinds: ["bar-widget"]` and a `barWidget` entry point. See
[../../README.md](../../README.md) for the manifest schema. Enable or
rescan third-party plugins with `omarchy-shell shell setPluginEnabled`
and `omarchy-shell shell rescanPlugins`.
@@ -92,7 +92,7 @@ Item {
}
}
// Service-side IPC (omarchy-shell-ipc notifications showHistory) flips
// Service-side IPC (omarchy-shell notifications showHistory) flips
// historyOpenRequested; we toggle our local popup state from here so the
// keybind path lands in the same PopupCard the click path uses.
Connections {
@@ -14,7 +14,7 @@ Item {
property var pluginRegistry: null
// Plugin lifecycle hooks. The host calls open(payloadJson) after
// `omarchy-shell-ipc shell summon omarchy.menu ...` and close() when hidden.
// `omarchy-shell shell summon omarchy.menu ...` and close() when hidden.
property string pendingInitialMenu: "root"
function open(payloadJson) {
@@ -616,7 +616,7 @@ Item {
// ----------------------------------------------------------- IPC surface
//
// `omarchy-shell-ipc menu summon <id>` is the keybind hot path — the
// `omarchy-shell menu summon <id>` is the keybind hot path — the
// Hyprland bindings call straight into here, no bash hops. `refresh` is
// a manual nudge for when watchChanges isn't enough (e.g. someone wired
// a CI step that re-emits the JSONC).
@@ -646,7 +646,7 @@ Item {
var id = root.resolveRoute(initialMenu)
var entry = root.items[id]
// If the resolved id is an action (i.e. the user invoked an alias for
// a leaf, e.g. `omarchy-shell-ipc menu summon screenrecord-stop`),
// a leaf, e.g. `omarchy-shell menu summon screenrecord-stop`),
// run it directly instead of opening an action with no children.
if (entry && entry.kind === "action" && entry.action) {
Quickshell.execDetached(["bash", "-lc", entry.action])
@@ -46,7 +46,7 @@ Item {
readonly property int liveBarSize: shell && shell.bar && !shell.bar.barHidden ? Math.max(0, shell.bar.barSize) : defaultBarSize
readonly property int barClearance: liveBarSize + 12
// Fired by IPC (`omarchy-shell-ipc notifications showHistory`) so the
// Fired by IPC (`omarchy-shell notifications showHistory`) so the
// bar widget can drop its PopupCard from the same anchor a click would.
signal historyOpenRequested()
@@ -1,6 +1,6 @@
#!/bin/bash
state=$(omarchy-shell-ipc notifications isDnd 2>/dev/null || echo off)
state=$(omarchy-shell notifications isDnd 2>/dev/null || echo off)
if [[ $state == "on" ]]; then
echo '{"text": "󰂛", "tooltip": "Notifications silenced", "class": "active"}'
else
+4
View File
@@ -0,0 +1,4 @@
mkdir -p ~/.config/systemd/user
cp "$OMARCHY_PATH/config/systemd/user/omarchy-shell.service" ~/.config/systemd/user/omarchy-shell.service
systemctl --user daemon-reload
systemctl --user enable --now omarchy-shell.service
+16
View File
@@ -0,0 +1,16 @@
echo "Run omarchy-shell as a supervised user service"
mkdir -p ~/.config/systemd/user
cp "$OMARCHY_PATH/config/systemd/user/omarchy-shell.service" ~/.config/systemd/user/omarchy-shell.service
systemctl --user daemon-reload
for file in ~/.config/hypr/bindings.lua ~/.config/hypr/bindings/*.lua; do
[[ -f $file ]] || continue
sed -i 's/omarchy-shell-ipc-fast/omarchy-shell/g; s/omarchy-shell-ipc/omarchy-shell/g; s/omarchy-shell[[:space:]]\+--if-running/omarchy-shell/g' "$file"
done
if ! systemctl --user is-active --quiet omarchy-shell.service && omarchy-cmd-present quickshell; then
quickshell kill -p "$OMARCHY_PATH/default/quickshell/omarchy-shell" >/dev/null 2>&1 || true
fi
systemctl --user enable --now omarchy-shell.service || true