Switch to running quickshell as a systemd service that is auto-restarted if it crashes
This commit is contained in:
@@ -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`,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 "{}"
|
||||
|
||||
@@ -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
@@ -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" \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
# omarchy:summary=Show the current weather notification
|
||||
|
||||
omarchy-shell-ipc weatherFlyout show
|
||||
omarchy-shell weatherFlyout show
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+141
@@ -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" "$@"
|
||||
@@ -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 -- "$@"
|
||||
@@ -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" "$@"
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \"{}\""))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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 \"{}\"")
|
||||
|
||||
@@ -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" })
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user