diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index 44b2dd66..fe5aa14a 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -83,8 +83,6 @@ selection_file=$(mktemp) done_file=$(mktemp) rm -f "$done_file" trap 'rm -f "$selection_file" "$done_file"' EXIT -socket_path="${XDG_RUNTIME_DIR:-/run/user/$UID}/omarchy-image-selector.sock" -shell_dir="$OMARCHY_PATH/default/quickshell/omarchy-shell" image_dirs_env="" for dir in "${image_dirs[@]}"; do @@ -214,59 +212,37 @@ else fi fi -rows_payload=${rows//$'\t'/$'\f'} -rows_payload=${rows_payload//$'\n'/$'\v'} -colors_file=${colors_file:-$HOME/.config/omarchy/current/theme/background-switcher-colors.json} +colors_file=${colors_file:-$HOME/.config/omarchy/current/theme/image-picker-colors.json} colors_payload="" if [[ -f $colors_file ]]; then colors_payload=$(<"$colors_file") - colors_payload=${colors_payload//$'\t'/$'\f'} - colors_payload=${colors_payload//$'\n'/$'\v'} fi if [[ $cache_only == true || $prepare_only == true ]]; then exit 0 fi -ensure_selector() { - # The image-selector socket is owned by omarchy-shell. Make sure the shell is - # alive; once it is, the BackgroundSwitcher plugin keeps the socket bound for - # the lifetime of the shell. - if [[ ! -S $socket_path ]]; then - omarchy-shell-ipc shell ping >/dev/null 2>&1 || true +# Image rows and the raw colors blob can contain newlines and tabs, which +# don't survive a positional bash argv into `quickshell ipc call`. Base64 +# them; the ImagePicker plugin Qt.atob()s on the other side. +rows_b64=$(printf '%s' "$rows" | base64 -w 0) +colors_b64=$(printf '%s' "$colors_payload" | base64 -w 0) - for ((i = 0; i < 100; i++)); do - if [[ -S $socket_path ]]; then - break - fi - - sleep 0.01 - done - fi - - [[ -S $socket_path ]] -} - -send_request() { - printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$rows_payload" "$selected_list_image" "$selection_file" "$done_file" "$colors_payload" "$show_labels" "$filterable" | - socat -u - "UNIX-CONNECT:$socket_path" -} - -if ! ensure_selector; then - echo "Image selector failed to start" >&2 +if ! omarchy-shell-ipc image-selector open \ + "" \ + "$rows_b64" \ + "$selected_list_image" \ + "$selection_file" \ + "$done_file" \ + "" \ + "$colors_b64" \ + "$show_labels" \ + "$filterable" >/dev/null; then + echo "Image selector failed to accept request" >&2 exit 1 fi -if ! send_request; then - rm -f "$socket_path" - - if ! ensure_selector || ! send_request; then - echo "Image selector failed to accept request" >&2 - exit 1 - fi -fi - while [[ ! -e $done_file ]]; do sleep 0.01 done diff --git a/bin/omarchy-shell-ipc b/bin/omarchy-shell-ipc index 6ecec8a0..6533ebb4 100755 --- a/bin/omarchy-shell-ipc +++ b/bin/omarchy-shell-ipc @@ -12,9 +12,11 @@ Starts omarchy-shell if not running, then forwards a quickshell ipc call. Examples: omarchy-shell-ipc shell ping omarchy-shell-ipc shell summon omarchy.bar-settings "{}" - omarchy-shell-ipc shell hide omarchy.background-switcher + omarchy-shell-ipc shell hide omarchy.image-picker omarchy-shell-ipc shell listPlugins omarchy-shell-ipc shell rescanPlugins + omarchy-shell-ipc image-selector ping + omarchy-shell-ipc image-selector cancel "" USAGE exit 0 fi @@ -28,7 +30,10 @@ lockfile="${XDG_RUNTIME_DIR:-/tmp}/omarchy-shell-ipc.lock" { flock 9 if ! quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance '; then - setsid uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$SHELL_DIR" >/dev/null 2>&1 & + # 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 -- env OMARCHY_PATH="$OMARCHY_PATH" 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 quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance ' && break diff --git a/default/quickshell/omarchy-shell/README.md b/default/quickshell/omarchy-shell/README.md index 00c966cf..db65be2a 100644 --- a/default/quickshell/omarchy-shell/README.md +++ b/default/quickshell/omarchy-shell/README.md @@ -28,7 +28,7 @@ default/quickshell/omarchy-shell/ plugins/ bar/ first-party plugins (see plugins/README.md) bar-settings/ - background-switcher/ + image-picker/ ``` The plugin discovery path is documented in [plugins/README.md](plugins/README.md). @@ -73,8 +73,9 @@ Supported `kinds`: `activation` is either `persistent` (loaded on startup, never unloaded) or `on-demand` (loaded by `shell summon ` and unloaded by `shell hide`). -Plugins that need their IPC socket to outlive a single summon can set -`keepLoaded: true` (e.g. background-switcher's legacy unix socket). +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`. @@ -147,7 +148,9 @@ Built up in phases on this branch: - 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` (this commit) +- 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` 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 index 08353ddb..d3f270dd 100644 --- a/default/quickshell/omarchy-shell/plugins/README.md +++ b/default/quickshell/omarchy-shell/plugins/README.md @@ -8,11 +8,11 @@ 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.bar-settings` | `panel` | on-demand | `bar-settings/BarSettingsPanel.qml` | -| Background switcher | `omarchy.background-switcher` | `overlay` | on-demand | `background-switcher/BackgroundSwitcher.qml` | +| Plugin | id | kinds | activation | entry point | +|------------------|--------------------------|--------------|-------------|----------------------------------------------| +| Bar | `omarchy.bar` | `bar` | persistent | `bar/Bar.qml` | +| Bar settings | `omarchy.bar-settings` | `panel` | on-demand | `bar-settings/BarSettingsPanel.qml` | +| Image picker | `omarchy.image-picker` | `overlay` | on-demand | `image-picker/ImagePicker.qml` | ## Bar @@ -33,13 +33,31 @@ Visual editor for the bar layout. Summoned by - a Plugin Manager tab for enabling/disabling third-party plugins - a dynamic settings form driven by each widget's manifest schema -## Background switcher +## Image picker -Fullscreen wallpaper / image picker overlay. Summoned for ad-hoc wallpaper -selection. Keeps its legacy unix socket protocol at -`/run/user//omarchy-image-selector.sock` so existing callers like -`omarchy-menu-images` keep working without any wire-format change. The -plugin has `keepLoaded: true` so the socket survives between summons. +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-ipc shell summon omarchy.image-picker ''`. + The payload can carry `imageDirs`, `imageRows`, `selectedImage`, + `selectionFile`, `doneFile`, `colorsFile`, `colorsRaw`, `showLabels`, + `filterable`. Best for in-shell callers that already speak JSON. +- Direct IPC target: `omarchy-shell-ipc image-selector open `. + Positional args; `imageRowsB64` and `colorsRawB64` are base64-encoded so + embedded newlines / tabs survive the bash argv handoff. This is what + `omarchy-menu-images` uses. + +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. ## Coming soon diff --git a/default/quickshell/omarchy-shell/plugins/background-switcher/manifest.json b/default/quickshell/omarchy-shell/plugins/background-switcher/manifest.json deleted file mode 100644 index c335c7fc..00000000 --- a/default/quickshell/omarchy-shell/plugins/background-switcher/manifest.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schemaVersion": 1, - "id": "omarchy.background-switcher", - "name": "Background switcher", - "version": "1.0.0", - "author": "Omarchy", - "description": "Wallpaper / image picker overlay", - "kinds": ["overlay"], - "activation": "on-demand", - "keepLoaded": true, - "entryPoints": { "overlay": "BackgroundSwitcher.qml" }, - "ipc": { - "summon": "background-switcher", - "legacySocket": "omarchy-image-selector.sock" - } -} diff --git a/default/quickshell/omarchy-shell/plugins/background-switcher/BackgroundSwitcher.qml b/default/quickshell/omarchy-shell/plugins/image-picker/ImagePicker.qml similarity index 90% rename from default/quickshell/omarchy-shell/plugins/background-switcher/BackgroundSwitcher.qml rename to default/quickshell/omarchy-shell/plugins/image-picker/ImagePicker.qml index 096c0b06..4633145b 100644 --- a/default/quickshell/omarchy-shell/plugins/background-switcher/BackgroundSwitcher.qml +++ b/default/quickshell/omarchy-shell/plugins/image-picker/ImagePicker.qml @@ -22,7 +22,7 @@ Item { property string imageRows: "" property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_BACKGROUND_SELECTION_FILE") property string selectedImage: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTED") - property string colorsFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_COLORS_FILE") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/background-switcher-colors.json") + property string colorsFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_COLORS_FILE") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/image-picker-colors.json") property int selectedIndex: 0 property bool imagesLoaded: false property bool opened: false @@ -34,7 +34,6 @@ Item { property string doneFile: "" property string filterText: "" property var doneFilesToRelease: [] - property string socketPath: (Quickshell.env("XDG_RUNTIME_DIR") || ("/run/user/" + Quickshell.env("UID"))) + "/omarchy-image-selector.sock" property color accent: "#798186" property color background: "#101315" property color foreground: "#cacccc" @@ -53,8 +52,13 @@ Item { return "'" + String(value).replace(/'/g, "'\\''") + "'" } - function decodeField(value) { - return String(value || "").replace(/\v/g, "\n").replace(/\f/g, "\t") + // Decode a base64-encoded UTF-8 string sent via IPC. Used for fields that + // would otherwise carry embedded newlines or tabs (image rows, raw colors + // JSON) which bash IPC arguments can't reliably round-trip. + function decodeBase64(value) { + var s = String(value || "") + if (!s) return "" + try { return Qt.atob(s) } catch (e) { return s } } function withAlpha(color, alpha) { @@ -251,7 +255,7 @@ Item { showLabels = nextShowLabels === true || nextShowLabels === "true" filterable = nextFilterable === true || nextFilterable === "true" filterText = "" - colorsFile = nextColorsFile || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/background-switcher-colors.json") + colorsFile = nextColorsFile || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/image-picker-colors.json") if (nextColorsRaw) loadColors(nextColorsRaw) imageModel.clear() @@ -303,16 +307,11 @@ Item { } } - // The plugin is keep-loaded inside omarchy-shell, so the env-driven - // auto-open path that the standalone background-switcher.qml used would - // now fire once at shell startup with stale env. The legacy callers - // (omarchy-menu-images) deliver their request over the unix socket - // declared below; modern callers go through `shell summon` -> open(payload). - - // Lifecycle hooks invoked by omarchy-shell summon/hide. The legacy entry - // point remains the unix socket below — callers that already have a - // selection_file/done_file flow keep using it. summon() with no payload - // simply opens the picker against the user's current theme backgrounds. + // Lifecycle hooks invoked by omarchy-shell summon/hide. shell.summon(id, + // payloadJson) hands the JSON to open() here; shell.hide(id) calls close(). + // External CLI callers can either go through `shell summon omarchy.image- + // picker` (JSON payload), or hit the dedicated `image-selector` IpcHandler + // below for the lower-level positional call that omarchy-menu-images uses. function open(payload) { var args = {} if (payload) { @@ -334,39 +333,44 @@ Item { cancel() } + // IPC surface. All arguments are strings (Quickshell IPC marshalling). + // imageRows and colorsRaw can contain newlines/tabs, so the CLI caller + // base64-encodes them; everything else passes through verbatim. The two + // boolean-like fields use the literal strings "true" or "false". IpcHandler { target: "image-selector" - function open(imageDirs: string, imageRows: string, selectedImage: string, selectionFile: string, doneFile: string, colorsFile: string): void { - root.openSelector(imageDirs, imageRows, selectedImage, selectionFile, doneFile, colorsFile, "", false, false) + function open(imageDirs: string, + imageRowsB64: string, + selectedImage: string, + selectionFile: string, + doneFile: string, + colorsFile: string, + colorsRawB64: string, + showLabels: string, + filterable: string): string { + var rows = root.decodeBase64(imageRowsB64) + var colorsRaw = root.decodeBase64(colorsRawB64) + root.openSelector(imageDirs, rows, selectedImage, selectionFile, doneFile, + colorsFile, colorsRaw, showLabels, filterable) + return "ok" } - } - SocketServer { - active: true - path: root.socketPath - - handler: Socket { - id: clientSocket - parser: SplitParser { - onRead: function(message) { - var fields = message.split("\t") - if (root.opened) { - root.closeSelector(fields[3] || "") - clientSocket.connected = false - return - } + function cancel(doneFile: string): void { + root.closeSelector(doneFile || "") + } - root.openSelector("", root.decodeField(fields[0]), fields[1] || "", fields[2] || "", fields[3] || "", "", root.decodeField(fields[4]), fields[5] || "false", fields[6] || "false") - clientSocket.connected = false - } - } + function ping(): string { + return "ok" } } + // Tolerate the file being absent (e.g. theme templates not yet re-rendered + // after the rename) without a startup warning. FileView { path: root.colorsFile watchChanges: true + printErrors: false onLoaded: root.loadColors(text()) onFileChanged: { reload(); root.loadColors(text()) } } diff --git a/default/quickshell/omarchy-shell/plugins/image-picker/manifest.json b/default/quickshell/omarchy-shell/plugins/image-picker/manifest.json new file mode 100644 index 00000000..3c356421 --- /dev/null +++ b/default/quickshell/omarchy-shell/plugins/image-picker/manifest.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": 1, + "id": "omarchy.image-picker", + "name": "Image picker", + "version": "1.0.0", + "author": "Omarchy", + "description": "Image-grid selector overlay used for wallpapers, themes, and any other directory of images", + "kinds": ["overlay"], + "activation": "on-demand", + "keepLoaded": true, + "entryPoints": { "overlay": "ImagePicker.qml" }, + "ipc": { + "summon": "image-picker" + } +} diff --git a/default/themed/background-switcher-colors.json.tpl b/default/themed/image-picker-colors.json.tpl similarity index 100% rename from default/themed/background-switcher-colors.json.tpl rename to default/themed/image-picker-colors.json.tpl