diff --git a/AGENTS.md b/AGENTS.md index 63ef0897..26cf032e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`, diff --git a/bin/omarchy b/bin/omarchy index 6f5b0413..8c1fbed2 100755 --- a/bin/omarchy +++ b/bin/omarchy @@ -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" diff --git a/bin/omarchy-capture-screenrecording b/bin/omarchy-capture-screenrecording index 75beedc3..5532b4aa 100755 --- a/bin/omarchy-capture-screenrecording +++ b/bin/omarchy-capture-screenrecording @@ -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() { diff --git a/bin/omarchy-first-run b/bin/omarchy-first-run index 9f48eb3d..737e1a3b 100755 --- a/bin/omarchy-first-run +++ b/bin/omarchy-first-run @@ -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" diff --git a/bin/omarchy-launch-bar-settings b/bin/omarchy-launch-bar-settings index 08aa584a..6f59409a 100755 --- a/bin/omarchy-launch-bar-settings +++ b/bin/omarchy-launch-bar-settings @@ -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 "{}" diff --git a/bin/omarchy-menu b/bin/omarchy-menu index bc3b30ff..e947995e 100755 --- a/bin/omarchy-menu +++ b/bin/omarchy-menu @@ -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 </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" \ diff --git a/bin/omarchy-notification-dismiss b/bin/omarchy-notification-dismiss index 5779dc02..20651510 100755 --- a/bin/omarchy-notification-dismiss +++ b/bin/omarchy-notification-dismiss @@ -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 diff --git a/bin/omarchy-notification-weather b/bin/omarchy-notification-weather index 9c6dbf31..52835cdd 100755 --- a/bin/omarchy-notification-weather +++ b/bin/omarchy-notification-weather @@ -2,4 +2,4 @@ # omarchy:summary=Show the current weather notification -omarchy-shell-ipc weatherFlyout show +omarchy-shell weatherFlyout show diff --git a/bin/omarchy-osd b/bin/omarchy-osd index 3090bd87..ed3ef72a 100755 --- a/bin/omarchy-osd +++ b/bin/omarchy-osd @@ -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 diff --git a/bin/omarchy-restart-shell b/bin/omarchy-restart-shell index d6e06f5f..d00be603 100755 --- a/bin/omarchy-restart-shell +++ b/bin/omarchy-restart-shell @@ -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 diff --git a/bin/omarchy-shell b/bin/omarchy-shell new file mode 100755 index 00000000..128d5d2f --- /dev/null +++ b/bin/omarchy-shell @@ -0,0 +1,141 @@ +#!/bin/bash + +# omarchy:summary=Send an IPC call to the running Omarchy shell +# omarchy:args= [args...] +# omarchy:examples=omarchy shell shell ping | omarchy-shell menu toggle root + +if [[ $# -eq 0 || $1 == "-h" || $1 == "--help" ]]; then + cat < [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 [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" "$@" diff --git a/bin/omarchy-shell-ipc b/bin/omarchy-shell-ipc deleted file mode 100755 index 1a634c37..00000000 --- a/bin/omarchy-shell-ipc +++ /dev/null @@ -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 < [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 -- "$@" diff --git a/bin/omarchy-shell-ipc-fast b/bin/omarchy-shell-ipc-fast deleted file mode 100755 index 7a37a707..00000000 --- a/bin/omarchy-shell-ipc-fast +++ /dev/null @@ -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 [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" "$@" diff --git a/bin/omarchy-theme-bg-set b/bin/omarchy-theme-bg-set index 427262ce..f31d554b 100755 --- a/bin/omarchy-theme-bg-set +++ b/bin/omarchy-theme-bg-set @@ -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 diff --git a/bin/omarchy-theme-set b/bin/omarchy-theme-set index 3ffcfbaa..5ce64f81 100755 --- a/bin/omarchy-theme-set +++ b/bin/omarchy-theme-set @@ -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() { diff --git a/bin/omarchy-toggle-idle b/bin/omarchy-toggle-idle index 757f04b9..9685f047 100755 --- a/bin/omarchy-toggle-idle +++ b/bin/omarchy-toggle-idle @@ -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 diff --git a/bin/omarchy-toggle-notification-silencing b/bin/omarchy-toggle-notification-silencing index 965af896..c3787a18 100755 --- a/bin/omarchy-toggle-notification-silencing +++ b/bin/omarchy-toggle-notification-silencing @@ -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 diff --git a/bin/omarchy-update-available-reset b/bin/omarchy-update-available-reset index d034fdbe..5c8cf30e 100755 --- a/bin/omarchy-update-available-reset +++ b/bin/omarchy-update-available-reset @@ -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 diff --git a/config/hypr/bindings.lua b/config/hypr/bindings.lua index 3a6c76e5..ced13f91 100644 --- a/config/hypr/bindings.lua +++ b/config/hypr/bindings.lua @@ -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 \"{}\"")) diff --git a/config/omarchy/extensions/omarchy-menu.jsonc b/config/omarchy/extensions/omarchy-menu.jsonc index bf32d350..d989e1f3 100644 --- a/config/omarchy/extensions/omarchy-menu.jsonc +++ b/config/omarchy/extensions/omarchy-menu.jsonc @@ -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 ` routes; also searchable. + // aliases alternate `omarchy-shell menu summon ` 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. diff --git a/config/systemd/user/omarchy-shell.service b/config/systemd/user/omarchy-shell.service new file mode 100644 index 00000000..30fc93e6 --- /dev/null +++ b/config/systemd/user/omarchy-shell.service @@ -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 diff --git a/default/hypr/autostart.lua b/default/hypr/autostart.lua index b1053cad..14700187 100644 --- a/default/hypr/autostart.lua +++ b/default/hypr/autostart.lua @@ -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) diff --git a/default/hypr/bindings/clipboard.lua b/default/hypr/bindings/clipboard.lua index 8a4b034f..cd3256a5 100644 --- a/default/hypr/bindings/clipboard.lua +++ b/default/hypr/bindings/clipboard.lua @@ -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 \"{}\"") diff --git a/default/hypr/bindings/utilities.lua b/default/hypr/bindings/utilities.lua index f126fa38..eb5533a2 100644 --- a/default/hypr/bindings/utilities.lua +++ b/default/hypr/bindings/utilities.lua @@ -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" }) diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index 925dbc3c..479c944b 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -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 ` routes; also searchable + // aliases alternate `omarchy-shell menu summon ` 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 diff --git a/default/quickshell/omarchy-shell/README.md b/default/quickshell/omarchy-shell/README.md new file mode 100644 index 00000000..b81af154 --- /dev/null +++ b/default/quickshell/omarchy-shell/README.md @@ -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 ` 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//`. + 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 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 ` | `ok` / `unknown` | load + open a panel/overlay plugin | +| `hide ` | — | close a previously-summoned plugin | +| `toggle ` | — | summon if closed, hide if open | +| `rescanPlugins` | — | re-walk plugin dirs and pick up new/changed manifests | +| `setPluginEnabled ` | — | 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//` | 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.
` + 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. diff --git a/default/quickshell/omarchy-shell/plugins/README.md b/default/quickshell/omarchy-shell/plugins/README.md new file mode 100644 index 00000000..948a2e9d --- /dev/null +++ b/default/quickshell/omarchy-shell/plugins/README.md @@ -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//` 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 ''`. + 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 `. + 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. diff --git a/default/quickshell/omarchy-shell/plugins/bar/Bar.qml b/default/quickshell/omarchy-shell/plugins/bar/Bar.qml index 3b634c03..45218a84 100644 --- a/default/quickshell/omarchy-shell/plugins/bar/Bar.qml +++ b/default/quickshell/omarchy-shell/plugins/bar/Bar.qml @@ -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") } } } diff --git a/default/quickshell/omarchy-shell/plugins/bar/README.md b/default/quickshell/omarchy-shell/plugins/bar/README.md new file mode 100644 index 00000000..3bc72c1d --- /dev/null +++ b/default/quickshell/omarchy-shell/plugins/bar/README.md @@ -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.
` 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/.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//` 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`. diff --git a/default/quickshell/omarchy-shell/plugins/bar/widgets/notificationCenter.qml b/default/quickshell/omarchy-shell/plugins/bar/widgets/notificationCenter.qml index 15cbf08e..2ce4c703 100644 --- a/default/quickshell/omarchy-shell/plugins/bar/widgets/notificationCenter.qml +++ b/default/quickshell/omarchy-shell/plugins/bar/widgets/notificationCenter.qml @@ -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 { diff --git a/default/quickshell/omarchy-shell/plugins/menu/Menu.qml b/default/quickshell/omarchy-shell/plugins/menu/Menu.qml index 1231c456..3c0c38dc 100644 --- a/default/quickshell/omarchy-shell/plugins/menu/Menu.qml +++ b/default/quickshell/omarchy-shell/plugins/menu/Menu.qml @@ -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 ` is the keybind hot path — the + // `omarchy-shell menu summon ` 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]) diff --git a/default/quickshell/omarchy-shell/plugins/notifications/Service.qml b/default/quickshell/omarchy-shell/plugins/notifications/Service.qml index 5d5ca495..f38ff9d8 100644 --- a/default/quickshell/omarchy-shell/plugins/notifications/Service.qml +++ b/default/quickshell/omarchy-shell/plugins/notifications/Service.qml @@ -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() diff --git a/default/quickshell/omarchy-shell/scripts/indicators/notification-silencing.sh b/default/quickshell/omarchy-shell/scripts/indicators/notification-silencing.sh index f981b402..7f8c5e8b 100755 --- a/default/quickshell/omarchy-shell/scripts/indicators/notification-silencing.sh +++ b/default/quickshell/omarchy-shell/scripts/indicators/notification-silencing.sh @@ -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 diff --git a/install/first-run/omarchy-shell.sh b/install/first-run/omarchy-shell.sh new file mode 100644 index 00000000..2d1636b6 --- /dev/null +++ b/install/first-run/omarchy-shell.sh @@ -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 diff --git a/migrations/1779047753.sh b/migrations/1779047753.sh new file mode 100644 index 00000000..4ba65754 --- /dev/null +++ b/migrations/1779047753.sh @@ -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