From da822f6ffb750696cca59c6657f6ff05d61d37f0 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 13:39:30 +0200 Subject: [PATCH 01/27] Change from swaybg to skwd-wall Nicer picker and support for movie bgs --- bin/omarchy-first-run | 1 + bin/omarchy-restart-skwd-wall | 5 ++ bin/omarchy-theme-bg-next | 9 +-- bin/omarchy-theme-bg-set | 5 +- bin/omarchy-theme-set | 1 + bin/omarchy-theme-set-skwd-wall | 14 +++++ config/skwd-wall/config.json | 79 ++++++++++++++++++++++++ default/hypr/autostart.lua | 1 - default/themed/skwd-wall-colors.json.tpl | 27 ++++++++ install/first-run/skwd-wall.sh | 2 + install/omarchy-base.packages | 2 +- migrations/1778494413.sh | 16 +++++ 12 files changed, 151 insertions(+), 11 deletions(-) create mode 100755 bin/omarchy-restart-skwd-wall create mode 100755 bin/omarchy-theme-set-skwd-wall create mode 100644 config/skwd-wall/config.json create mode 100644 default/themed/skwd-wall-colors.json.tpl create mode 100644 install/first-run/skwd-wall.sh create mode 100644 migrations/1778494413.sh diff --git a/bin/omarchy-first-run b/bin/omarchy-first-run index 97b41f80..277b0b2d 100755 --- a/bin/omarchy-first-run +++ b/bin/omarchy-first-run @@ -18,6 +18,7 @@ if [[ -f $FIRST_RUN_MODE ]]; then bash "$OMARCHY_PATH/install/first-run/gnome-theme.sh" bash "$OMARCHY_PATH/install/first-run/gdk-scale.sh" bash "$OMARCHY_PATH/install/first-run/swayosd.sh" + bash "$OMARCHY_PATH/install/first-run/skwd-wall.sh" bash "$OMARCHY_PATH/install/first-run/gtk-primary-paste.sh" bash "$OMARCHY_PATH/install/first-run/elephant.sh" omarchy-hook-install post-update "$OMARCHY_PATH/install/first-run/install-voxtype.hook" diff --git a/bin/omarchy-restart-skwd-wall b/bin/omarchy-restart-skwd-wall new file mode 100755 index 00000000..c83521d6 --- /dev/null +++ b/bin/omarchy-restart-skwd-wall @@ -0,0 +1,5 @@ +#!/bin/bash + +# omarchy:summary=Restart Skwd-wall wallpaper daemon + +systemctl --user restart skwd-daemon.service 2>/dev/null || true diff --git a/bin/omarchy-theme-bg-next b/bin/omarchy-theme-bg-next index e2d0bd4a..a559431e 100755 --- a/bin/omarchy-theme-bg-next +++ b/bin/omarchy-theme-bg-next @@ -12,9 +12,7 @@ mapfile -d '' -t BACKGROUNDS < <(find -L "$USER_BACKGROUNDS_PATH" "$THEME_BACKGR TOTAL=${#BACKGROUNDS[@]} if (( TOTAL == 0 )); then - notify-send "No background was found for theme" -t 2000 - pkill -x swaybg - setsid uwsm-app -- swaybg --color '#000000' >/dev/null 2>&1 & + omarchy-notification-send "No background was found for theme" -t 2000 else # Get current background from symlink if [[ -L $CURRENT_BACKGROUND_LINK ]]; then @@ -45,7 +43,6 @@ else # Set new background symlink ln -nsf "$NEW_BACKGROUND" "$CURRENT_BACKGROUND_LINK" - # Relaunch swaybg - pkill -x swaybg - setsid uwsm-app -- swaybg -i "$CURRENT_BACKGROUND_LINK" -m fill >/dev/null 2>&1 & +# Apply the new background through Skwd-wall +skwd wall apply "{\"type\":\"static\",\"path\":\"$CURRENT_BACKGROUND_LINK\"}" >/dev/null fi diff --git a/bin/omarchy-theme-bg-set b/bin/omarchy-theme-bg-set index 9c695d0e..1eb00009 100755 --- a/bin/omarchy-theme-bg-set +++ b/bin/omarchy-theme-bg-set @@ -20,6 +20,5 @@ fi # Create symlink to the new background ln -nsf "$BACKGROUND" "$CURRENT_BACKGROUND_LINK" -# Kill existing swaybg and start new one -pkill -x swaybg -setsid uwsm-app -- swaybg -i "$CURRENT_BACKGROUND_LINK" -m fill >/dev/null 2>&1 & +# Apply the new background through Skwd-wall +skwd wall apply "{\"type\":\"static\",\"path\":\"$CURRENT_BACKGROUND_LINK\"}" >/dev/null diff --git a/bin/omarchy-theme-set b/bin/omarchy-theme-set index fe57c2af..a10d6c27 100755 --- a/bin/omarchy-theme-set +++ b/bin/omarchy-theme-set @@ -68,6 +68,7 @@ omarchy-theme-set-browser omarchy-theme-set-vscode omarchy-theme-set-obsidian omarchy-theme-set-keyboard +omarchy-theme-set-skwd-wall # Call hook on theme set omarchy-hook theme-set "$THEME_NAME" >/dev/null diff --git a/bin/omarchy-theme-set-skwd-wall b/bin/omarchy-theme-set-skwd-wall new file mode 100755 index 00000000..787908eb --- /dev/null +++ b/bin/omarchy-theme-set-skwd-wall @@ -0,0 +1,14 @@ +#!/bin/bash + +# omarchy:summary=Refresh Skwd-wall after an Omarchy theme change +# omarchy:hidden=true + +THEME_COLORS="$HOME/.config/omarchy/current/theme/skwd-wall-colors.json" +SKWD_COLORS="$HOME/.cache/skwd-wall/colors.json" + +if [[ -f $THEME_COLORS ]]; then + mkdir -p "$(dirname "$SKWD_COLORS")" + cp "$THEME_COLORS" "$SKWD_COLORS" +fi + +skwd wall cache_rebuild >/dev/null diff --git a/config/skwd-wall/config.json b/config/skwd-wall/config.json new file mode 100644 index 00000000..aaa2d7e6 --- /dev/null +++ b/config/skwd-wall/config.json @@ -0,0 +1,79 @@ +{ + "compositor": "hyprland", + "monitor": "", + "general": { + "locale": "", + "closeOnSelection": true, + "reopenAtLastSelection": false, + "filterBarAlwaysVisible": false, + "notifyOnWallpaperChange": false, + "randomIncludeWE": false, + "randomIncludeFavourites": false + }, + "paths": { + "wallpaper": "~/.config/omarchy/current/theme/backgrounds", + "videoWallpaper": "~/videowalls", + "cache": "", + "templates": "", + "scripts": "", + "steam": "", + "steamWorkshop": "", + "steamWeAssets": "" + }, + "features": { + "matugen": false, + "ollama": false, + "steam": false, + "wallhaven": false + }, + "colorSource": "magick", + "ollama": { + "url": "http://localhost:11434", + "model": "gemma3:4b", + "consolidateEnabled": true + }, + "steam": { + "apiKey": "", + "username": "" + }, + "wallhaven": { + "apiKey": "" + }, + "matugen": { + "schemeType": "scheme-fidelity", + "mode": "dark" + }, + "integrations": [], + "components": { + "wallpaperSelector": { + "displayMode": "slices", + "sliceSpacing": -30, + "hexScrollStep": 1, + "customPresets": {}, + "expandedWidth": 768, + "sliceHeight": 432, + "sliceWidth": 108, + "visibleCount": 14, + "skewOffset": 28 + } + }, + "wallpaperMute": true, + "pickOnlyMode": false, + "externalWallpaperCommand": "", + "postProcessing": [], + "performance": { + "imageOptimizePreset": "balanced", + "imageOptimizeResolution": "4k", + "videoConvertPreset": "balanced", + "videoConvertResolution": "2k", + "autoOptimizeImages": false, + "autoConvertVideos": false, + "imageTrashDays": 7, + "videoTrashDays": 7, + "autoDeleteImageTrash": false, + "autoDeleteVideoTrash": false + }, + "transition": { + "durationMs": 200 + } +} diff --git a/default/hypr/autostart.lua b/default/hypr/autostart.lua index 7eed5fbb..3ac29274 100644 --- a/default/hypr/autostart.lua +++ b/default/hypr/autostart.lua @@ -3,7 +3,6 @@ hl.on("hyprland.start", function() hl.exec_cmd("uwsm-app -- mako") hl.exec_cmd("! omarchy-toggle-enabled waybar-off && uwsm-app -- waybar") hl.exec_cmd("uwsm-app -- fcitx5 --disable notificationitem") - hl.exec_cmd("uwsm-app -- swaybg -i ~/.config/omarchy/current/background -m fill") hl.exec_cmd("/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1") hl.exec_cmd("omarchy-first-run") hl.exec_cmd("omarchy-powerprofiles-init") diff --git a/default/themed/skwd-wall-colors.json.tpl b/default/themed/skwd-wall-colors.json.tpl new file mode 100644 index 00000000..60d0c05e --- /dev/null +++ b/default/themed/skwd-wall-colors.json.tpl @@ -0,0 +1,27 @@ +{ + "primary": "{{ accent }}", + "primaryText": "{{ selection_foreground }}", + "primaryContainer": "{{ selection_background }}", + "primaryContainerText": "{{ selection_foreground }}", + "onPrimary": "{{ selection_foreground }}", + "secondary": "{{ color2 }}", + "secondaryText": "{{ background }}", + "secondaryContainer": "{{ color8 }}", + "secondaryContainerText": "{{ foreground }}", + "tertiary": "{{ active_border_color }}", + "tertiaryText": "{{ background }}", + "tertiaryContainer": "{{ color12 }}", + "tertiaryContainerText": "{{ foreground }}", + "background": "{{ background }}", + "backgroundText": "{{ foreground }}", + "surface": "{{ background }}", + "surfaceText": "{{ foreground }}", + "surfaceVariant": "{{ color8 }}", + "surfaceVariantText": "{{ foreground }}", + "surfaceContainer": "{{ color10 }}", + "outline": "{{ active_border_color }}", + "shadow": "#000000", + "inverseSurface": "{{ foreground }}", + "inverseSurfaceText": "{{ background }}", + "inversePrimary": "{{ accent }}" +} diff --git a/install/first-run/skwd-wall.sh b/install/first-run/skwd-wall.sh new file mode 100644 index 00000000..a3e95d1e --- /dev/null +++ b/install/first-run/skwd-wall.sh @@ -0,0 +1,2 @@ +systemctl --user daemon-reload +systemctl --user enable --now skwd-daemon.service diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index acdddc47..6ee75819 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -117,7 +117,7 @@ spotify socat starship sushi -swaybg +skwd-wall swayosd system-config-printer tesseract diff --git a/migrations/1778494413.sh b/migrations/1778494413.sh new file mode 100644 index 00000000..7e045b50 --- /dev/null +++ b/migrations/1778494413.sh @@ -0,0 +1,16 @@ +echo "Replace swaybg with Skwd-wall for wallpaper selection" + +omarchy-pkg-add skwd-wall +omarchy-pkg-drop swaybg + +if [[ ! -f ~/.config/skwd-wall/config.json ]]; then + mkdir -p ~/.config/skwd-wall + cp "$OMARCHY_PATH/config/skwd-wall/config.json" ~/.config/skwd-wall/config.json +fi + +omarchy-theme-refresh + +systemctl --user daemon-reload +systemctl --user enable --now skwd-daemon.service 2>/dev/null || true + +skwd wall apply "{\"type\":\"static\",\"path\":\"$HOME/.config/omarchy/current/background\"}" >/dev/null 2>&1 || true From 67751a45b5704ca111d02f5f2f4261c716944aee Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 14:01:04 +0200 Subject: [PATCH 02/27] Correct indention --- bin/omarchy-theme-bg-next | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/omarchy-theme-bg-next b/bin/omarchy-theme-bg-next index a559431e..2fa5c658 100755 --- a/bin/omarchy-theme-bg-next +++ b/bin/omarchy-theme-bg-next @@ -43,6 +43,6 @@ else # Set new background symlink ln -nsf "$NEW_BACKGROUND" "$CURRENT_BACKGROUND_LINK" -# Apply the new background through Skwd-wall -skwd wall apply "{\"type\":\"static\",\"path\":\"$CURRENT_BACKGROUND_LINK\"}" >/dev/null + # Apply the new background through Skwd-wall + skwd wall apply "{\"type\":\"static\",\"path\":\"$CURRENT_BACKGROUND_LINK\"}" >/dev/null fi From 7a988d89090cca80e47fc9bddcf38e7b2df17e36 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 14:50:52 +0200 Subject: [PATCH 03/27] Switch to using the switcher on all the normal keybindings --- bin/omarchy-menu | 2 +- default/hypr/bindings/utilities.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/omarchy-menu b/bin/omarchy-menu index dfac208f..22d95acb 100755 --- a/bin/omarchy-menu +++ b/bin/omarchy-menu @@ -326,7 +326,7 @@ show_theme_menu() { } show_background_menu() { - omarchy-launch-walker -m menus:omarchyBackgroundSelector --width 800 --minheight 400 + skwd wall toggle } show_font_menu() { diff --git a/default/hypr/bindings/utilities.lua b/default/hypr/bindings/utilities.lua index dc9f0824..3deccd4b 100644 --- a/default/hypr/bindings/utilities.lua +++ b/default/hypr/bindings/utilities.lua @@ -15,7 +15,7 @@ hl.bind("SUPER + SHIFT + CTRL + UP", hl.dsp.exec_cmd("omarchy-style-waybar-posit hl.bind("SUPER + SHIFT + CTRL + DOWN", hl.dsp.exec_cmd("omarchy-style-waybar-position bottom"), { description = "Move Waybar to bottom" }) hl.bind("SUPER + SHIFT + CTRL + LEFT", hl.dsp.exec_cmd("omarchy-style-waybar-position left"), { description = "Move Waybar to left" }) hl.bind("SUPER + SHIFT + CTRL + RIGHT", hl.dsp.exec_cmd("omarchy-style-waybar-position right"), { description = "Move Waybar to right" }) -hl.bind("SUPER + CTRL + SPACE", hl.dsp.exec_cmd("omarchy-menu background"), { description = "Theme background menu" }) +hl.bind("SUPER + CTRL + SPACE", hl.dsp.exec_cmd("skwd wall toggle"), { description = "Wallpaper switcher" }) hl.bind("SUPER + SHIFT + CTRL + SPACE", hl.dsp.exec_cmd("omarchy-menu 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" }) From 333ec367dfba1241972f7b580a344c818124f6e4 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 15:26:56 +0200 Subject: [PATCH 04/27] Let's build our own wall --- bin/omarchy-first-run | 1 - bin/omarchy-menu | 3 +- bin/omarchy-restart-skwd-wall | 5 - bin/omarchy-theme-bg-next | 6 +- bin/omarchy-theme-bg-set | 5 +- bin/omarchy-theme-bg-switcher | 20 ++ bin/omarchy-theme-set | 1 - bin/omarchy-theme-set-skwd-wall | 14 - config/skwd-wall/config.json | 79 ----- default/hypr/autostart.lua | 1 + default/hypr/bindings/utilities.lua | 2 +- default/quickshell/wallpaper-switcher.qml | 275 ++++++++++++++++++ default/themed/skwd-wall-colors.json.tpl | 27 -- .../themed/wallpaper-switcher-colors.json.tpl | 5 + install/first-run/skwd-wall.sh | 2 - install/omarchy-base.packages | 2 +- migrations/1778494413.sh | 16 - 17 files changed, 309 insertions(+), 155 deletions(-) delete mode 100755 bin/omarchy-restart-skwd-wall create mode 100755 bin/omarchy-theme-bg-switcher delete mode 100755 bin/omarchy-theme-set-skwd-wall delete mode 100644 config/skwd-wall/config.json create mode 100644 default/quickshell/wallpaper-switcher.qml delete mode 100644 default/themed/skwd-wall-colors.json.tpl create mode 100644 default/themed/wallpaper-switcher-colors.json.tpl delete mode 100644 install/first-run/skwd-wall.sh delete mode 100644 migrations/1778494413.sh diff --git a/bin/omarchy-first-run b/bin/omarchy-first-run index 277b0b2d..97b41f80 100755 --- a/bin/omarchy-first-run +++ b/bin/omarchy-first-run @@ -18,7 +18,6 @@ if [[ -f $FIRST_RUN_MODE ]]; then bash "$OMARCHY_PATH/install/first-run/gnome-theme.sh" bash "$OMARCHY_PATH/install/first-run/gdk-scale.sh" bash "$OMARCHY_PATH/install/first-run/swayosd.sh" - bash "$OMARCHY_PATH/install/first-run/skwd-wall.sh" bash "$OMARCHY_PATH/install/first-run/gtk-primary-paste.sh" bash "$OMARCHY_PATH/install/first-run/elephant.sh" omarchy-hook-install post-update "$OMARCHY_PATH/install/first-run/install-voxtype.hook" diff --git a/bin/omarchy-menu b/bin/omarchy-menu index 22d95acb..274aef0a 100755 --- a/bin/omarchy-menu +++ b/bin/omarchy-menu @@ -326,7 +326,8 @@ show_theme_menu() { } show_background_menu() { - skwd wall toggle + background=$(omarchy-theme-bg-switcher) + [[ -n $background ]] && omarchy-theme-bg-set "$background" } show_font_menu() { diff --git a/bin/omarchy-restart-skwd-wall b/bin/omarchy-restart-skwd-wall deleted file mode 100755 index c83521d6..00000000 --- a/bin/omarchy-restart-skwd-wall +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# omarchy:summary=Restart Skwd-wall wallpaper daemon - -systemctl --user restart skwd-daemon.service 2>/dev/null || true diff --git a/bin/omarchy-theme-bg-next b/bin/omarchy-theme-bg-next index 2fa5c658..0356d6f6 100755 --- a/bin/omarchy-theme-bg-next +++ b/bin/omarchy-theme-bg-next @@ -40,9 +40,5 @@ else NEW_BACKGROUND="${BACKGROUNDS[$NEXT_INDEX]}" fi - # Set new background symlink - ln -nsf "$NEW_BACKGROUND" "$CURRENT_BACKGROUND_LINK" - - # Apply the new background through Skwd-wall - skwd wall apply "{\"type\":\"static\",\"path\":\"$CURRENT_BACKGROUND_LINK\"}" >/dev/null + omarchy-theme-bg-set "$NEW_BACKGROUND" fi diff --git a/bin/omarchy-theme-bg-set b/bin/omarchy-theme-bg-set index 1eb00009..9c695d0e 100755 --- a/bin/omarchy-theme-bg-set +++ b/bin/omarchy-theme-bg-set @@ -20,5 +20,6 @@ fi # Create symlink to the new background ln -nsf "$BACKGROUND" "$CURRENT_BACKGROUND_LINK" -# Apply the new background through Skwd-wall -skwd wall apply "{\"type\":\"static\",\"path\":\"$CURRENT_BACKGROUND_LINK\"}" >/dev/null +# Kill existing swaybg and start new one +pkill -x swaybg +setsid uwsm-app -- swaybg -i "$CURRENT_BACKGROUND_LINK" -m fill >/dev/null 2>&1 & diff --git a/bin/omarchy-theme-bg-switcher b/bin/omarchy-theme-bg-switcher new file mode 100755 index 00000000..a39c30fc --- /dev/null +++ b/bin/omarchy-theme-bg-switcher @@ -0,0 +1,20 @@ +#!/bin/bash + +# omarchy:summary=Open the Omarchy wallpaper switcher +# omarchy:group=theme +# omarchy:name=bg-switcher +# omarchy:aliases=omarchy wallpaper + +OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} +selection_file=$(mktemp) +trap 'rm -f "$selection_file"' EXIT +theme_name=$(cat "$HOME/.config/omarchy/current/theme.name" 2>/dev/null) + +OMARCHY_WALLPAPER_SELECTION_FILE=$selection_file \ + OMARCHY_STOCK_BACKGROUNDS_DIR="$HOME/.config/omarchy/current/theme/backgrounds" \ + OMARCHY_USER_BACKGROUNDS_DIR="$HOME/.config/omarchy/backgrounds/$theme_name" \ + quickshell -p "$OMARCHY_PATH/default/quickshell/wallpaper-switcher.qml" >/dev/null + +if [[ -s $selection_file ]]; then + cat "$selection_file" +fi diff --git a/bin/omarchy-theme-set b/bin/omarchy-theme-set index a10d6c27..fe57c2af 100755 --- a/bin/omarchy-theme-set +++ b/bin/omarchy-theme-set @@ -68,7 +68,6 @@ omarchy-theme-set-browser omarchy-theme-set-vscode omarchy-theme-set-obsidian omarchy-theme-set-keyboard -omarchy-theme-set-skwd-wall # Call hook on theme set omarchy-hook theme-set "$THEME_NAME" >/dev/null diff --git a/bin/omarchy-theme-set-skwd-wall b/bin/omarchy-theme-set-skwd-wall deleted file mode 100755 index 787908eb..00000000 --- a/bin/omarchy-theme-set-skwd-wall +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# omarchy:summary=Refresh Skwd-wall after an Omarchy theme change -# omarchy:hidden=true - -THEME_COLORS="$HOME/.config/omarchy/current/theme/skwd-wall-colors.json" -SKWD_COLORS="$HOME/.cache/skwd-wall/colors.json" - -if [[ -f $THEME_COLORS ]]; then - mkdir -p "$(dirname "$SKWD_COLORS")" - cp "$THEME_COLORS" "$SKWD_COLORS" -fi - -skwd wall cache_rebuild >/dev/null diff --git a/config/skwd-wall/config.json b/config/skwd-wall/config.json deleted file mode 100644 index aaa2d7e6..00000000 --- a/config/skwd-wall/config.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "compositor": "hyprland", - "monitor": "", - "general": { - "locale": "", - "closeOnSelection": true, - "reopenAtLastSelection": false, - "filterBarAlwaysVisible": false, - "notifyOnWallpaperChange": false, - "randomIncludeWE": false, - "randomIncludeFavourites": false - }, - "paths": { - "wallpaper": "~/.config/omarchy/current/theme/backgrounds", - "videoWallpaper": "~/videowalls", - "cache": "", - "templates": "", - "scripts": "", - "steam": "", - "steamWorkshop": "", - "steamWeAssets": "" - }, - "features": { - "matugen": false, - "ollama": false, - "steam": false, - "wallhaven": false - }, - "colorSource": "magick", - "ollama": { - "url": "http://localhost:11434", - "model": "gemma3:4b", - "consolidateEnabled": true - }, - "steam": { - "apiKey": "", - "username": "" - }, - "wallhaven": { - "apiKey": "" - }, - "matugen": { - "schemeType": "scheme-fidelity", - "mode": "dark" - }, - "integrations": [], - "components": { - "wallpaperSelector": { - "displayMode": "slices", - "sliceSpacing": -30, - "hexScrollStep": 1, - "customPresets": {}, - "expandedWidth": 768, - "sliceHeight": 432, - "sliceWidth": 108, - "visibleCount": 14, - "skewOffset": 28 - } - }, - "wallpaperMute": true, - "pickOnlyMode": false, - "externalWallpaperCommand": "", - "postProcessing": [], - "performance": { - "imageOptimizePreset": "balanced", - "imageOptimizeResolution": "4k", - "videoConvertPreset": "balanced", - "videoConvertResolution": "2k", - "autoOptimizeImages": false, - "autoConvertVideos": false, - "imageTrashDays": 7, - "videoTrashDays": 7, - "autoDeleteImageTrash": false, - "autoDeleteVideoTrash": false - }, - "transition": { - "durationMs": 200 - } -} diff --git a/default/hypr/autostart.lua b/default/hypr/autostart.lua index 3ac29274..7eed5fbb 100644 --- a/default/hypr/autostart.lua +++ b/default/hypr/autostart.lua @@ -3,6 +3,7 @@ hl.on("hyprland.start", function() hl.exec_cmd("uwsm-app -- mako") hl.exec_cmd("! omarchy-toggle-enabled waybar-off && uwsm-app -- waybar") hl.exec_cmd("uwsm-app -- fcitx5 --disable notificationitem") + hl.exec_cmd("uwsm-app -- swaybg -i ~/.config/omarchy/current/background -m fill") hl.exec_cmd("/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1") hl.exec_cmd("omarchy-first-run") hl.exec_cmd("omarchy-powerprofiles-init") diff --git a/default/hypr/bindings/utilities.lua b/default/hypr/bindings/utilities.lua index 3deccd4b..57a3e70e 100644 --- a/default/hypr/bindings/utilities.lua +++ b/default/hypr/bindings/utilities.lua @@ -15,7 +15,7 @@ hl.bind("SUPER + SHIFT + CTRL + UP", hl.dsp.exec_cmd("omarchy-style-waybar-posit hl.bind("SUPER + SHIFT + CTRL + DOWN", hl.dsp.exec_cmd("omarchy-style-waybar-position bottom"), { description = "Move Waybar to bottom" }) hl.bind("SUPER + SHIFT + CTRL + LEFT", hl.dsp.exec_cmd("omarchy-style-waybar-position left"), { description = "Move Waybar to left" }) hl.bind("SUPER + SHIFT + CTRL + RIGHT", hl.dsp.exec_cmd("omarchy-style-waybar-position right"), { description = "Move Waybar to right" }) -hl.bind("SUPER + CTRL + SPACE", hl.dsp.exec_cmd("skwd wall toggle"), { description = "Wallpaper switcher" }) +hl.bind("SUPER + CTRL + SPACE", hl.dsp.exec_cmd("bash -lc 'background=$(omarchy-theme-bg-switcher) && [[ -n $background ]] && omarchy-theme-bg-set \"$background\"'"), { description = "Wallpaper switcher" }) hl.bind("SUPER + SHIFT + CTRL + SPACE", hl.dsp.exec_cmd("omarchy-menu 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" }) diff --git a/default/quickshell/wallpaper-switcher.qml b/default/quickshell/wallpaper-switcher.qml new file mode 100644 index 00000000..b6fdf16c --- /dev/null +++ b/default/quickshell/wallpaper-switcher.qml @@ -0,0 +1,275 @@ +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import QtQuick +import QtQuick.Effects +import QtQuick.Shapes + +ShellRoot { + id: root + + property string stockBackgroundsDir: Quickshell.env("OMARCHY_STOCK_BACKGROUNDS_DIR") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/backgrounds") + property string userBackgroundsDir: Quickshell.env("OMARCHY_USER_BACKGROUNDS_DIR") + property string currentBackground: Quickshell.env("HOME") + "/.config/omarchy/current/background" + property string selectionFile: Quickshell.env("OMARCHY_WALLPAPER_SELECTION_FILE") + property int selectedIndex: 0 + property color accent: "#798186" + property color background: "#101315" + property color foreground: "#cacccc" + property int expandedWidth: 768 + property int sliceWidth: 108 + property int sliceHeight: 432 + property int sliceSpacing: -30 + property int skewOffset: 28 + + function fileUrl(path) { + return "file://" + path.split("/").map(encodeURIComponent).join("/") + } + + function shellQuote(value) { + return "'" + String(value).replace(/'/g, "'\\''") + "'" + } + + function currentPath() { + if (wallpaperModel.count === 0) return "" + return wallpaperModel.get(selectedIndex, "filePath") + } + + function select(index) { + if (index < 0 || index >= wallpaperModel.count) return + selectedIndex = index + list.currentIndex = index + list.positionViewAtIndex(index, ListView.Center) + } + + function applySelected() { + var path = currentPath() + if (!path) return + applyProc.command = ["bash", "-lc", "printf '%s\\n' " + shellQuote(path) + " > " + shellQuote(selectionFile)] + applyProc.running = true + } + + ListModel { + id: wallpaperModel + } + + function addBackground(path) { + if (!path) return + var fileName = path.split("/").pop() + + for (var i = wallpaperModel.count - 1; i >= 0; i--) { + if (wallpaperModel.get(i).fileName === fileName) + wallpaperModel.remove(i) + } + + wallpaperModel.append({ filePath: path, fileName: fileName }) + } + + Process { + id: loadBackgroundsProc + command: ["bash", "-lc", "for dir in " + shellQuote(root.stockBackgroundsDir) + " " + shellQuote(root.userBackgroundsDir) + "; do [[ -d $dir ]] && find -L \"$dir\" -maxdepth 1 -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\) -print | sort; done"] + stdout: SplitParser { + onRead: function(data) { + var paths = data.split("\n") + for (var i = 0; i < paths.length; i++) + root.addBackground(paths[i].trim()) + } + } + } + + Component.onCompleted: loadBackgroundsProc.running = true + + FileView { + path: Quickshell.env("HOME") + "/.config/omarchy/current/theme/wallpaper-switcher-colors.json" + watchChanges: true + onLoaded: root.loadColors(text()) + onFileChanged: { reload(); root.loadColors(text()) } + } + + function loadColors(raw) { + try { + var colors = JSON.parse(raw || "{}") + root.accent = colors.primary || root.accent + root.background = colors.background || root.background + root.foreground = colors.backgroundText || root.foreground + } catch (e) {} + } + + Process { + id: applyProc + onExited: Qt.quit() + } + + PanelWindow { + id: panel + anchors { top: true; bottom: true; left: true; right: true } + color: "transparent" + WlrLayershell.namespace: "omarchy-wallpaper-switcher" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + exclusionMode: ExclusionMode.Ignore + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.55) + } + + MouseArea { + anchors.fill: parent + onClicked: Qt.quit() + } + + Item { + id: card + width: Math.min(parent.width - 80, root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing) + 40) + height: root.sliceHeight + 60 + anchors.centerIn: parent + + MouseArea { anchors.fill: parent; onClicked: {} } + + ListView { + id: list + anchors.top: parent.top + anchors.topMargin: 30 + anchors.bottom: parent.bottom + anchors.bottomMargin: 30 + anchors.horizontalCenter: parent.horizontalCenter + width: root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing) + orientation: ListView.Horizontal + model: wallpaperModel + spacing: root.sliceSpacing + clip: false + focus: true + currentIndex: root.selectedIndex + preferredHighlightBegin: (width - root.expandedWidth) / 2 + preferredHighlightEnd: (width + root.expandedWidth) / 2 + highlightRangeMode: ListView.StrictlyEnforceRange + highlightMoveDuration: 120 + highlight: Item {} + header: Item { width: (list.width - root.expandedWidth) / 2; height: 1 } + footer: Item { width: (list.width - root.expandedWidth) / 2; height: 1 } + + Keys.onEscapePressed: Qt.quit() + Keys.onReturnPressed: root.applySelected() + Keys.onLeftPressed: root.select(Math.max(0, root.selectedIndex - 1)) + Keys.onRightPressed: root.select(Math.min(wallpaperModel.count - 1, root.selectedIndex + 1)) + + Component.onCompleted: forceActiveFocus() + + delegate: Item { + id: item + required property int index + required property string filePath + required property string fileName + + width: index === root.selectedIndex ? root.expandedWidth : root.sliceWidth + height: list.height + z: index === root.selectedIndex ? 100 : 50 - Math.min(Math.abs(index - root.selectedIndex), 40) + + readonly property real skAbs: Math.abs(root.skewOffset) + readonly property real topLeft: root.skewOffset >= 0 ? skAbs : 0 + readonly property real topRight: root.skewOffset >= 0 ? width : width - skAbs + readonly property real bottomRight: root.skewOffset >= 0 ? width - skAbs : width + readonly property real bottomLeft: root.skewOffset >= 0 ? 0 : skAbs + + Behavior on width { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } } + + Item { + id: maskShape + anchors.fill: parent + visible: false + layer.enabled: true + + Shape { + anchors.fill: parent + antialiasing: true + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: "white" + strokeColor: "transparent" + startX: item.topLeft; startY: 0 + PathLine { x: item.topRight; y: 0 } + PathLine { x: item.bottomRight; y: item.height } + PathLine { x: item.bottomLeft; y: item.height } + PathLine { x: item.topLeft; y: 0 } + } + } + } + + Shape { + x: index === root.selectedIndex ? 4 : 2 + y: index === root.selectedIndex ? 10 : 5 + width: item.width + height: item.height + opacity: index === root.selectedIndex ? 0.5 : 0.32 + antialiasing: true + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: "#000000" + strokeColor: "transparent" + startX: item.topLeft; startY: 0 + PathLine { x: item.topRight; y: 0 } + PathLine { x: item.bottomRight; y: item.height } + PathLine { x: item.bottomLeft; y: item.height } + PathLine { x: item.topLeft; y: 0 } + } + } + + Item { + anchors.fill: parent + layer.enabled: true + layer.smooth: true + layer.effect: MultiEffect { + maskEnabled: true + maskSource: maskShape + maskThresholdMin: 0.3 + maskSpreadAtMin: 0.3 + } + + Image { + id: image + anchors.fill: parent + source: root.fileUrl(item.filePath) + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + smooth: true + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 120 } } + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, index === root.selectedIndex ? 0 : 0.42) + Behavior on color { ColorAnimation { duration: 120 } } + } + } + + Shape { + anchors.fill: parent + antialiasing: true + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: "transparent" + strokeColor: index === root.selectedIndex ? root.accent : Qt.rgba(0, 0, 0, 0.65) + strokeWidth: index === root.selectedIndex ? 3 : 1 + startX: item.topLeft; startY: 0 + PathLine { x: item.topRight; y: 0 } + PathLine { x: item.bottomRight; y: item.height } + PathLine { x: item.bottomLeft; y: item.height } + PathLine { x: item.topLeft; y: 0 } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: root.select(index) + onClicked: index === root.selectedIndex ? root.applySelected() : root.select(index) + } + } + } + } + } +} diff --git a/default/themed/skwd-wall-colors.json.tpl b/default/themed/skwd-wall-colors.json.tpl deleted file mode 100644 index 60d0c05e..00000000 --- a/default/themed/skwd-wall-colors.json.tpl +++ /dev/null @@ -1,27 +0,0 @@ -{ - "primary": "{{ accent }}", - "primaryText": "{{ selection_foreground }}", - "primaryContainer": "{{ selection_background }}", - "primaryContainerText": "{{ selection_foreground }}", - "onPrimary": "{{ selection_foreground }}", - "secondary": "{{ color2 }}", - "secondaryText": "{{ background }}", - "secondaryContainer": "{{ color8 }}", - "secondaryContainerText": "{{ foreground }}", - "tertiary": "{{ active_border_color }}", - "tertiaryText": "{{ background }}", - "tertiaryContainer": "{{ color12 }}", - "tertiaryContainerText": "{{ foreground }}", - "background": "{{ background }}", - "backgroundText": "{{ foreground }}", - "surface": "{{ background }}", - "surfaceText": "{{ foreground }}", - "surfaceVariant": "{{ color8 }}", - "surfaceVariantText": "{{ foreground }}", - "surfaceContainer": "{{ color10 }}", - "outline": "{{ active_border_color }}", - "shadow": "#000000", - "inverseSurface": "{{ foreground }}", - "inverseSurfaceText": "{{ background }}", - "inversePrimary": "{{ accent }}" -} diff --git a/default/themed/wallpaper-switcher-colors.json.tpl b/default/themed/wallpaper-switcher-colors.json.tpl new file mode 100644 index 00000000..96a92436 --- /dev/null +++ b/default/themed/wallpaper-switcher-colors.json.tpl @@ -0,0 +1,5 @@ +{ + "primary": "{{ accent }}", + "background": "{{ background }}", + "backgroundText": "{{ foreground }}" +} diff --git a/install/first-run/skwd-wall.sh b/install/first-run/skwd-wall.sh deleted file mode 100644 index a3e95d1e..00000000 --- a/install/first-run/skwd-wall.sh +++ /dev/null @@ -1,2 +0,0 @@ -systemctl --user daemon-reload -systemctl --user enable --now skwd-daemon.service diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index 6ee75819..acdddc47 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -117,7 +117,7 @@ spotify socat starship sushi -skwd-wall +swaybg swayosd system-config-printer tesseract diff --git a/migrations/1778494413.sh b/migrations/1778494413.sh deleted file mode 100644 index 7e045b50..00000000 --- a/migrations/1778494413.sh +++ /dev/null @@ -1,16 +0,0 @@ -echo "Replace swaybg with Skwd-wall for wallpaper selection" - -omarchy-pkg-add skwd-wall -omarchy-pkg-drop swaybg - -if [[ ! -f ~/.config/skwd-wall/config.json ]]; then - mkdir -p ~/.config/skwd-wall - cp "$OMARCHY_PATH/config/skwd-wall/config.json" ~/.config/skwd-wall/config.json -fi - -omarchy-theme-refresh - -systemctl --user daemon-reload -systemctl --user enable --now skwd-daemon.service 2>/dev/null || true - -skwd wall apply "{\"type\":\"static\",\"path\":\"$HOME/.config/omarchy/current/background\"}" >/dev/null 2>&1 || true From aba11e716fb26fd6b1df4e162536c05581c957d5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 15:40:40 +0200 Subject: [PATCH 05/27] Tweaks --- default/quickshell/wallpaper-switcher.qml | 66 +++++++++++++++-------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/default/quickshell/wallpaper-switcher.qml b/default/quickshell/wallpaper-switcher.qml index b6fdf16c..66cde71c 100644 --- a/default/quickshell/wallpaper-switcher.qml +++ b/default/quickshell/wallpaper-switcher.qml @@ -32,11 +32,14 @@ ShellRoot { function currentPath() { if (wallpaperModel.count === 0) return "" - return wallpaperModel.get(selectedIndex, "filePath") + return wallpaperModel.get(selectedIndex).filePath } function select(index) { - if (index < 0 || index >= wallpaperModel.count) return + if (wallpaperModel.count === 0) return + if (index < 0) index = wallpaperModel.count - 1 + else if (index >= wallpaperModel.count) index = 0 + selectedIndex = index list.currentIndex = index list.positionViewAtIndex(index, ListView.Center) @@ -51,6 +54,10 @@ ShellRoot { ListModel { id: wallpaperModel + onCountChanged: { + if (count > 0 && list.currentIndex < 0) + root.select(0) + } } function addBackground(path) { @@ -67,14 +74,19 @@ ShellRoot { Process { id: loadBackgroundsProc + property string output: "" command: ["bash", "-lc", "for dir in " + shellQuote(root.stockBackgroundsDir) + " " + shellQuote(root.userBackgroundsDir) + "; do [[ -d $dir ]] && find -L \"$dir\" -maxdepth 1 -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\) -print | sort; done"] stdout: SplitParser { onRead: function(data) { - var paths = data.split("\n") - for (var i = 0; i < paths.length; i++) - root.addBackground(paths[i].trim()) + loadBackgroundsProc.output += data + "\n" } } + onExited: { + var paths = output.split("\n") + for (var i = 0; i < paths.length; i++) + root.addBackground(paths[i].trim()) + root.select(0) + } } Component.onCompleted: loadBackgroundsProc.running = true @@ -140,7 +152,7 @@ ShellRoot { spacing: root.sliceSpacing clip: false focus: true - currentIndex: root.selectedIndex + currentIndex: 0 preferredHighlightBegin: (width - root.expandedWidth) / 2 preferredHighlightEnd: (width + root.expandedWidth) / 2 highlightRangeMode: ListView.StrictlyEnforceRange @@ -149,10 +161,22 @@ ShellRoot { header: Item { width: (list.width - root.expandedWidth) / 2; height: 1 } footer: Item { width: (list.width - root.expandedWidth) / 2; height: 1 } - Keys.onEscapePressed: Qt.quit() - Keys.onReturnPressed: root.applySelected() - Keys.onLeftPressed: root.select(Math.max(0, root.selectedIndex - 1)) - Keys.onRightPressed: root.select(Math.min(wallpaperModel.count - 1, root.selectedIndex + 1)) + Keys.priority: Keys.BeforeItem + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Escape) { + Qt.quit() + event.accepted = true + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + root.applySelected() + event.accepted = true + } else if (event.key === Qt.Key_Left) { + root.select(root.selectedIndex - 1) + event.accepted = true + } else if (event.key === Qt.Key_Right) { + root.select(root.selectedIndex + 1) + event.accepted = true + } + } Component.onCompleted: forceActiveFocus() @@ -162,9 +186,11 @@ ShellRoot { required property string filePath required property string fileName - width: index === root.selectedIndex ? root.expandedWidth : root.sliceWidth + readonly property bool selected: index === root.selectedIndex + + width: selected ? root.expandedWidth : root.sliceWidth height: list.height - z: index === root.selectedIndex ? 100 : 50 - Math.min(Math.abs(index - root.selectedIndex), 40) + z: selected ? 100 : 50 - Math.min(Math.abs(index - root.selectedIndex), 40) readonly property real skAbs: Math.abs(root.skewOffset) readonly property real topLeft: root.skewOffset >= 0 ? skAbs : 0 @@ -197,11 +223,11 @@ ShellRoot { } Shape { - x: index === root.selectedIndex ? 4 : 2 - y: index === root.selectedIndex ? 10 : 5 + x: item.selected ? 4 : 2 + y: item.selected ? 10 : 5 width: item.width height: item.height - opacity: index === root.selectedIndex ? 0.5 : 0.32 + opacity: item.selected ? 0.5 : 0.32 antialiasing: true preferredRendererType: Shape.CurveRenderer ShapePath { @@ -240,7 +266,7 @@ ShellRoot { Rectangle { anchors.fill: parent - color: Qt.rgba(0, 0, 0, index === root.selectedIndex ? 0 : 0.42) + color: Qt.rgba(0, 0, 0, item.selected ? 0 : 0.42) Behavior on color { ColorAnimation { duration: 120 } } } } @@ -251,8 +277,8 @@ ShellRoot { preferredRendererType: Shape.CurveRenderer ShapePath { fillColor: "transparent" - strokeColor: index === root.selectedIndex ? root.accent : Qt.rgba(0, 0, 0, 0.65) - strokeWidth: index === root.selectedIndex ? 3 : 1 + strokeColor: item.selected ? root.accent : Qt.rgba(0, 0, 0, 0.65) + strokeWidth: item.selected ? 3 : 1 startX: item.topLeft; startY: 0 PathLine { x: item.topRight; y: 0 } PathLine { x: item.bottomRight; y: item.height } @@ -263,10 +289,8 @@ ShellRoot { MouseArea { anchors.fill: parent - hoverEnabled: true cursorShape: Qt.PointingHandCursor - onEntered: root.select(index) - onClicked: index === root.selectedIndex ? root.applySelected() : root.select(index) + onClicked: item.selected ? root.applySelected() : root.select(index) } } } From 1e646be7ee9c7b9a05a9e2e008a3e66b1e9c9721 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 16:23:18 +0200 Subject: [PATCH 06/27] We need quickshell available --- install/omarchy-base.packages | 1 + migrations/1778506941.sh | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 migrations/1778506941.sh diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index acdddc47..a84f73b1 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -106,6 +106,7 @@ python-gobject python-poetry-core python-terminaltexteffects qt5-wayland +quickshell ripgrep ruby rust diff --git a/migrations/1778506941.sh b/migrations/1778506941.sh new file mode 100644 index 00000000..e9e4f638 --- /dev/null +++ b/migrations/1778506941.sh @@ -0,0 +1,3 @@ +echo "Install quickshell for the image selector" + +omarchy-pkg-add quickshell From 008f0cf7dd42f714e6bdb8836612a84610d7cbc9 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 16:30:22 +0200 Subject: [PATCH 07/27] Extract generic switcher --- bin/omarchy-menu-images | 94 +++++++++++++++++++++++ bin/omarchy-theme-bg-switcher | 16 ++-- default/hypr/apps/system.lua | 3 + default/quickshell/wallpaper-switcher.qml | 91 ++++++++++++++-------- 4 files changed, 162 insertions(+), 42 deletions(-) create mode 100755 bin/omarchy-menu-images diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images new file mode 100755 index 00000000..c4787f45 --- /dev/null +++ b/bin/omarchy-menu-images @@ -0,0 +1,94 @@ +#!/bin/bash + +# omarchy:summary=Open a generic image selector menu +# omarchy:group=menu +# omarchy:name=images +# omarchy:args=[--selected ] [--namespace ] [--colors-file ] ... + +OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} + +selected_image="" +namespace="omarchy-image-selector" +colors_file="" +image_dirs=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --selected) + selected_image="$2" + shift 2 + ;; + --namespace) + namespace="$2" + shift 2 + ;; + --colors-file) + colors_file="$2" + shift 2 + ;; + --help|-h) + echo "Usage: omarchy-menu-images [--selected ] [--namespace ] [--colors-file ] ..." + exit 0 + ;; + *) + image_dirs+=("$1") + shift + ;; + esac +done + +if (( ${#image_dirs[@]} == 0 )); then + echo "Usage: omarchy-menu-images [--selected ] [--namespace ] [--colors-file ] ..." >&2 + exit 1 +fi + +selection_file=$(mktemp) +trap 'rm -f "$selection_file"' EXIT + +image_dirs_env="" +for dir in "${image_dirs[@]}"; do + if [[ -z $image_dirs_env ]]; then + image_dirs_env="$dir" + else + image_dirs_env+=$'\n'"$dir" + fi +done + +current_image=$(readlink -f "$selected_image" 2>/dev/null) +selected_list_image="" + +if [[ -n $current_image ]]; then + for dir in "${image_dirs[@]}"; do + if [[ -d $dir ]]; then + selected_list_image=$(find -L "$dir" -maxdepth 1 -type f -samefile "$current_image" -print -quit 2>/dev/null) + [[ -n $selected_list_image ]] && break + fi + done +fi + +cache_dir=${XDG_CACHE_HOME:-$HOME/.cache}/omarchy/image-selector +mkdir -p "$cache_dir" + +for dir in "${image_dirs[@]}"; do + if [[ -d $dir ]]; then + while IFS= read -r -d '' image; do + hash=$(md5sum "$image" | cut -d ' ' -f 1) + thumbnail="$cache_dir/$hash.jpg" + + if [[ ! -f $thumbnail ]]; then + magick "$image" -auto-orient -resize '1536x864^' -gravity center -extent '1536x864' -strip -quality 82 "$thumbnail" + fi + done < <(find -L "$dir" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) -print0 2>/dev/null) + fi +done + +OMARCHY_IMAGE_SELECTOR_SELECTION_FILE=$selection_file \ + OMARCHY_IMAGE_SELECTOR_DIRS="$image_dirs_env" \ + OMARCHY_IMAGE_SELECTOR_SELECTED="$selected_list_image" \ + OMARCHY_IMAGE_SELECTOR_NAMESPACE="$namespace" \ + OMARCHY_IMAGE_SELECTOR_COLORS_FILE="$colors_file" \ + quickshell -p "$OMARCHY_PATH/default/quickshell/wallpaper-switcher.qml" >/dev/null + +if [[ -s $selection_file ]]; then + cat "$selection_file" +fi diff --git a/bin/omarchy-theme-bg-switcher b/bin/omarchy-theme-bg-switcher index a39c30fc..06037639 100755 --- a/bin/omarchy-theme-bg-switcher +++ b/bin/omarchy-theme-bg-switcher @@ -6,15 +6,11 @@ # omarchy:aliases=omarchy wallpaper OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} -selection_file=$(mktemp) -trap 'rm -f "$selection_file"' EXIT theme_name=$(cat "$HOME/.config/omarchy/current/theme.name" 2>/dev/null) +current_background=$(readlink -f "$HOME/.config/omarchy/current/background" 2>/dev/null) -OMARCHY_WALLPAPER_SELECTION_FILE=$selection_file \ - OMARCHY_STOCK_BACKGROUNDS_DIR="$HOME/.config/omarchy/current/theme/backgrounds" \ - OMARCHY_USER_BACKGROUNDS_DIR="$HOME/.config/omarchy/backgrounds/$theme_name" \ - quickshell -p "$OMARCHY_PATH/default/quickshell/wallpaper-switcher.qml" >/dev/null - -if [[ -s $selection_file ]]; then - cat "$selection_file" -fi +omarchy-menu-images \ + --selected "$current_background" \ + --namespace "omarchy-wallpaper-switcher" \ + "$HOME/.config/omarchy/current/theme/backgrounds" \ + "$HOME/.config/omarchy/backgrounds/$theme_name" diff --git a/default/hypr/apps/system.lua b/default/hypr/apps/system.lua index 71e828fe..9ce1fcd1 100644 --- a/default/hypr/apps/system.lua +++ b/default/hypr/apps/system.lua @@ -21,3 +21,6 @@ hl.window_rule({ match = { tag = "pop" }, rounding = 8 }) -- Prevent idle while open. hl.window_rule({ match = { tag = "noidle" }, idle_inhibit = "always" }) + +-- Image selector. +hl.layer_rule({ match = { namespace = "omarchy-image-selector" }, no_anim = true }) diff --git a/default/quickshell/wallpaper-switcher.qml b/default/quickshell/wallpaper-switcher.qml index 66cde71c..f54a4d4e 100644 --- a/default/quickshell/wallpaper-switcher.qml +++ b/default/quickshell/wallpaper-switcher.qml @@ -8,11 +8,13 @@ import QtQuick.Shapes ShellRoot { id: root - property string stockBackgroundsDir: Quickshell.env("OMARCHY_STOCK_BACKGROUNDS_DIR") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/backgrounds") - property string userBackgroundsDir: Quickshell.env("OMARCHY_USER_BACKGROUNDS_DIR") - property string currentBackground: Quickshell.env("HOME") + "/.config/omarchy/current/background" - property string selectionFile: Quickshell.env("OMARCHY_WALLPAPER_SELECTION_FILE") + property string imageDirs: Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIRS") || Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIR") || Quickshell.env("OMARCHY_STOCK_BACKGROUNDS_DIR") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/backgrounds") + property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_WALLPAPER_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/wallpaper-switcher-colors.json") + property string layerNamespace: Quickshell.env("OMARCHY_IMAGE_SELECTOR_NAMESPACE") || "omarchy-image-selector" property int selectedIndex: 0 + property bool imagesLoaded: false property color accent: "#798186" property color background: "#101315" property color foreground: "#cacccc" @@ -31,18 +33,18 @@ ShellRoot { } function currentPath() { - if (wallpaperModel.count === 0) return "" - return wallpaperModel.get(selectedIndex).filePath + if (imageModel.count === 0) return "" + return imageModel.get(selectedIndex).filePath } function select(index) { - if (wallpaperModel.count === 0) return - if (index < 0) index = wallpaperModel.count - 1 - else if (index >= wallpaperModel.count) index = 0 + if (imageModel.count === 0) return + if (index < 0) index = imageModel.count - 1 + else if (index >= imageModel.count) index = 0 selectedIndex = index list.currentIndex = index - list.positionViewAtIndex(index, ListView.Center) + list.centerSelected() } function applySelected() { @@ -53,46 +55,61 @@ ShellRoot { } ListModel { - id: wallpaperModel + id: imageModel onCountChanged: { if (count > 0 && list.currentIndex < 0) - root.select(0) + root.select(0) } } - function addBackground(path) { + function addImage(path, thumbnailPath) { if (!path) return var fileName = path.split("/").pop() - for (var i = wallpaperModel.count - 1; i >= 0; i--) { - if (wallpaperModel.get(i).fileName === fileName) - wallpaperModel.remove(i) + for (var i = imageModel.count - 1; i >= 0; i--) { + if (imageModel.get(i).fileName === fileName) + imageModel.remove(i) } - wallpaperModel.append({ filePath: path, fileName: fileName }) + imageModel.append({ filePath: path, fileName: fileName, thumbnailPath: thumbnailPath || path }) + } + + function selectedImageIndex() { + for (var i = 0; i < imageModel.count; i++) { + if (imageModel.get(i).filePath === selectedImage) + return i + } + + return 0 } Process { - id: loadBackgroundsProc + id: loadImagesProc property string output: "" - command: ["bash", "-lc", "for dir in " + shellQuote(root.stockBackgroundsDir) + " " + shellQuote(root.userBackgroundsDir) + "; do [[ -d $dir ]] && find -L \"$dir\" -maxdepth 1 -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\) -print | sort; done"] + command: ["bash", "-lc", "cache_dir=${XDG_CACHE_HOME:-$HOME/.cache}/omarchy/image-selector; while IFS= read -r dir; do [[ -d $dir ]] && find -L \"$dir\" -maxdepth 1 -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\) -print0; done <<< " + shellQuote(root.imageDirs) + " | sort -z | while IFS= read -r -d '' image; do hash=$(md5sum \"$image\" | cut -d ' ' -f 1); thumb=\"$cache_dir/$hash.jpg\"; [[ -f $thumb ]] || thumb=$image; printf '%s\\t%s\\n' \"$image\" \"$thumb\"; done"] stdout: SplitParser { onRead: function(data) { - loadBackgroundsProc.output += data + "\n" + loadImagesProc.output += data + "\n" } } onExited: { var paths = output.split("\n") - for (var i = 0; i < paths.length; i++) - root.addBackground(paths[i].trim()) - root.select(0) + for (var i = 0; i < paths.length; i++) { + var row = paths[i].trim() + if (!row) continue + + var columns = row.split("\t") + root.addImage(columns[0], columns[1]) } + root.select(root.selectedImageIndex()) + root.imagesLoaded = true + } } - Component.onCompleted: loadBackgroundsProc.running = true + Component.onCompleted: loadImagesProc.running = true FileView { - path: Quickshell.env("HOME") + "/.config/omarchy/current/theme/wallpaper-switcher-colors.json" + path: root.colorsFile watchChanges: true onLoaded: root.loadColors(text()) onFileChanged: { reload(); root.loadColors(text()) } @@ -114,9 +131,10 @@ ShellRoot { PanelWindow { id: panel + visible: root.imagesLoaded anchors { top: true; bottom: true; left: true; right: true } color: "transparent" - WlrLayershell.namespace: "omarchy-wallpaper-switcher" + WlrLayershell.namespace: root.layerNamespace WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive exclusionMode: ExclusionMode.Ignore @@ -148,19 +166,27 @@ ShellRoot { anchors.horizontalCenter: parent.horizontalCenter width: root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing) orientation: ListView.Horizontal - model: wallpaperModel + model: imageModel spacing: root.sliceSpacing clip: false focus: true currentIndex: 0 preferredHighlightBegin: (width - root.expandedWidth) / 2 preferredHighlightEnd: (width + root.expandedWidth) / 2 - highlightRangeMode: ListView.StrictlyEnforceRange + highlightRangeMode: ListView.NoHighlightRange highlightMoveDuration: 120 highlight: Item {} header: Item { width: (list.width - root.expandedWidth) / 2; height: 1 } footer: Item { width: (list.width - root.expandedWidth) / 2; height: 1 } + function centerSelected() { + Qt.callLater(function() { + var selectedItem = list.itemAtIndex(root.selectedIndex) + if (selectedItem) + list.contentX = selectedItem.x + selectedItem.width / 2 - list.width / 2 + }) + } + Keys.priority: Keys.BeforeItem Keys.onPressed: function(event) { if (event.key === Qt.Key_Escape) { @@ -185,6 +211,7 @@ ShellRoot { required property int index required property string filePath required property string fileName + required property string thumbnailPath readonly property bool selected: index === root.selectedIndex @@ -192,6 +219,8 @@ ShellRoot { height: list.height z: selected ? 100 : 50 - Math.min(Math.abs(index - root.selectedIndex), 40) + onWidthChanged: if (selected) list.centerSelected() + readonly property real skAbs: Math.abs(root.skewOffset) readonly property real topLeft: root.skewOffset >= 0 ? skAbs : 0 readonly property real topRight: root.skewOffset >= 0 ? width : width - skAbs @@ -255,13 +284,11 @@ ShellRoot { Image { id: image anchors.fill: parent - source: root.fileUrl(item.filePath) + source: root.fileUrl(item.thumbnailPath) fillMode: Image.PreserveAspectCrop - asynchronous: true + asynchronous: false cache: true smooth: true - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { NumberAnimation { duration: 120 } } } Rectangle { From 5487c17d196cf0382c2259639ecb02ce017107fe Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 16:38:58 +0200 Subject: [PATCH 08/27] Single namespace is fine --- bin/omarchy-menu | 3 +- bin/omarchy-menu-images | 14 ++--- bin/omarchy-theme-bg-switcher | 1 - bin/omarchy-theme-switcher | 62 +++++++++++++++++++++++ default/quickshell/wallpaper-switcher.qml | 3 +- 5 files changed, 68 insertions(+), 15 deletions(-) create mode 100755 bin/omarchy-theme-switcher diff --git a/bin/omarchy-menu b/bin/omarchy-menu index 274aef0a..9486a3d1 100755 --- a/bin/omarchy-menu +++ b/bin/omarchy-menu @@ -322,7 +322,8 @@ show_screensaver_menu() { } show_theme_menu() { - omarchy-launch-walker -m menus:omarchythemes --width 800 --minheight 400 + theme=$(omarchy-theme-switcher) + [[ -n $theme ]] && omarchy-theme-set "$theme" } show_background_menu() { diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index c4787f45..11d08667 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -1,14 +1,11 @@ #!/bin/bash # omarchy:summary=Open a generic image selector menu -# omarchy:group=menu -# omarchy:name=images -# omarchy:args=[--selected ] [--namespace ] [--colors-file ] ... +# omarchy:args=[--selected ] [--colors-file ] ... OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} selected_image="" -namespace="omarchy-image-selector" colors_file="" image_dirs=() @@ -18,16 +15,12 @@ while [[ $# -gt 0 ]]; do selected_image="$2" shift 2 ;; - --namespace) - namespace="$2" - shift 2 - ;; --colors-file) colors_file="$2" shift 2 ;; --help|-h) - echo "Usage: omarchy-menu-images [--selected ] [--namespace ] [--colors-file ] ..." + echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] ..." exit 0 ;; *) @@ -38,7 +31,7 @@ while [[ $# -gt 0 ]]; do done if (( ${#image_dirs[@]} == 0 )); then - echo "Usage: omarchy-menu-images [--selected ] [--namespace ] [--colors-file ] ..." >&2 + echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] ..." >&2 exit 1 fi @@ -85,7 +78,6 @@ done OMARCHY_IMAGE_SELECTOR_SELECTION_FILE=$selection_file \ OMARCHY_IMAGE_SELECTOR_DIRS="$image_dirs_env" \ OMARCHY_IMAGE_SELECTOR_SELECTED="$selected_list_image" \ - OMARCHY_IMAGE_SELECTOR_NAMESPACE="$namespace" \ OMARCHY_IMAGE_SELECTOR_COLORS_FILE="$colors_file" \ quickshell -p "$OMARCHY_PATH/default/quickshell/wallpaper-switcher.qml" >/dev/null diff --git a/bin/omarchy-theme-bg-switcher b/bin/omarchy-theme-bg-switcher index 06037639..16b9e2bb 100755 --- a/bin/omarchy-theme-bg-switcher +++ b/bin/omarchy-theme-bg-switcher @@ -11,6 +11,5 @@ current_background=$(readlink -f "$HOME/.config/omarchy/current/background" 2>/d omarchy-menu-images \ --selected "$current_background" \ - --namespace "omarchy-wallpaper-switcher" \ "$HOME/.config/omarchy/current/theme/backgrounds" \ "$HOME/.config/omarchy/backgrounds/$theme_name" diff --git a/bin/omarchy-theme-switcher b/bin/omarchy-theme-switcher new file mode 100755 index 00000000..3b498394 --- /dev/null +++ b/bin/omarchy-theme-switcher @@ -0,0 +1,62 @@ +#!/bin/bash + +# omarchy:summary=Open the Omarchy theme switcher + +OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} +USER_THEMES_PATH="$HOME/.config/omarchy/themes" +OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes" + +preview_dir=$(mktemp -d) +trap 'rm -rf "$preview_dir"' EXIT + +find_preview() { + local theme_path="$1" + + if [[ -f $theme_path/preview.png ]]; then + printf '%s\n' "$theme_path/preview.png" + elif [[ -f $theme_path/preview.jpg ]]; then + printf '%s\n' "$theme_path/preview.jpg" + elif [[ -d $theme_path/backgrounds ]]; then + find -L "$theme_path/backgrounds" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) -print -quit 2>/dev/null + fi +} + +add_theme_preview() { + local theme_name="$1" + local preview="$2" + local extension="${preview##*.}" + + [[ -n $preview ]] || return + [[ -e $preview_dir/$theme_name.$extension ]] && return + + ln -s "$preview" "$preview_dir/$theme_name.$extension" +} + +while IFS= read -r theme_path; do + theme_name=${theme_path##*/} + preview=$(find_preview "$theme_path") + + if [[ -z $preview ]]; then + preview=$(find_preview "$OMARCHY_THEMES_PATH/$theme_name") + fi + + add_theme_preview "$theme_name" "$preview" +done < <(find -L "$USER_THEMES_PATH" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -print 2>/dev/null | sort) + +while IFS= read -r theme_path; do + theme_name=${theme_path##*/} + preview=$(find_preview "$theme_path") + add_theme_preview "$theme_name" "$preview" +done < <(find -L "$OMARCHY_THEMES_PATH" -mindepth 1 -maxdepth 1 -type d -print 2>/dev/null | sort) + +current_theme=$(cat "$HOME/.config/omarchy/current/theme.name" 2>/dev/null) +selected_preview=$(find -L "$preview_dir" -maxdepth 1 -type f -name "$current_theme.*" -print -quit 2>/dev/null) +selected_preview=$(omarchy-menu-images \ + --selected "$selected_preview" \ + "$preview_dir") + +if [[ -n $selected_preview ]]; then + selected_theme=${selected_preview##*/} + selected_theme=${selected_theme%.*} + printf '%s\n' "$selected_theme" +fi diff --git a/default/quickshell/wallpaper-switcher.qml b/default/quickshell/wallpaper-switcher.qml index f54a4d4e..e686fbe9 100644 --- a/default/quickshell/wallpaper-switcher.qml +++ b/default/quickshell/wallpaper-switcher.qml @@ -12,7 +12,6 @@ ShellRoot { property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_WALLPAPER_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/wallpaper-switcher-colors.json") - property string layerNamespace: Quickshell.env("OMARCHY_IMAGE_SELECTOR_NAMESPACE") || "omarchy-image-selector" property int selectedIndex: 0 property bool imagesLoaded: false property color accent: "#798186" @@ -134,7 +133,7 @@ ShellRoot { visible: root.imagesLoaded anchors { top: true; bottom: true; left: true; right: true } color: "transparent" - WlrLayershell.namespace: root.layerNamespace + WlrLayershell.namespace: "omarchy-image-selector" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive exclusionMode: ExclusionMode.Ignore From 0b2332e45d7f4c93e3c7e8fd41a59ec218eca4b3 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 17:35:21 +0200 Subject: [PATCH 09/27] Permanent process for speed --- bin/omarchy-menu-images | 109 +++++++++++++++++++--- default/hypr/autostart.lua | 1 + default/quickshell/wallpaper-switcher.qml | 104 +++++++++++++++++---- 3 files changed, 184 insertions(+), 30 deletions(-) diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index 11d08667..9f50c729 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -36,7 +36,11 @@ if (( ${#image_dirs[@]} == 0 )); then fi selection_file=$(mktemp) -trap 'rm -f "$selection_file"' EXIT +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" +selector_qml="$OMARCHY_PATH/default/quickshell/wallpaper-switcher.qml" image_dirs_env="" for dir in "${image_dirs[@]}"; do @@ -60,26 +64,103 @@ if [[ -n $current_image ]]; then fi cache_dir=${XDG_CACHE_HOME:-$HOME/.cache}/omarchy/image-selector +index_file="$cache_dir/index.tsv" +rows="" mkdir -p "$cache_dir" +cache_key=$(printf '%s' "$image_dirs_env" | md5sum | cut -d ' ' -f 1) +rows_cache_file="$cache_dir/$cache_key.rows" +rows_signature_file="$cache_dir/$cache_key.signature" +rows_signature="" + for dir in "${image_dirs[@]}"; do if [[ -d $dir ]]; then - while IFS= read -r -d '' image; do - hash=$(md5sum "$image" | cut -d ' ' -f 1) - thumbnail="$cache_dir/$hash.jpg" - - if [[ ! -f $thumbnail ]]; then - magick "$image" -auto-orient -resize '1536x864^' -gravity center -extent '1536x864' -strip -quality 82 "$thumbnail" - fi - done < <(find -L "$dir" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) -print0 2>/dev/null) + rows_signature+="$dir:$(stat -Lc '%Y' "$dir")"$'\n' fi done -OMARCHY_IMAGE_SELECTOR_SELECTION_FILE=$selection_file \ - OMARCHY_IMAGE_SELECTOR_DIRS="$image_dirs_env" \ - OMARCHY_IMAGE_SELECTOR_SELECTED="$selected_list_image" \ - OMARCHY_IMAGE_SELECTOR_COLORS_FILE="$colors_file" \ - quickshell -p "$OMARCHY_PATH/default/quickshell/wallpaper-switcher.qml" >/dev/null +thumbnail_for() { + local image="$1" + local signature hash thumbnail + + signature=$(stat -Lc '%s:%Y' "$image") || return + hash=$(awk -F '\t' -v path="$image" -v sig="$signature" '$1 == path && $2 == sig { print $3; exit }' "$index_file" 2>/dev/null) + + if [[ -z $hash ]]; then + hash=$(md5sum "$image" | cut -d ' ' -f 1) + printf '%s\t%s\t%s\n' "$image" "$signature" "$hash" >>"$index_file" + fi + + thumbnail="$cache_dir/$hash.jpg" + + if [[ ! -f $thumbnail ]]; then + magick "$image" -auto-orient -resize '1536x864^' -gravity center -extent '1536x864' -strip -quality 82 "$thumbnail" + fi + + printf '%s' "$thumbnail" +} + +if [[ -f $rows_cache_file && -f $rows_signature_file ]] && cmp -s "$rows_signature_file" <(printf '%s' "$rows_signature"); then + rows=$(<"$rows_cache_file") +else + for dir in "${image_dirs[@]}"; do + if [[ -d $dir ]]; then + while IFS= read -r -d '' image; do + thumbnail=$(thumbnail_for "$image") + [[ -n $thumbnail ]] || continue + + if [[ -z $rows ]]; then + rows="$image"$'\t'"$thumbnail" + else + rows+=$'\n'"$image"$'\t'"$thumbnail" + fi + done < <(find -L "$dir" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) -print0 2>/dev/null) + fi + done + + printf '%s' "$rows" >"$rows_cache_file" + printf '%s' "$rows_signature" >"$rows_signature_file" +fi + +rows_payload=${rows//$'\t'/$'\f'} +rows_payload=${rows_payload//$'\n'/$'\v'} + +selector_pid=$(quickshell list -p "$selector_qml" --json 2>/dev/null | jq -r '.[0].pid // empty') +if [[ -n $selector_pid ]]; then + selector_age=$(ps -o etimes= -p "$selector_pid" 2>/dev/null | tr -d ' ') + qml_mtime=$(stat -c %Y "$selector_qml") + now=$(date +%s) + + if [[ -n $selector_age ]] && (( now - selector_age < qml_mtime )); then + quickshell kill -p "$selector_qml" >/dev/null 2>&1 || true + rm -f "$socket_path" + fi +fi + +if [[ ! -S $socket_path ]]; then + quickshell kill -p "$selector_qml" >/dev/null 2>&1 || true + quickshell -d -p "$selector_qml" >/dev/null + + for ((i = 0; i < 100; i++)); do + if [[ -S $socket_path ]]; then + break + fi + + sleep 0.01 + done +fi + +if [[ ! -S $socket_path ]]; then + echo "Image selector failed to start" >&2 + exit 1 +fi + +printf '%s\t%s\t%s\t%s\t%s\n' "$rows_payload" "$selected_list_image" "$selection_file" "$done_file" "$colors_file" | + socat -u - "UNIX-CONNECT:$socket_path" + +while [[ ! -e $done_file ]]; do + sleep 0.01 +done if [[ -s $selection_file ]]; then cat "$selection_file" diff --git a/default/hypr/autostart.lua b/default/hypr/autostart.lua index 7eed5fbb..846b788a 100644 --- a/default/hypr/autostart.lua +++ b/default/hypr/autostart.lua @@ -4,6 +4,7 @@ hl.on("hyprland.start", function() hl.exec_cmd("! omarchy-toggle-enabled waybar-off && uwsm-app -- waybar") hl.exec_cmd("uwsm-app -- fcitx5 --disable notificationitem") hl.exec_cmd("uwsm-app -- swaybg -i ~/.config/omarchy/current/background -m fill") + hl.exec_cmd("uwsm-app -- quickshell -p ~/.local/share/omarchy/default/quickshell/wallpaper-switcher.qml") hl.exec_cmd("/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1") hl.exec_cmd("omarchy-first-run") hl.exec_cmd("omarchy-powerprofiles-init") diff --git a/default/quickshell/wallpaper-switcher.qml b/default/quickshell/wallpaper-switcher.qml index e686fbe9..01bc508e 100644 --- a/default/quickshell/wallpaper-switcher.qml +++ b/default/quickshell/wallpaper-switcher.qml @@ -9,11 +9,15 @@ ShellRoot { id: root property string imageDirs: Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIRS") || Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIR") || Quickshell.env("OMARCHY_STOCK_BACKGROUNDS_DIR") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/backgrounds") + property string imageRows: "" property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_WALLPAPER_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/wallpaper-switcher-colors.json") property int selectedIndex: 0 property bool imagesLoaded: false + property bool opened: false + property string doneFile: "" + 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" @@ -31,6 +35,10 @@ ShellRoot { return "'" + String(value).replace(/'/g, "'\\''") + "'" } + function decodeField(value) { + return String(value || "").replace(/\v/g, "\n").replace(/\f/g, "\t") + } + function currentPath() { if (imageModel.count === 0) return "" return imageModel.get(selectedIndex).filePath @@ -49,10 +57,51 @@ ShellRoot { function applySelected() { var path = currentPath() if (!path) return - applyProc.command = ["bash", "-lc", "printf '%s\\n' " + shellQuote(path) + " > " + shellQuote(selectionFile)] + applyProc.command = ["bash", "-lc", "printf '%s\\n' " + shellQuote(path) + " > " + shellQuote(selectionFile) + "; : > " + shellQuote(doneFile)] applyProc.running = true } + function cancel() { + cancelProc.command = ["bash", "-lc", ": > " + shellQuote(doneFile)] + cancelProc.running = true + } + + function loadRows(rows) { + var paths = rows.split("\n") + for (var i = 0; i < paths.length; i++) { + var row = paths[i].trim() + if (!row) continue + + var columns = row.split("\t") + root.addImage(columns[0], columns[1]) + } + + root.select(root.selectedImageIndex()) + root.imagesLoaded = true + root.opened = true + list.forceActiveFocus() + } + + function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextColorsFile) { + imageDirs = nextImageDirs + imageRows = nextImageRows + selectedImage = nextSelectedImage + selectionFile = nextSelectionFile + doneFile = nextDoneFile + colorsFile = nextColorsFile || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/wallpaper-switcher-colors.json") + imageModel.clear() + selectedIndex = 0 + list.currentIndex = 0 + imagesLoaded = false + opened = false + if (imageRows) { + loadRows(imageRows) + } else { + loadImagesProc.output = "" + loadImagesProc.running = true + } + } + ListModel { id: imageModel onCountChanged: { @@ -92,20 +141,38 @@ ShellRoot { } } onExited: { - var paths = output.split("\n") - for (var i = 0; i < paths.length; i++) { - var row = paths[i].trim() - if (!row) continue - - var columns = row.split("\t") - root.addImage(columns[0], columns[1]) - } - root.select(root.selectedImageIndex()) - root.imagesLoaded = true + root.loadRows(output) } } - Component.onCompleted: loadImagesProc.running = true + Component.onCompleted: { + if (selectionFile) + openSelector(imageDirs, "", selectedImage, selectionFile, Quickshell.env("OMARCHY_IMAGE_SELECTOR_DONE_FILE"), colorsFile) + } + + 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) + } + } + + SocketServer { + active: true + path: root.socketPath + + handler: Socket { + id: clientSocket + parser: SplitParser { + onRead: function(message) { + var fields = message.split("\t") + root.openSelector("", root.decodeField(fields[0]), fields[1] || "", fields[2] || "", fields[3] || "", fields[4] || "") + clientSocket.connected = false + } + } + } + } FileView { path: root.colorsFile @@ -125,12 +192,17 @@ ShellRoot { Process { id: applyProc - onExited: Qt.quit() + onExited: root.opened = false + } + + Process { + id: cancelProc + onExited: root.opened = false } PanelWindow { id: panel - visible: root.imagesLoaded + visible: root.opened && root.imagesLoaded anchors { top: true; bottom: true; left: true; right: true } color: "transparent" WlrLayershell.namespace: "omarchy-image-selector" @@ -145,7 +217,7 @@ ShellRoot { MouseArea { anchors.fill: parent - onClicked: Qt.quit() + onClicked: root.cancel() } Item { @@ -189,7 +261,7 @@ ShellRoot { Keys.priority: Keys.BeforeItem Keys.onPressed: function(event) { if (event.key === Qt.Key_Escape) { - Qt.quit() + root.cancel() event.accepted = true } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { root.applySelected() From 000b7c79afb61f535d2b3135a96dcf47b8448d9a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 17:42:55 +0200 Subject: [PATCH 10/27] Improve performance --- bin/omarchy-menu-images | 37 +++++++++++++--------- bin/omarchy-theme-switcher | 65 ++++++++++++++++++++++++-------------- 2 files changed, 63 insertions(+), 39 deletions(-) diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index 9f50c729..ad8e0dc2 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -1,12 +1,13 @@ #!/bin/bash # omarchy:summary=Open a generic image selector menu -# omarchy:args=[--selected ] [--colors-file ] ... +# omarchy:args=[--selected ] [--colors-file ] [--print-name] ... OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} selected_image="" colors_file="" +print_name=false image_dirs=() while [[ $# -gt 0 ]]; do @@ -19,8 +20,12 @@ while [[ $# -gt 0 ]]; do colors_file="$2" shift 2 ;; + --print-name) + print_name=true + shift + ;; --help|-h) - echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] ..." + echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] ..." exit 0 ;; *) @@ -31,7 +36,7 @@ while [[ $# -gt 0 ]]; do done if (( ${#image_dirs[@]} == 0 )); then - echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] ..." >&2 + echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] ..." >&2 exit 1 fi @@ -56,7 +61,10 @@ selected_list_image="" if [[ -n $current_image ]]; then for dir in "${image_dirs[@]}"; do - if [[ -d $dir ]]; then + if [[ -d $dir && -f $selected_image && ${selected_image%/*} == "$dir" ]]; then + selected_list_image="$selected_image" + break + elif [[ -d $dir ]]; then selected_list_image=$(find -L "$dir" -maxdepth 1 -type f -samefile "$current_image" -print -quit 2>/dev/null) [[ -n $selected_list_image ]] && break fi @@ -125,16 +133,9 @@ fi rows_payload=${rows//$'\t'/$'\f'} rows_payload=${rows_payload//$'\n'/$'\v'} -selector_pid=$(quickshell list -p "$selector_qml" --json 2>/dev/null | jq -r '.[0].pid // empty') -if [[ -n $selector_pid ]]; then - selector_age=$(ps -o etimes= -p "$selector_pid" 2>/dev/null | tr -d ' ') - qml_mtime=$(stat -c %Y "$selector_qml") - now=$(date +%s) - - if [[ -n $selector_age ]] && (( now - selector_age < qml_mtime )); then - quickshell kill -p "$selector_qml" >/dev/null 2>&1 || true - rm -f "$socket_path" - fi +if [[ -S $socket_path && $selector_qml -nt $socket_path ]]; then + quickshell kill -p "$selector_qml" >/dev/null 2>&1 || true + rm -f "$socket_path" fi if [[ ! -S $socket_path ]]; then @@ -163,5 +164,11 @@ while [[ ! -e $done_file ]]; do done if [[ -s $selection_file ]]; then - cat "$selection_file" + if [[ $print_name == true ]]; then + selection=$(<"$selection_file") + selection=${selection##*/} + printf '%s\n' "${selection%.*}" + else + cat "$selection_file" + fi fi diff --git a/bin/omarchy-theme-switcher b/bin/omarchy-theme-switcher index 3b498394..89480c96 100755 --- a/bin/omarchy-theme-switcher +++ b/bin/omarchy-theme-switcher @@ -5,9 +5,11 @@ OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} USER_THEMES_PATH="$HOME/.config/omarchy/themes" OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes" +CACHE_PATH="${XDG_CACHE_HOME:-$HOME/.cache}/omarchy/theme-selector" +preview_dir="$CACHE_PATH/previews" +signature_file="$CACHE_PATH/signature" -preview_dir=$(mktemp -d) -trap 'rm -rf "$preview_dir"' EXIT +mkdir -p "$preview_dir" find_preview() { local theme_path="$1" @@ -32,31 +34,46 @@ add_theme_preview() { ln -s "$preview" "$preview_dir/$theme_name.$extension" } -while IFS= read -r theme_path; do - theme_name=${theme_path##*/} - preview=$(find_preview "$theme_path") - - if [[ -z $preview ]]; then - preview=$(find_preview "$OMARCHY_THEMES_PATH/$theme_name") +theme_signature="" +for theme_dir in "$USER_THEMES_PATH" "$OMARCHY_THEMES_PATH"; do + if [[ -d $theme_dir ]]; then + theme_signature+="$theme_dir:$(stat -Lc '%Y' "$theme_dir")"$'\n' fi +done - add_theme_preview "$theme_name" "$preview" -done < <(find -L "$USER_THEMES_PATH" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -print 2>/dev/null | sort) +if [[ ! -f $signature_file ]] || ! cmp -s "$signature_file" <(printf '%s' "$theme_signature"); then + rm -rf "$preview_dir" + mkdir -p "$preview_dir" -while IFS= read -r theme_path; do - theme_name=${theme_path##*/} - preview=$(find_preview "$theme_path") - add_theme_preview "$theme_name" "$preview" -done < <(find -L "$OMARCHY_THEMES_PATH" -mindepth 1 -maxdepth 1 -type d -print 2>/dev/null | sort) + while IFS= read -r theme_path; do + theme_name=${theme_path##*/} + preview=$(find_preview "$theme_path") + + if [[ -z $preview ]]; then + preview=$(find_preview "$OMARCHY_THEMES_PATH/$theme_name") + fi + + add_theme_preview "$theme_name" "$preview" + done < <(find -L "$USER_THEMES_PATH" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -print 2>/dev/null | sort) + + while IFS= read -r theme_path; do + theme_name=${theme_path##*/} + preview=$(find_preview "$theme_path") + add_theme_preview "$theme_name" "$preview" + done < <(find -L "$OMARCHY_THEMES_PATH" -mindepth 1 -maxdepth 1 -type d -print 2>/dev/null | sort) + + printf '%s' "$theme_signature" >"$signature_file" +fi current_theme=$(cat "$HOME/.config/omarchy/current/theme.name" 2>/dev/null) -selected_preview=$(find -L "$preview_dir" -maxdepth 1 -type f -name "$current_theme.*" -print -quit 2>/dev/null) -selected_preview=$(omarchy-menu-images \ +selected_preview="" +for extension in png jpg jpeg webp; do + if [[ -e $preview_dir/$current_theme.$extension ]]; then + selected_preview="$preview_dir/$current_theme.$extension" + break + fi +done +exec omarchy-menu-images \ + --print-name \ --selected "$selected_preview" \ - "$preview_dir") - -if [[ -n $selected_preview ]]; then - selected_theme=${selected_preview##*/} - selected_theme=${selected_theme%.*} - printf '%s\n' "$selected_theme" -fi + "$preview_dir" From b48f67058be4645dc723fceeeb99fbb5182d52f8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 17:50:04 +0200 Subject: [PATCH 11/27] Use consistent terms --- bin/omarchy-menu-images | 2 +- bin/omarchy-theme-bg-set | 2 +- bin/omarchy-theme-bg-switcher | 4 ++-- default/elephant/omarchy_background_selector.lua | 4 ++-- default/hypr/autostart.lua | 2 +- default/hypr/bindings/utilities.lua | 2 +- .../{wallpaper-switcher.qml => background-switcher.qml} | 9 +++++---- ...lors.json.tpl => background-switcher-colors.json.tpl} | 0 8 files changed, 13 insertions(+), 12 deletions(-) rename default/quickshell/{wallpaper-switcher.qml => background-switcher.qml} (94%) rename default/themed/{wallpaper-switcher-colors.json.tpl => background-switcher-colors.json.tpl} (100%) diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index ad8e0dc2..f76cf6ea 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -45,7 +45,7 @@ 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" -selector_qml="$OMARCHY_PATH/default/quickshell/wallpaper-switcher.qml" +selector_qml="$OMARCHY_PATH/default/quickshell/background-switcher.qml" image_dirs_env="" for dir in "${image_dirs[@]}"; do diff --git a/bin/omarchy-theme-bg-set b/bin/omarchy-theme-bg-set index 9c695d0e..803bea36 100755 --- a/bin/omarchy-theme-bg-set +++ b/bin/omarchy-theme-bg-set @@ -2,7 +2,7 @@ # omarchy:summary=Set the current background image # omarchy:args= -# omarchy:examples=omarchy theme bg set ~/Pictures/wallpaper.png +# omarchy:examples=omarchy theme bg set ~/Pictures/background.png if [[ -z $1 ]]; then echo "Usage: omarchy-theme-bg-set " >&2 diff --git a/bin/omarchy-theme-bg-switcher b/bin/omarchy-theme-bg-switcher index 16b9e2bb..d3846027 100755 --- a/bin/omarchy-theme-bg-switcher +++ b/bin/omarchy-theme-bg-switcher @@ -1,9 +1,9 @@ #!/bin/bash -# omarchy:summary=Open the Omarchy wallpaper switcher +# omarchy:summary=Open the Omarchy background switcher # omarchy:group=theme # omarchy:name=bg-switcher -# omarchy:aliases=omarchy wallpaper +# omarchy:aliases=omarchy background OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} theme_name=$(cat "$HOME/.config/omarchy/current/theme.name" 2>/dev/null) diff --git a/default/elephant/omarchy_background_selector.lua b/default/elephant/omarchy_background_selector.lua index 38bc2bf1..4b96f4a6 100644 --- a/default/elephant/omarchy_background_selector.lua +++ b/default/elephant/omarchy_background_selector.lua @@ -44,9 +44,9 @@ function GetEntries() -- Track added files to avoid duplicates local seen = {} - for _, wallpaper_dir in ipairs(dirs) do + for _, background_dir in ipairs(dirs) do local handle = io.popen( - "find -L " .. ShellEscape(wallpaper_dir) + "find -L " .. ShellEscape(background_dir) .. " -maxdepth 1 -type f \\( -name '*.jpg' -o -name '*.jpeg' -o -name '*.png' -o -name '*.gif' -o -name '*.bmp' -o -name '*.webp' \\) 2>/dev/null | sort" ) if handle then diff --git a/default/hypr/autostart.lua b/default/hypr/autostart.lua index 846b788a..6d2812c9 100644 --- a/default/hypr/autostart.lua +++ b/default/hypr/autostart.lua @@ -4,7 +4,7 @@ hl.on("hyprland.start", function() hl.exec_cmd("! omarchy-toggle-enabled waybar-off && uwsm-app -- waybar") hl.exec_cmd("uwsm-app -- fcitx5 --disable notificationitem") hl.exec_cmd("uwsm-app -- swaybg -i ~/.config/omarchy/current/background -m fill") - hl.exec_cmd("uwsm-app -- quickshell -p ~/.local/share/omarchy/default/quickshell/wallpaper-switcher.qml") + hl.exec_cmd("uwsm-app -- quickshell -p ~/.local/share/omarchy/default/quickshell/background-switcher.qml") hl.exec_cmd("/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1") hl.exec_cmd("omarchy-first-run") hl.exec_cmd("omarchy-powerprofiles-init") diff --git a/default/hypr/bindings/utilities.lua b/default/hypr/bindings/utilities.lua index 57a3e70e..ef068b02 100644 --- a/default/hypr/bindings/utilities.lua +++ b/default/hypr/bindings/utilities.lua @@ -15,7 +15,7 @@ hl.bind("SUPER + SHIFT + CTRL + UP", hl.dsp.exec_cmd("omarchy-style-waybar-posit hl.bind("SUPER + SHIFT + CTRL + DOWN", hl.dsp.exec_cmd("omarchy-style-waybar-position bottom"), { description = "Move Waybar to bottom" }) hl.bind("SUPER + SHIFT + CTRL + LEFT", hl.dsp.exec_cmd("omarchy-style-waybar-position left"), { description = "Move Waybar to left" }) hl.bind("SUPER + SHIFT + CTRL + RIGHT", hl.dsp.exec_cmd("omarchy-style-waybar-position right"), { description = "Move Waybar to right" }) -hl.bind("SUPER + CTRL + SPACE", hl.dsp.exec_cmd("bash -lc 'background=$(omarchy-theme-bg-switcher) && [[ -n $background ]] && omarchy-theme-bg-set \"$background\"'"), { description = "Wallpaper switcher" }) +hl.bind("SUPER + CTRL + SPACE", hl.dsp.exec_cmd("omarchy-menu background"), { description = "Background switcher" }) hl.bind("SUPER + SHIFT + CTRL + SPACE", hl.dsp.exec_cmd("omarchy-menu 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" }) diff --git a/default/quickshell/wallpaper-switcher.qml b/default/quickshell/background-switcher.qml similarity index 94% rename from default/quickshell/wallpaper-switcher.qml rename to default/quickshell/background-switcher.qml index 01bc508e..aeea282f 100644 --- a/default/quickshell/wallpaper-switcher.qml +++ b/default/quickshell/background-switcher.qml @@ -10,9 +10,9 @@ ShellRoot { property string imageDirs: Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIRS") || Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIR") || Quickshell.env("OMARCHY_STOCK_BACKGROUNDS_DIR") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/backgrounds") property string imageRows: "" - property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_WALLPAPER_SELECTION_FILE") + 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/wallpaper-switcher-colors.json") + property string colorsFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_COLORS_FILE") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/background-switcher-colors.json") property int selectedIndex: 0 property bool imagesLoaded: false property bool opened: false @@ -57,6 +57,7 @@ ShellRoot { function applySelected() { var path = currentPath() if (!path) return + if (!selectionFile) return applyProc.command = ["bash", "-lc", "printf '%s\\n' " + shellQuote(path) + " > " + shellQuote(selectionFile) + "; : > " + shellQuote(doneFile)] applyProc.running = true } @@ -88,7 +89,7 @@ ShellRoot { selectedImage = nextSelectedImage selectionFile = nextSelectionFile doneFile = nextDoneFile - colorsFile = nextColorsFile || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/wallpaper-switcher-colors.json") + colorsFile = nextColorsFile || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/background-switcher-colors.json") imageModel.clear() selectedIndex = 0 list.currentIndex = 0 @@ -134,7 +135,7 @@ ShellRoot { Process { id: loadImagesProc property string output: "" - command: ["bash", "-lc", "cache_dir=${XDG_CACHE_HOME:-$HOME/.cache}/omarchy/image-selector; while IFS= read -r dir; do [[ -d $dir ]] && find -L \"$dir\" -maxdepth 1 -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\) -print0; done <<< " + shellQuote(root.imageDirs) + " | sort -z | while IFS= read -r -d '' image; do hash=$(md5sum \"$image\" | cut -d ' ' -f 1); thumb=\"$cache_dir/$hash.jpg\"; [[ -f $thumb ]] || thumb=$image; printf '%s\\t%s\\n' \"$image\" \"$thumb\"; done"] + command: ["bash", "-lc", "cache_dir=${XDG_CACHE_HOME:-$HOME/.cache}/omarchy/image-selector; while IFS= read -r dir; do [[ -n $dir && -d $dir ]] && find -L \"$dir\" -maxdepth 1 -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\) -print0; done <<< " + shellQuote(root.imageDirs) + " | sort -z | while IFS= read -r -d '' image; do hash=$(md5sum \"$image\" | cut -d ' ' -f 1); thumb=\"$cache_dir/$hash.jpg\"; [[ -f $thumb ]] || thumb=$image; printf '%s\\t%s\\n' \"$image\" \"$thumb\"; done"] stdout: SplitParser { onRead: function(data) { loadImagesProc.output += data + "\n" diff --git a/default/themed/wallpaper-switcher-colors.json.tpl b/default/themed/background-switcher-colors.json.tpl similarity index 100% rename from default/themed/wallpaper-switcher-colors.json.tpl rename to default/themed/background-switcher-colors.json.tpl From 0426f9b206b6d18cd337a1adf662acf3f3781959 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 18:07:09 +0200 Subject: [PATCH 12/27] Cache warming --- bin/omarchy-menu-images | 15 ++++++++++++--- bin/omarchy-theme-bg-cache | 10 ++++++++++ bin/omarchy-theme-set | 3 +++ 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100755 bin/omarchy-theme-bg-cache diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index f76cf6ea..e5909560 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -1,13 +1,14 @@ #!/bin/bash # omarchy:summary=Open a generic image selector menu -# omarchy:args=[--selected ] [--colors-file ] [--print-name] ... +# omarchy:args=[--selected ] [--colors-file ] [--print-name] [--cache-only] ... OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} selected_image="" colors_file="" print_name=false +cache_only=false image_dirs=() while [[ $# -gt 0 ]]; do @@ -24,8 +25,12 @@ while [[ $# -gt 0 ]]; do print_name=true shift ;; + --cache-only) + cache_only=true + shift + ;; --help|-h) - echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] ..." + echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--cache-only] ..." exit 0 ;; *) @@ -36,7 +41,7 @@ while [[ $# -gt 0 ]]; do done if (( ${#image_dirs[@]} == 0 )); then - echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] ..." >&2 + echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--cache-only] ..." >&2 exit 1 fi @@ -133,6 +138,10 @@ fi rows_payload=${rows//$'\t'/$'\f'} rows_payload=${rows_payload//$'\n'/$'\v'} +if [[ $cache_only == true ]]; then + exit 0 +fi + if [[ -S $socket_path && $selector_qml -nt $socket_path ]]; then quickshell kill -p "$selector_qml" >/dev/null 2>&1 || true rm -f "$socket_path" diff --git a/bin/omarchy-theme-bg-cache b/bin/omarchy-theme-bg-cache new file mode 100755 index 00000000..4564e5bd --- /dev/null +++ b/bin/omarchy-theme-bg-cache @@ -0,0 +1,10 @@ +#!/bin/bash + +# omarchy:summary=Cache background switcher thumbnails for the current theme + +theme_name=$(cat "$HOME/.config/omarchy/current/theme.name" 2>/dev/null) + +omarchy-menu-images \ + --cache-only \ + "$HOME/.config/omarchy/current/theme/backgrounds" \ + "$HOME/.config/omarchy/backgrounds/$theme_name" diff --git a/bin/omarchy-theme-set b/bin/omarchy-theme-set index fe57c2af..8dafed8b 100755 --- a/bin/omarchy-theme-set +++ b/bin/omarchy-theme-set @@ -71,3 +71,6 @@ omarchy-theme-set-keyboard # Call hook on theme set omarchy-hook theme-set "$THEME_NAME" >/dev/null + +# Warm the background selector cache after the theme is applied, off the critical path. +omarchy-theme-bg-cache >/dev/null 2>&1 & From ef129b9dabc5013af5e9c8a9c65486bdfc798e2a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 18:07:16 +0200 Subject: [PATCH 13/27] Consistency --- bin/omarchy-transcode | 2 +- default/omarchy-skill/SKILL.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/omarchy-transcode b/bin/omarchy-transcode index 24103f56..a8b3d8ad 100755 --- a/bin/omarchy-transcode +++ b/bin/omarchy-transcode @@ -4,7 +4,7 @@ # omarchy:name= # omarchy:summary=Transcode pictures and videos for sharing # omarchy:args=[--path path] [input] [format] [resolution] -# omarchy:examples=omarchy transcode|omarchy transcode --path ~/Downloads|omarchy transcode ~/Videos/demo.mov mp4 1080p|omarchy transcode ~/Pictures/wallpaper.heic jpg medium +# omarchy:examples=omarchy transcode|omarchy transcode --path ~/Downloads|omarchy transcode ~/Videos/demo.mov mp4 1080p|omarchy transcode ~/Pictures/background.heic jpg medium set -euo pipefail diff --git a/default/omarchy-skill/SKILL.md b/default/omarchy-skill/SKILL.md index 37ae3d12..b843bae3 100644 --- a/default/omarchy-skill/SKILL.md +++ b/default/omarchy-skill/SKILL.md @@ -6,7 +6,7 @@ description: > ~/.config/alacritty/, ~/.config/foot/, ~/.config/kitty/, ~/.config/ghostty/, ~/.config/mako/, or ~/.config/omarchy/. Triggers: Hyprland, window rules, animations, keybindings, monitors, gaps, borders, blur, opacity, waybar, walker, terminal config, themes, - wallpaper, night light, idle, lock screen, screenshots, reminders, layer rules, + background, night light, idle, lock screen, screenshots, reminders, layer rules, workspace settings, display config, and user-facing omarchy commands. Excludes Omarchy source development in ~/.local/share/omarchy/ and `omarchy dev` workflows. --- @@ -28,7 +28,7 @@ It is not for contributing to Omarchy source code. - Editing ANY file in `~/.config/omarchy/` - Window behavior, animations, opacity, blur, gaps, borders - Layer rules, workspace settings, display/monitor configuration -- Themes, wallpapers, fonts, appearance changes +- Themes, backgrounds, fonts, appearance changes - User-facing `omarchy` commands (`omarchy theme ...`, `omarchy refresh ...`, `omarchy restart ...`, etc.) - Screenshots, screen recording, reminders, night light, idle behavior, lock screen @@ -254,7 +254,7 @@ omarchy refresh hyprland omarchy theme list # Show available themes omarchy theme current # Show current theme omarchy theme set # Apply theme (use "Tokyo Night" not "tokyo-night") -omarchy theme bg next # Cycle wallpaper +omarchy theme bg next # Cycle background omarchy theme install # Install from git repo ``` From f6bd69bc0f0261e9397443d0ae84240921023346 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 11 May 2026 18:20:27 +0200 Subject: [PATCH 14/27] Fix color sync --- bin/omarchy-menu-images | 10 +++++++++- default/quickshell/background-switcher.qml | 10 ++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index e5909560..1d702079 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -137,6 +137,14 @@ 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_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 ]]; then exit 0 @@ -165,7 +173,7 @@ if [[ ! -S $socket_path ]]; then exit 1 fi -printf '%s\t%s\t%s\t%s\t%s\n' "$rows_payload" "$selected_list_image" "$selection_file" "$done_file" "$colors_file" | +printf '%s\t%s\t%s\t%s\t%s\n' "$rows_payload" "$selected_list_image" "$selection_file" "$done_file" "$colors_payload" | socat -u - "UNIX-CONNECT:$socket_path" while [[ ! -e $done_file ]]; do diff --git a/default/quickshell/background-switcher.qml b/default/quickshell/background-switcher.qml index aeea282f..83c2b7b8 100644 --- a/default/quickshell/background-switcher.qml +++ b/default/quickshell/background-switcher.qml @@ -83,13 +83,15 @@ ShellRoot { list.forceActiveFocus() } - function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextColorsFile) { + function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextColorsFile, nextColorsRaw) { imageDirs = nextImageDirs imageRows = nextImageRows selectedImage = nextSelectedImage selectionFile = nextSelectionFile doneFile = nextDoneFile colorsFile = nextColorsFile || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/background-switcher-colors.json") + if (nextColorsRaw) + loadColors(nextColorsRaw) imageModel.clear() selectedIndex = 0 list.currentIndex = 0 @@ -148,14 +150,14 @@ ShellRoot { Component.onCompleted: { if (selectionFile) - openSelector(imageDirs, "", selectedImage, selectionFile, Quickshell.env("OMARCHY_IMAGE_SELECTOR_DONE_FILE"), colorsFile) + openSelector(imageDirs, "", selectedImage, selectionFile, Quickshell.env("OMARCHY_IMAGE_SELECTOR_DONE_FILE"), colorsFile, "") } 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) + root.openSelector(imageDirs, imageRows, selectedImage, selectionFile, doneFile, colorsFile, "") } } @@ -168,7 +170,7 @@ ShellRoot { parser: SplitParser { onRead: function(message) { var fields = message.split("\t") - root.openSelector("", root.decodeField(fields[0]), fields[1] || "", fields[2] || "", fields[3] || "", fields[4] || "") + root.openSelector("", root.decodeField(fields[0]), fields[1] || "", fields[2] || "", fields[3] || "", "", root.decodeField(fields[4])) clientSocket.connected = false } } From b89079ae5c979fc01eb1552f1a1e250b3674fe60 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 15:40:19 -0400 Subject: [PATCH 15/27] Show theme names in theme switcher --- bin/omarchy-menu-images | 13 +++++--- bin/omarchy-theme-switcher | 1 + default/quickshell/background-switcher.qml | 36 ++++++++++++++++++---- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index 1d702079..bf0c535d 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -1,13 +1,14 @@ #!/bin/bash # omarchy:summary=Open a generic image selector menu -# omarchy:args=[--selected ] [--colors-file ] [--print-name] [--cache-only] ... +# omarchy:args=[--selected ] [--colors-file ] [--print-name] [--show-labels] [--cache-only] ... OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} selected_image="" colors_file="" print_name=false +show_labels=false cache_only=false image_dirs=() @@ -25,12 +26,16 @@ while [[ $# -gt 0 ]]; do print_name=true shift ;; + --show-labels) + show_labels=true + shift + ;; --cache-only) cache_only=true shift ;; --help|-h) - echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--cache-only] ..." + echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--show-labels] [--cache-only] ..." exit 0 ;; *) @@ -41,7 +46,7 @@ while [[ $# -gt 0 ]]; do done if (( ${#image_dirs[@]} == 0 )); then - echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--cache-only] ..." >&2 + echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--show-labels] [--cache-only] ..." >&2 exit 1 fi @@ -173,7 +178,7 @@ if [[ ! -S $socket_path ]]; then exit 1 fi -printf '%s\t%s\t%s\t%s\t%s\n' "$rows_payload" "$selected_list_image" "$selection_file" "$done_file" "$colors_payload" | +printf '%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" | socat -u - "UNIX-CONNECT:$socket_path" while [[ ! -e $done_file ]]; do diff --git a/bin/omarchy-theme-switcher b/bin/omarchy-theme-switcher index 89480c96..a21ed4aa 100755 --- a/bin/omarchy-theme-switcher +++ b/bin/omarchy-theme-switcher @@ -75,5 +75,6 @@ for extension in png jpg jpeg webp; do done exec omarchy-menu-images \ --print-name \ + --show-labels \ --selected "$selected_preview" \ "$preview_dir" diff --git a/default/quickshell/background-switcher.qml b/default/quickshell/background-switcher.qml index 83c2b7b8..5889345e 100644 --- a/default/quickshell/background-switcher.qml +++ b/default/quickshell/background-switcher.qml @@ -16,6 +16,7 @@ ShellRoot { property int selectedIndex: 0 property bool imagesLoaded: false property bool opened: false + property bool showLabels: false property string doneFile: "" property string socketPath: (Quickshell.env("XDG_RUNTIME_DIR") || ("/run/user/" + Quickshell.env("UID"))) + "/omarchy-image-selector.sock" property color accent: "#798186" @@ -44,6 +45,14 @@ ShellRoot { return imageModel.get(selectedIndex).filePath } + function currentLabel() { + var path = currentPath() + if (!path) return "" + + var name = path.split("/").pop().replace(/\.[^/.]+$/, "") + return name.replace(/[-_]+/g, " ").replace(/\b\w/g, function(match) { return match.toUpperCase() }) + } + function select(index) { if (imageModel.count === 0) return if (index < 0) index = imageModel.count - 1 @@ -83,12 +92,13 @@ ShellRoot { list.forceActiveFocus() } - function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextColorsFile, nextColorsRaw) { + function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextColorsFile, nextColorsRaw, nextShowLabels) { imageDirs = nextImageDirs imageRows = nextImageRows selectedImage = nextSelectedImage selectionFile = nextSelectionFile doneFile = nextDoneFile + showLabels = nextShowLabels === true || nextShowLabels === "true" colorsFile = nextColorsFile || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/background-switcher-colors.json") if (nextColorsRaw) loadColors(nextColorsRaw) @@ -150,14 +160,14 @@ ShellRoot { Component.onCompleted: { if (selectionFile) - openSelector(imageDirs, "", selectedImage, selectionFile, Quickshell.env("OMARCHY_IMAGE_SELECTOR_DONE_FILE"), colorsFile, "") + openSelector(imageDirs, "", selectedImage, selectionFile, Quickshell.env("OMARCHY_IMAGE_SELECTOR_DONE_FILE"), colorsFile, "", 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, "") + root.openSelector(imageDirs, imageRows, selectedImage, selectionFile, doneFile, colorsFile, "", false) } } @@ -170,7 +180,7 @@ ShellRoot { parser: SplitParser { onRead: function(message) { var fields = message.split("\t") - root.openSelector("", root.decodeField(fields[0]), fields[1] || "", fields[2] || "", fields[3] || "", "", root.decodeField(fields[4])) + root.openSelector("", root.decodeField(fields[0]), fields[1] || "", fields[2] || "", fields[3] || "", "", root.decodeField(fields[4]), fields[5] || "false") clientSocket.connected = false } } @@ -226,7 +236,7 @@ ShellRoot { Item { id: card width: Math.min(parent.width - 80, root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing) + 40) - height: root.sliceHeight + 60 + height: root.sliceHeight + (root.showLabels ? 104 : 60) anchors.centerIn: parent MouseArea { anchors.fill: parent; onClicked: {} } @@ -236,7 +246,7 @@ ShellRoot { anchors.top: parent.top anchors.topMargin: 30 anchors.bottom: parent.bottom - anchors.bottomMargin: 30 + anchors.bottomMargin: root.showLabels ? 74 : 30 anchors.horizontalCenter: parent.horizontalCenter width: root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing) orientation: ListView.Horizontal @@ -395,6 +405,20 @@ ShellRoot { } } } + + Text { + visible: root.showLabels + anchors.top: list.bottom + anchors.topMargin: 16 + anchors.horizontalCenter: list.horizontalCenter + width: root.expandedWidth + text: root.currentLabel() + color: root.foreground + font.pixelSize: 24 + font.weight: Font.DemiBold + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } } } } From 693a23c58ce47a22b109982e54e8b7fe2443e9f3 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 15:47:01 -0400 Subject: [PATCH 16/27] Open image selector at selected item immediately --- default/quickshell/background-switcher.qml | 33 +++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/default/quickshell/background-switcher.qml b/default/quickshell/background-switcher.qml index 5889345e..e63b6f49 100644 --- a/default/quickshell/background-switcher.qml +++ b/default/quickshell/background-switcher.qml @@ -17,6 +17,7 @@ ShellRoot { property bool imagesLoaded: false property bool opened: false property bool showLabels: false + property bool animateSelection: true property string doneFile: "" property string socketPath: (Quickshell.env("XDG_RUNTIME_DIR") || ("/run/user/" + Quickshell.env("UID"))) + "/omarchy-image-selector.sock" property color accent: "#798186" @@ -53,14 +54,15 @@ ShellRoot { return name.replace(/[-_]+/g, " ").replace(/\b\w/g, function(match) { return match.toUpperCase() }) } - function select(index) { + function select(index, immediate) { if (imageModel.count === 0) return if (index < 0) index = imageModel.count - 1 else if (index >= imageModel.count) index = 0 + animateSelection = immediate !== true selectedIndex = index list.currentIndex = index - list.centerSelected() + list.centerSelected(immediate === true) } function applySelected() { @@ -86,7 +88,7 @@ ShellRoot { root.addImage(columns[0], columns[1]) } - root.select(root.selectedImageIndex()) + root.select(root.selectedImageIndex(), true) root.imagesLoaded = true root.opened = true list.forceActiveFocus() @@ -263,12 +265,20 @@ ShellRoot { header: Item { width: (list.width - root.expandedWidth) / 2; height: 1 } footer: Item { width: (list.width - root.expandedWidth) / 2; height: 1 } - function centerSelected() { - Qt.callLater(function() { - var selectedItem = list.itemAtIndex(root.selectedIndex) - if (selectedItem) - list.contentX = selectedItem.x + selectedItem.width / 2 - list.width / 2 - }) + function centerSelected(immediate) { + var center = function() { + list.contentX = Math.max(0, root.selectedIndex * (root.sliceWidth + root.sliceSpacing)) + } + + if (immediate) { + center() + Qt.callLater(function() { + center() + root.animateSelection = true + }) + } else { + Qt.callLater(center) + } } Keys.priority: Keys.BeforeItem @@ -311,7 +321,10 @@ ShellRoot { readonly property real bottomRight: root.skewOffset >= 0 ? width - skAbs : width readonly property real bottomLeft: root.skewOffset >= 0 ? 0 : skAbs - Behavior on width { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } } + Behavior on width { + enabled: root.animateSelection + NumberAnimation { duration: 120; easing.type: Easing.OutCubic } + } Item { id: maskShape From ca0ea7af57d733ec920770e18e760691630be2ca Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 15:52:38 -0400 Subject: [PATCH 17/27] Clamp image selector navigation --- default/quickshell/background-switcher.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/default/quickshell/background-switcher.qml b/default/quickshell/background-switcher.qml index e63b6f49..1e3ba1e3 100644 --- a/default/quickshell/background-switcher.qml +++ b/default/quickshell/background-switcher.qml @@ -56,8 +56,9 @@ ShellRoot { function select(index, immediate) { if (imageModel.count === 0) return - if (index < 0) index = imageModel.count - 1 - else if (index >= imageModel.count) index = 0 + if (index < 0) index = 0 + else if (index >= imageModel.count) index = imageModel.count - 1 + if (index === selectedIndex && immediate !== true) return animateSelection = immediate !== true selectedIndex = index From 3aaedd88b19d64362f8f9ea1f3ee9532258bbbf5 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 15:57:59 -0400 Subject: [PATCH 18/27] Remove selector width animation jitter --- default/quickshell/background-switcher.qml | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/default/quickshell/background-switcher.qml b/default/quickshell/background-switcher.qml index 1e3ba1e3..cd25486f 100644 --- a/default/quickshell/background-switcher.qml +++ b/default/quickshell/background-switcher.qml @@ -17,7 +17,6 @@ ShellRoot { property bool imagesLoaded: false property bool opened: false property bool showLabels: false - property bool animateSelection: true property string doneFile: "" property string socketPath: (Quickshell.env("XDG_RUNTIME_DIR") || ("/run/user/" + Quickshell.env("UID"))) + "/omarchy-image-selector.sock" property color accent: "#798186" @@ -60,7 +59,6 @@ ShellRoot { else if (index >= imageModel.count) index = imageModel.count - 1 if (index === selectedIndex && immediate !== true) return - animateSelection = immediate !== true selectedIndex = index list.currentIndex = index list.centerSelected(immediate === true) @@ -271,15 +269,9 @@ ShellRoot { list.contentX = Math.max(0, root.selectedIndex * (root.sliceWidth + root.sliceSpacing)) } - if (immediate) { - center() - Qt.callLater(function() { - center() - root.animateSelection = true - }) - } else { + center() + if (immediate) Qt.callLater(center) - } } Keys.priority: Keys.BeforeItem @@ -314,19 +306,12 @@ ShellRoot { height: list.height z: selected ? 100 : 50 - Math.min(Math.abs(index - root.selectedIndex), 40) - onWidthChanged: if (selected) list.centerSelected() - readonly property real skAbs: Math.abs(root.skewOffset) readonly property real topLeft: root.skewOffset >= 0 ? skAbs : 0 readonly property real topRight: root.skewOffset >= 0 ? width : width - skAbs readonly property real bottomRight: root.skewOffset >= 0 ? width - skAbs : width readonly property real bottomLeft: root.skewOffset >= 0 ? 0 : skAbs - Behavior on width { - enabled: root.animateSelection - NumberAnimation { duration: 120; easing.type: Easing.OutCubic } - } - Item { id: maskShape anchors.fill: parent From 314a7f75c0f26d10fbf6e58e54b5bd20044a3999 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 16:03:31 -0400 Subject: [PATCH 19/27] Keep image selector preview centered --- default/quickshell/background-switcher.qml | 217 ++++++++++----------- 1 file changed, 99 insertions(+), 118 deletions(-) diff --git a/default/quickshell/background-switcher.qml b/default/quickshell/background-switcher.qml index cd25486f..f588609f 100644 --- a/default/quickshell/background-switcher.qml +++ b/default/quickshell/background-switcher.qml @@ -60,8 +60,6 @@ ShellRoot { if (index === selectedIndex && immediate !== true) return selectedIndex = index - list.currentIndex = index - list.centerSelected(immediate === true) } function applySelected() { @@ -90,7 +88,7 @@ ShellRoot { root.select(root.selectedImageIndex(), true) root.imagesLoaded = true root.opened = true - list.forceActiveFocus() + carousel.forceActiveFocus() } function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextColorsFile, nextColorsRaw, nextShowLabels) { @@ -105,7 +103,6 @@ ShellRoot { loadColors(nextColorsRaw) imageModel.clear() selectedIndex = 0 - list.currentIndex = 0 imagesLoaded = false opened = false if (imageRows) { @@ -116,13 +113,7 @@ ShellRoot { } } - ListModel { - id: imageModel - onCountChanged: { - if (count > 0 && list.currentIndex < 0) - root.select(0) - } - } + ListModel { id: imageModel } function addImage(path, thumbnailPath) { if (!path) return @@ -242,37 +233,19 @@ ShellRoot { MouseArea { anchors.fill: parent; onClicked: {} } - ListView { - id: list + Item { + id: carousel anchors.top: parent.top anchors.topMargin: 30 anchors.bottom: parent.bottom anchors.bottomMargin: root.showLabels ? 74 : 30 anchors.horizontalCenter: parent.horizontalCenter width: root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing) - orientation: ListView.Horizontal - model: imageModel - spacing: root.sliceSpacing clip: false focus: true - currentIndex: 0 - preferredHighlightBegin: (width - root.expandedWidth) / 2 - preferredHighlightEnd: (width + root.expandedWidth) / 2 - highlightRangeMode: ListView.NoHighlightRange - highlightMoveDuration: 120 - highlight: Item {} - header: Item { width: (list.width - root.expandedWidth) / 2; height: 1 } - footer: Item { width: (list.width - root.expandedWidth) / 2; height: 1 } - function centerSelected(immediate) { - var center = function() { - list.contentX = Math.max(0, root.selectedIndex * (root.sliceWidth + root.sliceSpacing)) - } - - center() - if (immediate) - Qt.callLater(center) - } + readonly property real itemStep: root.sliceWidth + root.sliceSpacing + readonly property real previewX: (width - root.expandedWidth) / 2 Keys.priority: Keys.BeforeItem Keys.onPressed: function(event) { @@ -293,37 +266,64 @@ ShellRoot { Component.onCompleted: forceActiveFocus() - delegate: Item { - id: item - required property int index - required property string filePath - required property string fileName - required property string thumbnailPath + Repeater { + model: imageModel - readonly property bool selected: index === root.selectedIndex + delegate: Item { + id: item + required property int index + required property string filePath + required property string fileName + required property string thumbnailPath - width: selected ? root.expandedWidth : root.sliceWidth - height: list.height - z: selected ? 100 : 50 - Math.min(Math.abs(index - root.selectedIndex), 40) + readonly property int relativeIndex: index - root.selectedIndex + readonly property bool selected: relativeIndex === 0 + readonly property bool nearby: Math.abs(relativeIndex) <= 16 - readonly property real skAbs: Math.abs(root.skewOffset) - readonly property real topLeft: root.skewOffset >= 0 ? skAbs : 0 - readonly property real topRight: root.skewOffset >= 0 ? width : width - skAbs - readonly property real bottomRight: root.skewOffset >= 0 ? width - skAbs : width - readonly property real bottomLeft: root.skewOffset >= 0 ? 0 : skAbs + visible: nearby + x: selected ? carousel.previewX : (relativeIndex < 0 ? carousel.previewX + relativeIndex * carousel.itemStep : carousel.previewX + root.expandedWidth + root.sliceSpacing + (relativeIndex - 1) * carousel.itemStep) + width: selected ? root.expandedWidth : root.sliceWidth + height: carousel.height + z: selected ? 100 : 50 - Math.min(Math.abs(relativeIndex), 40) - Item { - id: maskShape - anchors.fill: parent - visible: false - layer.enabled: true + readonly property real skAbs: Math.abs(root.skewOffset) + readonly property real topLeft: root.skewOffset >= 0 ? skAbs : 0 + readonly property real topRight: root.skewOffset >= 0 ? width : width - skAbs + readonly property real bottomRight: root.skewOffset >= 0 ? width - skAbs : width + readonly property real bottomLeft: root.skewOffset >= 0 ? 0 : skAbs + + Item { + id: maskShape + anchors.fill: parent + visible: false + layer.enabled: true + + Shape { + anchors.fill: parent + antialiasing: true + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: "white" + strokeColor: "transparent" + startX: item.topLeft; startY: 0 + PathLine { x: item.topRight; y: 0 } + PathLine { x: item.bottomRight; y: item.height } + PathLine { x: item.bottomLeft; y: item.height } + PathLine { x: item.topLeft; y: 0 } + } + } + } Shape { - anchors.fill: parent + x: item.selected ? 4 : 2 + y: item.selected ? 10 : 5 + width: item.width + height: item.height + opacity: item.selected ? 0.5 : 0.32 antialiasing: true preferredRendererType: Shape.CurveRenderer ShapePath { - fillColor: "white" + fillColor: "#000000" strokeColor: "transparent" startX: item.topLeft; startY: 0 PathLine { x: item.topRight; y: 0 } @@ -332,84 +332,65 @@ ShellRoot { PathLine { x: item.topLeft; y: 0 } } } - } - Shape { - x: item.selected ? 4 : 2 - y: item.selected ? 10 : 5 - width: item.width - height: item.height - opacity: item.selected ? 0.5 : 0.32 - antialiasing: true - preferredRendererType: Shape.CurveRenderer - ShapePath { - fillColor: "#000000" - strokeColor: "transparent" - startX: item.topLeft; startY: 0 - PathLine { x: item.topRight; y: 0 } - PathLine { x: item.bottomRight; y: item.height } - PathLine { x: item.bottomLeft; y: item.height } - PathLine { x: item.topLeft; y: 0 } - } - } - - Item { - anchors.fill: parent - layer.enabled: true - layer.smooth: true - layer.effect: MultiEffect { - maskEnabled: true - maskSource: maskShape - maskThresholdMin: 0.3 - maskSpreadAtMin: 0.3 - } - - Image { - id: image + Item { anchors.fill: parent - source: root.fileUrl(item.thumbnailPath) - fillMode: Image.PreserveAspectCrop - asynchronous: false - cache: true - smooth: true + layer.enabled: true + layer.smooth: true + layer.effect: MultiEffect { + maskEnabled: true + maskSource: maskShape + maskThresholdMin: 0.3 + maskSpreadAtMin: 0.3 + } + + Image { + id: image + anchors.fill: parent + source: item.nearby ? root.fileUrl(item.thumbnailPath) : "" + fillMode: Image.PreserveAspectCrop + asynchronous: false + cache: true + smooth: true + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, item.selected ? 0 : 0.42) + Behavior on color { ColorAnimation { duration: 120 } } + } } - Rectangle { + Shape { anchors.fill: parent - color: Qt.rgba(0, 0, 0, item.selected ? 0 : 0.42) - Behavior on color { ColorAnimation { duration: 120 } } + antialiasing: true + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: "transparent" + strokeColor: item.selected ? root.accent : Qt.rgba(0, 0, 0, 0.65) + strokeWidth: item.selected ? 3 : 1 + startX: item.topLeft; startY: 0 + PathLine { x: item.topRight; y: 0 } + PathLine { x: item.bottomRight; y: item.height } + PathLine { x: item.bottomLeft; y: item.height } + PathLine { x: item.topLeft; y: 0 } + } } - } - Shape { - anchors.fill: parent - antialiasing: true - preferredRendererType: Shape.CurveRenderer - ShapePath { - fillColor: "transparent" - strokeColor: item.selected ? root.accent : Qt.rgba(0, 0, 0, 0.65) - strokeWidth: item.selected ? 3 : 1 - startX: item.topLeft; startY: 0 - PathLine { x: item.topRight; y: 0 } - PathLine { x: item.bottomRight; y: item.height } - PathLine { x: item.bottomLeft; y: item.height } - PathLine { x: item.topLeft; y: 0 } + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: item.selected ? root.applySelected() : root.select(index) } } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: item.selected ? root.applySelected() : root.select(index) - } } } Text { visible: root.showLabels - anchors.top: list.bottom + anchors.top: carousel.bottom anchors.topMargin: 16 - anchors.horizontalCenter: list.horizontalCenter + anchors.horizontalCenter: carousel.horizontalCenter width: root.expandedWidth text: root.currentLabel() color: root.foreground From 0801209090ced782abfbd1a5404862b88969ac35 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 16:07:37 -0400 Subject: [PATCH 20/27] Keep selector labels readable --- default/quickshell/background-switcher.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/default/quickshell/background-switcher.qml b/default/quickshell/background-switcher.qml index f588609f..1810f823 100644 --- a/default/quickshell/background-switcher.qml +++ b/default/quickshell/background-switcher.qml @@ -393,7 +393,9 @@ ShellRoot { anchors.horizontalCenter: carousel.horizontalCenter width: root.expandedWidth text: root.currentLabel() - color: root.foreground + color: "#ffffff" + style: Text.Outline + styleColor: Qt.rgba(0, 0, 0, 0.7) font.pixelSize: 24 font.weight: Font.DemiBold horizontalAlignment: Text.AlignHCenter From 903f27fdd105f964e2feb0ade81933ee296c3a8b Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 16:50:26 -0400 Subject: [PATCH 21/27] Tighten image selector request handling --- bin/omarchy-menu-images | 96 ++++++++++++++-------- bin/omarchy-theme-switcher | 30 +++++-- default/quickshell/background-switcher.qml | 62 +++++++++++--- 3 files changed, 139 insertions(+), 49 deletions(-) diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index bf0c535d..540a6313 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -12,13 +12,27 @@ show_labels=false cache_only=false image_dirs=() +usage() { + echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--show-labels] [--cache-only] ..." +} + while [[ $# -gt 0 ]]; do case "$1" in --selected) + if (( $# < 2 )); then + usage >&2 + exit 1 + fi + selected_image="$2" shift 2 ;; --colors-file) + if (( $# < 2 )); then + usage >&2 + exit 1 + fi + colors_file="$2" shift 2 ;; @@ -35,7 +49,7 @@ while [[ $# -gt 0 ]]; do shift ;; --help|-h) - echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--show-labels] [--cache-only] ..." + usage exit 0 ;; *) @@ -46,7 +60,7 @@ while [[ $# -gt 0 ]]; do done if (( ${#image_dirs[@]} == 0 )); then - echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--show-labels] [--cache-only] ..." >&2 + usage >&2 exit 1 fi @@ -90,10 +104,17 @@ cache_key=$(printf '%s' "$image_dirs_env" | md5sum | cut -d ' ' -f 1) rows_cache_file="$cache_dir/$cache_key.rows" rows_signature_file="$cache_dir/$cache_key.signature" rows_signature="" +image_files=() for dir in "${image_dirs[@]}"; do if [[ -d $dir ]]; then rows_signature+="$dir:$(stat -Lc '%Y' "$dir")"$'\n' + + while IFS= read -r -d '' image; do + image_files+=("$image") + image_signature=$(stat -Lc '%s:%Y' "$image") || continue + rows_signature+="$image:$image_signature"$'\n' + done < <(find -L "$dir" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.bmp' -o -iname '*.webp' \) -print0 2>/dev/null | sort -z) fi done @@ -112,27 +133,23 @@ thumbnail_for() { thumbnail="$cache_dir/$hash.jpg" if [[ ! -f $thumbnail ]]; then - magick "$image" -auto-orient -resize '1536x864^' -gravity center -extent '1536x864' -strip -quality 82 "$thumbnail" + magick "${image}[0]" -auto-orient -resize '1536x864^' -gravity center -extent '1536x864' -strip -quality 82 "$thumbnail" fi - printf '%s' "$thumbnail" + [[ -f $thumbnail ]] && printf '%s' "$thumbnail" } if [[ -f $rows_cache_file && -f $rows_signature_file ]] && cmp -s "$rows_signature_file" <(printf '%s' "$rows_signature"); then rows=$(<"$rows_cache_file") else - for dir in "${image_dirs[@]}"; do - if [[ -d $dir ]]; then - while IFS= read -r -d '' image; do - thumbnail=$(thumbnail_for "$image") - [[ -n $thumbnail ]] || continue + for image in "${image_files[@]}"; do + thumbnail=$(thumbnail_for "$image") + [[ -n $thumbnail ]] || continue - if [[ -z $rows ]]; then - rows="$image"$'\t'"$thumbnail" - else - rows+=$'\n'"$image"$'\t'"$thumbnail" - fi - done < <(find -L "$dir" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) -print0 2>/dev/null) + if [[ -z $rows ]]; then + rows="$image"$'\t'"$thumbnail" + else + rows+=$'\n'"$image"$'\t'"$thumbnail" fi done @@ -155,31 +172,46 @@ if [[ $cache_only == true ]]; then exit 0 fi -if [[ -S $socket_path && $selector_qml -nt $socket_path ]]; then - quickshell kill -p "$selector_qml" >/dev/null 2>&1 || true - rm -f "$socket_path" -fi +ensure_selector() { + if [[ -S $socket_path && $selector_qml -nt $socket_path ]]; then + quickshell kill -p "$selector_qml" >/dev/null 2>&1 || true + rm -f "$socket_path" + fi -if [[ ! -S $socket_path ]]; then - quickshell kill -p "$selector_qml" >/dev/null 2>&1 || true - quickshell -d -p "$selector_qml" >/dev/null + if [[ ! -S $socket_path ]]; then + quickshell kill -p "$selector_qml" >/dev/null 2>&1 || true + quickshell -d -p "$selector_qml" >/dev/null - for ((i = 0; i < 100; i++)); do - if [[ -S $socket_path ]]; then - break - fi + for ((i = 0; i < 100; i++)); do + if [[ -S $socket_path ]]; then + break + fi - sleep 0.01 - done -fi + sleep 0.01 + done + fi -if [[ ! -S $socket_path ]]; then + [[ -S $socket_path ]] +} + +send_request() { + printf '%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" | + socat -u - "UNIX-CONNECT:$socket_path" +} + +if ! ensure_selector; then echo "Image selector failed to start" >&2 exit 1 fi -printf '%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" | - socat -u - "UNIX-CONNECT:$socket_path" +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 diff --git a/bin/omarchy-theme-switcher b/bin/omarchy-theme-switcher index a21ed4aa..a053a70d 100755 --- a/bin/omarchy-theme-switcher +++ b/bin/omarchy-theme-switcher @@ -13,13 +13,19 @@ mkdir -p "$preview_dir" find_preview() { local theme_path="$1" + local preview preview_name - if [[ -f $theme_path/preview.png ]]; then - printf '%s\n' "$theme_path/preview.png" - elif [[ -f $theme_path/preview.jpg ]]; then - printf '%s\n' "$theme_path/preview.jpg" - elif [[ -d $theme_path/backgrounds ]]; then - find -L "$theme_path/backgrounds" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) -print -quit 2>/dev/null + for preview_name in preview.png preview.jpg preview.jpeg preview.webp preview.gif preview.bmp; do + preview=$(find -L "$theme_path" -maxdepth 1 -type f -iname "$preview_name" -print -quit 2>/dev/null) + + if [[ -n $preview ]]; then + printf '%s\n' "$preview" + return + fi + done + + if [[ -d $theme_path/backgrounds ]]; then + find -L "$theme_path/backgrounds" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.bmp' -o -iname '*.webp' \) -print 2>/dev/null | sort | head -n 1 fi } @@ -27,6 +33,7 @@ add_theme_preview() { local theme_name="$1" local preview="$2" local extension="${preview##*.}" + extension="${extension,,}" [[ -n $preview ]] || return [[ -e $preview_dir/$theme_name.$extension ]] && return @@ -38,6 +45,15 @@ theme_signature="" for theme_dir in "$USER_THEMES_PATH" "$OMARCHY_THEMES_PATH"; do if [[ -d $theme_dir ]]; then theme_signature+="$theme_dir:$(stat -Lc '%Y' "$theme_dir")"$'\n' + + while IFS= read -r -d '' theme_path; do + preview=$(find_preview "$theme_path") + theme_signature+="$theme_path:$(stat -Lc '%Y' "$theme_path")"$'\n' + + if [[ -n $preview ]]; then + theme_signature+="$preview:$(stat -Lc '%s:%Y' "$preview")"$'\n' + fi + done < <(find -L "$theme_dir" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -print0 2>/dev/null | sort -z) fi done @@ -67,7 +83,7 @@ fi current_theme=$(cat "$HOME/.config/omarchy/current/theme.name" 2>/dev/null) selected_preview="" -for extension in png jpg jpeg webp; do +for extension in png jpg jpeg webp gif bmp; do if [[ -e $preview_dir/$current_theme.$extension ]]; then selected_preview="$preview_dir/$current_theme.$extension" break diff --git a/default/quickshell/background-switcher.qml b/default/quickshell/background-switcher.qml index 1810f823..0552b1de 100644 --- a/default/quickshell/background-switcher.qml +++ b/default/quickshell/background-switcher.qml @@ -17,7 +17,11 @@ ShellRoot { property bool imagesLoaded: false property bool opened: false property bool showLabels: false + property bool requestActive: false + property int requestSerial: 0 + property int applySerial: 0 property string doneFile: "" + 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" @@ -62,23 +66,52 @@ ShellRoot { selectedIndex = index } + function releaseNextDoneFile() { + if (releaseProc.running || doneFilesToRelease.length === 0) return + + var path = doneFilesToRelease.shift() + releaseProc.command = ["bash", "-lc", ": > " + shellQuote(path)] + releaseProc.running = true + } + + function finishDoneFile(path) { + if (!path) return + doneFilesToRelease.push(path) + releaseNextDoneFile() + } + function applySelected() { var path = currentPath() - if (!path) return - if (!selectionFile) return - applyProc.command = ["bash", "-lc", "printf '%s\\n' " + shellQuote(path) + " > " + shellQuote(selectionFile) + "; : > " + shellQuote(doneFile)] + if (!path || !selectionFile) { + cancel() + return + } + + var activeSelectionFile = selectionFile + var activeDoneFile = doneFile + applySerial = requestSerial + requestActive = false + selectionFile = "" + doneFile = "" + + applyProc.command = ["bash", "-lc", "printf '%s\\n' " + shellQuote(path) + " > " + shellQuote(activeSelectionFile) + "; : > " + shellQuote(activeDoneFile)] applyProc.running = true } function cancel() { - cancelProc.command = ["bash", "-lc", ": > " + shellQuote(doneFile)] - cancelProc.running = true + if (requestActive) + finishDoneFile(doneFile) + + requestActive = false + selectionFile = "" + doneFile = "" + root.opened = false } function loadRows(rows) { var paths = rows.split("\n") for (var i = 0; i < paths.length; i++) { - var row = paths[i].trim() + var row = paths[i] if (!row) continue var columns = row.split("\t") @@ -92,11 +125,17 @@ ShellRoot { } function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextColorsFile, nextColorsRaw, nextShowLabels) { + if (requestActive && doneFile && doneFile !== nextDoneFile) + finishDoneFile(doneFile) + + requestSerial += 1 + imageDirs = nextImageDirs imageRows = nextImageRows selectedImage = nextSelectedImage selectionFile = nextSelectionFile doneFile = nextDoneFile + requestActive = !!doneFile showLabels = nextShowLabels === true || nextShowLabels === "true" colorsFile = nextColorsFile || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/background-switcher-colors.json") if (nextColorsRaw) @@ -139,7 +178,7 @@ ShellRoot { Process { id: loadImagesProc property string output: "" - command: ["bash", "-lc", "cache_dir=${XDG_CACHE_HOME:-$HOME/.cache}/omarchy/image-selector; while IFS= read -r dir; do [[ -n $dir && -d $dir ]] && find -L \"$dir\" -maxdepth 1 -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\) -print0; done <<< " + shellQuote(root.imageDirs) + " | sort -z | while IFS= read -r -d '' image; do hash=$(md5sum \"$image\" | cut -d ' ' -f 1); thumb=\"$cache_dir/$hash.jpg\"; [[ -f $thumb ]] || thumb=$image; printf '%s\\t%s\\n' \"$image\" \"$thumb\"; done"] + command: ["bash", "-lc", "cache_dir=${XDG_CACHE_HOME:-$HOME/.cache}/omarchy/image-selector; while IFS= read -r dir; do [[ -n $dir && -d $dir ]] && find -L \"$dir\" -maxdepth 1 -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.bmp' -o -iname '*.webp' \\) -print0; done <<< " + shellQuote(root.imageDirs) + " | sort -z | while IFS= read -r -d '' image; do hash=$(md5sum \"$image\" | cut -d ' ' -f 1); thumb=\"$cache_dir/$hash.jpg\"; [[ -f $thumb ]] || thumb=$image; printf '%s\\t%s\\n' \"$image\" \"$thumb\"; done"] stdout: SplitParser { onRead: function(data) { loadImagesProc.output += data + "\n" @@ -197,12 +236,15 @@ ShellRoot { Process { id: applyProc - onExited: root.opened = false + onExited: { + if (root.applySerial === root.requestSerial) + root.opened = false + } } Process { - id: cancelProc - onExited: root.opened = false + id: releaseProc + onExited: root.releaseNextDoneFile() } PanelWindow { From f24dd82181ffc3c752788983862ddb065b16c5b3 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 17:10:45 -0400 Subject: [PATCH 22/27] Add filter typing to theme switcher --- bin/omarchy-menu-images | 11 +- bin/omarchy-theme-switcher | 1 + default/quickshell/background-switcher.qml | 145 ++++++++++++++++++--- 3 files changed, 138 insertions(+), 19 deletions(-) diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index 540a6313..86369daf 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -1,7 +1,7 @@ #!/bin/bash # omarchy:summary=Open a generic image selector menu -# omarchy:args=[--selected ] [--colors-file ] [--print-name] [--show-labels] [--cache-only] ... +# omarchy:args=[--selected ] [--colors-file ] [--print-name] [--show-labels] [--filterable] [--cache-only] ... OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} @@ -9,11 +9,12 @@ selected_image="" colors_file="" print_name=false show_labels=false +filterable=false cache_only=false image_dirs=() usage() { - echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--show-labels] [--cache-only] ..." + echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--show-labels] [--filterable] [--cache-only] ..." } while [[ $# -gt 0 ]]; do @@ -44,6 +45,10 @@ while [[ $# -gt 0 ]]; do show_labels=true shift ;; + --filterable) + filterable=true + shift + ;; --cache-only) cache_only=true shift @@ -195,7 +200,7 @@ ensure_selector() { } send_request() { - printf '%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" | + 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" } diff --git a/bin/omarchy-theme-switcher b/bin/omarchy-theme-switcher index a053a70d..2b3209ec 100755 --- a/bin/omarchy-theme-switcher +++ b/bin/omarchy-theme-switcher @@ -92,5 +92,6 @@ done exec omarchy-menu-images \ --print-name \ --show-labels \ + --filterable \ --selected "$selected_preview" \ "$preview_dir" diff --git a/default/quickshell/background-switcher.qml b/default/quickshell/background-switcher.qml index 0552b1de..718dc6a6 100644 --- a/default/quickshell/background-switcher.qml +++ b/default/quickshell/background-switcher.qml @@ -17,10 +17,12 @@ ShellRoot { property bool imagesLoaded: false property bool opened: false property bool showLabels: false + property bool filterable: false property bool requestActive: false property int requestSerial: 0 property int applySerial: 0 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" @@ -31,6 +33,7 @@ ShellRoot { property int sliceHeight: 432 property int sliceSpacing: -30 property int skewOffset: 28 + property int bottomChromeHeight: showLabels ? (filterable ? 104 : 74) : 30 function fileUrl(path) { return "file://" + path.split("/").map(encodeURIComponent).join("/") @@ -45,27 +48,106 @@ ShellRoot { } function currentPath() { - if (imageModel.count === 0) return "" + if (imageModel.count === 0 || !itemMatches(selectedIndex)) return "" return imageModel.get(selectedIndex).filePath } + function nameForPath(path) { + return path.split("/").pop().replace(/\.[^/.]+$/, "") + } + + function labelForPath(path) { + return nameForPath(path).replace(/[-_]+/g, " ").replace(/\b\w/g, function(match) { return match.toUpperCase() }) + } + function currentLabel() { var path = currentPath() - if (!path) return "" + if (!path) return filterText ? "No matches" : "" - var name = path.split("/").pop().replace(/\.[^/.]+$/, "") - return name.replace(/[-_]+/g, " ").replace(/\b\w/g, function(match) { return match.toUpperCase() }) + return labelForPath(path) + } + + function itemMatches(index) { + if (index < 0 || index >= imageModel.count) return false + if (!filterText) return true + + var path = imageModel.get(index).filePath + var needle = filterText.toLowerCase() + return nameForPath(path).toLowerCase().indexOf(needle) !== -1 || labelForPath(path).toLowerCase().indexOf(needle) !== -1 + } + + function matchingCount() { + if (!filterText) return imageModel.count + + var count = 0 + for (var i = 0; i < imageModel.count; i++) { + if (itemMatches(i)) count++ + } + + return count + } + + function firstMatchingIndex() { + for (var i = 0; i < imageModel.count; i++) { + if (itemMatches(i)) return i + } + + return -1 + } + + function filteredPosition(index) { + if (!filterText) return index + + var position = 0 + for (var i = 0; i < index; i++) { + if (itemMatches(i)) position++ + } + + return position + } + + function selectedFilteredPosition() { + if (!filterText) return selectedIndex + + return itemMatches(selectedIndex) ? filteredPosition(selectedIndex) : 0 } function select(index, immediate) { if (imageModel.count === 0) return if (index < 0) index = 0 else if (index >= imageModel.count) index = imageModel.count - 1 + if (!itemMatches(index)) return if (index === selectedIndex && immediate !== true) return selectedIndex = index } + function selectAdjacent(direction) { + if (!filterText) { + select(selectedIndex + direction) + return + } + + var index = selectedIndex + direction + while (index >= 0 && index < imageModel.count) { + if (itemMatches(index)) { + select(index) + return + } + + index += direction + } + } + + function updateFilter(nextFilterText) { + filterText = nextFilterText + + if (!itemMatches(selectedIndex)) { + var first = firstMatchingIndex() + if (first >= 0) selectedIndex = first + } + } + function releaseNextDoneFile() { if (releaseProc.running || doneFilesToRelease.length === 0) return @@ -124,7 +206,7 @@ ShellRoot { carousel.forceActiveFocus() } - function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextColorsFile, nextColorsRaw, nextShowLabels) { + function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextColorsFile, nextColorsRaw, nextShowLabels, nextFilterable) { if (requestActive && doneFile && doneFile !== nextDoneFile) finishDoneFile(doneFile) @@ -137,6 +219,8 @@ ShellRoot { doneFile = nextDoneFile requestActive = !!doneFile showLabels = nextShowLabels === true || nextShowLabels === "true" + filterable = nextFilterable === true || nextFilterable === "true" + filterText = "" colorsFile = nextColorsFile || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/background-switcher-colors.json") if (nextColorsRaw) loadColors(nextColorsRaw) @@ -191,14 +275,14 @@ ShellRoot { Component.onCompleted: { if (selectionFile) - openSelector(imageDirs, "", selectedImage, selectionFile, Quickshell.env("OMARCHY_IMAGE_SELECTOR_DONE_FILE"), colorsFile, "", false) + openSelector(imageDirs, "", selectedImage, selectionFile, Quickshell.env("OMARCHY_IMAGE_SELECTOR_DONE_FILE"), colorsFile, "", false, 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) + root.openSelector(imageDirs, imageRows, selectedImage, selectionFile, doneFile, colorsFile, "", false, false) } } @@ -211,7 +295,7 @@ ShellRoot { parser: SplitParser { onRead: function(message) { var fields = message.split("\t") - root.openSelector("", root.decodeField(fields[0]), fields[1] || "", fields[2] || "", fields[3] || "", "", root.decodeField(fields[4]), fields[5] || "false") + 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 } } @@ -270,7 +354,7 @@ ShellRoot { Item { id: card width: Math.min(parent.width - 80, root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing) + 40) - height: root.sliceHeight + (root.showLabels ? 104 : 60) + height: root.sliceHeight + 30 + root.bottomChromeHeight anchors.centerIn: parent MouseArea { anchors.fill: parent; onClicked: {} } @@ -280,7 +364,7 @@ ShellRoot { anchors.top: parent.top anchors.topMargin: 30 anchors.bottom: parent.bottom - anchors.bottomMargin: root.showLabels ? 74 : 30 + anchors.bottomMargin: root.bottomChromeHeight anchors.horizontalCenter: parent.horizontalCenter width: root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing) clip: false @@ -292,16 +376,27 @@ ShellRoot { Keys.priority: Keys.BeforeItem Keys.onPressed: function(event) { if (event.key === Qt.Key_Escape) { - root.cancel() + if (root.filterText) { + root.updateFilter("") + } else { + root.cancel() + } event.accepted = true } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { root.applySelected() event.accepted = true + } else if (event.key === Qt.Key_Backspace && root.filterable) { + if (root.filterText.length > 0) + root.updateFilter(root.filterText.slice(0, -1)) + event.accepted = true } else if (event.key === Qt.Key_Left) { - root.select(root.selectedIndex - 1) + root.selectAdjacent(-1) event.accepted = true } else if (event.key === Qt.Key_Right) { - root.select(root.selectedIndex + 1) + root.selectAdjacent(1) + event.accepted = true + } else if (root.filterable && event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127 && (event.modifiers === Qt.NoModifier || event.modifiers === Qt.ShiftModifier)) { + root.updateFilter(root.filterText + event.text) event.accepted = true } } @@ -318,9 +413,10 @@ ShellRoot { required property string fileName required property string thumbnailPath - readonly property int relativeIndex: index - root.selectedIndex - readonly property bool selected: relativeIndex === 0 - readonly property bool nearby: Math.abs(relativeIndex) <= 16 + readonly property bool matched: root.itemMatches(index) + readonly property int relativeIndex: root.filteredPosition(index) - root.selectedFilteredPosition() + readonly property bool selected: matched && index === root.selectedIndex + readonly property bool nearby: matched && Math.abs(relativeIndex) <= 16 visible: nearby x: selected ? carousel.previewX : (relativeIndex < 0 ? carousel.previewX + relativeIndex * carousel.itemStep : carousel.previewX + root.expandedWidth + root.sliceSpacing + (relativeIndex - 1) * carousel.itemStep) @@ -429,6 +525,7 @@ ShellRoot { } Text { + id: selectedLabel visible: root.showLabels anchors.top: carousel.bottom anchors.topMargin: 16 @@ -443,6 +540,22 @@ ShellRoot { horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight } + + Text { + visible: root.filterable + anchors.top: selectedLabel.bottom + anchors.topMargin: 8 + anchors.horizontalCenter: carousel.horizontalCenter + width: root.expandedWidth + text: root.filterText ? ("Filter: " + root.filterText + " (" + root.matchingCount() + ")") : "Type to filter" + color: "#ffffff" + opacity: root.filterText ? 0.85 : 0.55 + style: Text.Outline + styleColor: Qt.rgba(0, 0, 0, 0.7) + font.pixelSize: 14 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } } } } From 427783c41faa25bcc49ac9c330572bd9c9f10b51 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 17:20:46 -0400 Subject: [PATCH 23/27] Stub theme switcher benchmark --- bin/omarchy-dev-benchmark-theme-switcher | 145 +++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100755 bin/omarchy-dev-benchmark-theme-switcher diff --git a/bin/omarchy-dev-benchmark-theme-switcher b/bin/omarchy-dev-benchmark-theme-switcher new file mode 100755 index 00000000..b814025f --- /dev/null +++ b/bin/omarchy-dev-benchmark-theme-switcher @@ -0,0 +1,145 @@ +#!/bin/bash + +# omarchy:summary=Measure theme switcher cache and selector prep times +# omarchy:args=[--repeat=] [--keep-cache] +# omarchy:examples=omarchy dev benchmark theme switcher | omarchy dev benchmark theme-switcher --repeat=10 +# omarchy:aliases=omarchy dev benchmark theme-switcher + +set -euo pipefail + +OMARCHY_BIN_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +OMARCHY_PATH=${OMARCHY_PATH:-$(cd -- "$OMARCHY_BIN_DIR/.." && pwd)} +REPEAT=5 +KEEP_CACHE=false + +show_help() { + cat <<'EOF' +Usage: + omarchy dev benchmark theme switcher [--repeat=] [--keep-cache] + +Measure the non-interactive parts of the theme switcher: +- theme preview index build (omarchy-theme-switcher before UI handoff) +- image selector row/thumbnail cache prep (omarchy-menu-images --cache-only) + +Options: + --repeat= Number of warm runs to measure for each case (default: 5) + --keep-cache Keep the temporary benchmark cache and print its path +EOF +} + +now_us() { + local now="${EPOCHREALTIME/./}" + printf '%s' "$now" +} + +format_ms() { + local us="$1" + printf '%d.%03d' "$(( us / 1000 ))" "$(( us % 1000 ))" +} + +measure_once() { + local start_us end_us + start_us=$(now_us) + "$@" >/dev/null + end_us=$(now_us) + printf '%s' "$(( end_us - start_us ))" +} + +run_case() { + local label="$1" + shift + local total_us=0 + local min_us=0 + local max_us=0 + local elapsed_us=0 + + for (( i = 1; i <= REPEAT; i++ )); do + elapsed_us=$(measure_once "$@") + total_us=$(( total_us + elapsed_us )) + + if (( i == 1 || elapsed_us < min_us )); then + min_us=$elapsed_us + fi + + if (( elapsed_us > max_us )); then + max_us=$elapsed_us + fi + done + + printf '%-34s avg %8s ms min %8s ms max %8s ms\n' \ + "$label" \ + "$(format_ms "$(( total_us / REPEAT ))")" \ + "$(format_ms "$min_us")" \ + "$(format_ms "$max_us")" +} + +while (( $# > 0 )); do + case "$1" in + --repeat=*) + REPEAT="${1#*=}" + ;; + --keep-cache) + KEEP_CACHE=true + ;; + --help | -h) + show_help + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + show_help >&2 + exit 2 + ;; + esac + shift +done + +if [[ ! $REPEAT =~ ^[0-9]+$ ]] || (( REPEAT < 1 )); then + echo "--repeat must be a positive integer" >&2 + exit 2 +fi + +benchmark_cache=$(mktemp -d) +stub_bin=$(mktemp -d) + +cleanup() { + rm -rf "$stub_bin" + + if [[ $KEEP_CACHE == "true" ]]; then + printf 'Benchmark cache: %s\n' "$benchmark_cache" + else + rm -rf "$benchmark_cache" + fi +} +trap cleanup EXIT + +cat >"$stub_bin/omarchy-menu-images" <<'EOF' +#!/bin/bash +exit 0 +EOF +chmod +x "$stub_bin/omarchy-menu-images" + +benchmark_env=( + env + "OMARCHY_PATH=$OMARCHY_PATH" + "XDG_CACHE_HOME=$benchmark_cache" + "PATH=$stub_bin:$OMARCHY_BIN_DIR:$PATH" +) + +preview_dir="$benchmark_cache/omarchy/theme-selector/previews" + +build_theme_index() { + "${benchmark_env[@]}" "$OMARCHY_BIN_DIR/omarchy-theme-switcher" +} + +prepare_image_cache() { + "${benchmark_env[@]}" "$OMARCHY_BIN_DIR/omarchy-menu-images" --cache-only "$preview_dir" +} + +printf 'Theme switcher benchmark (%d warm runs each)\n\n' "$REPEAT" +printf '%-34s %s ms\n' "theme index cold" "$(format_ms "$(measure_once build_theme_index)")" +run_case "theme index warm" build_theme_index +printf '%-34s %s ms\n' "selector cache cold" "$(format_ms "$(measure_once prepare_image_cache)")" +run_case "selector cache warm" prepare_image_cache + +printf '\nTheme previews: %d\n' "$(find -L "$preview_dir" -maxdepth 1 -type f 2>/dev/null | wc -l)" From 3d8c061a396e9680c0069f1a347ad257d973ba43 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 17:41:30 -0400 Subject: [PATCH 24/27] Lazy load theme switcher thumbnails --- bin/omarchy-dev-benchmark-theme-switcher | 28 ++++++++-- bin/omarchy-menu-images | 68 +++++++++++++++++++++--- bin/omarchy-theme-switcher | 1 + 3 files changed, 84 insertions(+), 13 deletions(-) diff --git a/bin/omarchy-dev-benchmark-theme-switcher b/bin/omarchy-dev-benchmark-theme-switcher index b814025f..97460bed 100755 --- a/bin/omarchy-dev-benchmark-theme-switcher +++ b/bin/omarchy-dev-benchmark-theme-switcher @@ -19,7 +19,8 @@ Usage: Measure the non-interactive parts of the theme switcher: - theme preview index build (omarchy-theme-switcher before UI handoff) -- image selector row/thumbnail cache prep (omarchy-menu-images --cache-only) +- lazy selector row prep used by the interactive theme switcher +- full thumbnail cache warmup cost (omarchy-menu-images --cache-only) Options: --repeat= Number of warm runs to measure for each case (default: 5) @@ -100,6 +101,7 @@ if [[ ! $REPEAT =~ ^[0-9]+$ ]] || (( REPEAT < 1 )); then fi benchmark_cache=$(mktemp -d) +thumbnail_cache=$(mktemp -d) stub_bin=$(mktemp -d) cleanup() { @@ -107,8 +109,9 @@ cleanup() { if [[ $KEEP_CACHE == "true" ]]; then printf 'Benchmark cache: %s\n' "$benchmark_cache" + printf 'Thumbnail cache: %s\n' "$thumbnail_cache" else - rm -rf "$benchmark_cache" + rm -rf "$benchmark_cache" "$thumbnail_cache" fi } trap cleanup EXIT @@ -126,20 +129,35 @@ benchmark_env=( "PATH=$stub_bin:$OMARCHY_BIN_DIR:$PATH" ) +thumbnail_env=( + env + "OMARCHY_PATH=$OMARCHY_PATH" + "XDG_CACHE_HOME=$thumbnail_cache" + "PATH=$stub_bin:$OMARCHY_BIN_DIR:$PATH" +) + preview_dir="$benchmark_cache/omarchy/theme-selector/previews" +thumbnail_preview_dir="$thumbnail_cache/omarchy/theme-selector/previews" build_theme_index() { "${benchmark_env[@]}" "$OMARCHY_BIN_DIR/omarchy-theme-switcher" } +prepare_selector_lazy() { + "${benchmark_env[@]}" "$OMARCHY_BIN_DIR/omarchy-menu-images" --prepare-only --lazy-thumbnails --show-labels --filterable "$preview_dir" +} + prepare_image_cache() { - "${benchmark_env[@]}" "$OMARCHY_BIN_DIR/omarchy-menu-images" --cache-only "$preview_dir" + "${thumbnail_env[@]}" "$OMARCHY_BIN_DIR/omarchy-menu-images" --cache-only "$thumbnail_preview_dir" } printf 'Theme switcher benchmark (%d warm runs each)\n\n' "$REPEAT" printf '%-34s %s ms\n' "theme index cold" "$(format_ms "$(measure_once build_theme_index)")" run_case "theme index warm" build_theme_index -printf '%-34s %s ms\n' "selector cache cold" "$(format_ms "$(measure_once prepare_image_cache)")" -run_case "selector cache warm" prepare_image_cache +printf '%-34s %s ms\n' "selector prep cold (lazy)" "$(format_ms "$(measure_once prepare_selector_lazy)")" +run_case "selector prep warm (lazy)" prepare_selector_lazy +"${thumbnail_env[@]}" "$OMARCHY_BIN_DIR/omarchy-theme-switcher" >/dev/null +printf '%-34s %s ms\n' "thumbnail cache cold" "$(format_ms "$(measure_once prepare_image_cache)")" +run_case "thumbnail cache warm" prepare_image_cache printf '\nTheme previews: %d\n' "$(find -L "$preview_dir" -maxdepth 1 -type f 2>/dev/null | wc -l)" diff --git a/bin/omarchy-menu-images b/bin/omarchy-menu-images index 86369daf..970f9392 100755 --- a/bin/omarchy-menu-images +++ b/bin/omarchy-menu-images @@ -1,7 +1,7 @@ #!/bin/bash # omarchy:summary=Open a generic image selector menu -# omarchy:args=[--selected ] [--colors-file ] [--print-name] [--show-labels] [--filterable] [--cache-only] ... +# omarchy:args=[--selected ] [--colors-file ] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--cache-only] ... OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} @@ -10,11 +10,13 @@ colors_file="" print_name=false show_labels=false filterable=false +lazy_thumbnails=false +prepare_only=false cache_only=false image_dirs=() usage() { - echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--show-labels] [--filterable] [--cache-only] ..." + echo "Usage: omarchy-menu-images [--selected ] [--colors-file ] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--cache-only] ..." } while [[ $# -gt 0 ]]; do @@ -49,6 +51,14 @@ while [[ $# -gt 0 ]]; do filterable=true shift ;; + --lazy-thumbnails) + lazy_thumbnails=true + shift + ;; + --prepare-only) + prepare_only=true + shift + ;; --cache-only) cache_only=true shift @@ -108,7 +118,8 @@ mkdir -p "$cache_dir" cache_key=$(printf '%s' "$image_dirs_env" | md5sum | cut -d ' ' -f 1) rows_cache_file="$cache_dir/$cache_key.rows" rows_signature_file="$cache_dir/$cache_key.signature" -rows_signature="" +rows_signature="v2"$'\n' +rows_cacheable=true image_files=() for dir in "${image_dirs[@]}"; do @@ -123,6 +134,29 @@ for dir in "${image_dirs[@]}"; do fi done +generate_thumbnail() { + local image="$1" + local thumbnail="$2" + local lock="$thumbnail.lock" + local tmp="$thumbnail.$$.jpg" + + if mkdir "$lock" 2>/dev/null; then + if magick "${image}[0]" -auto-orient -resize '1536x864^' -gravity center -extent '1536x864' -strip -quality 82 "$tmp"; then + mv -f "$tmp" "$thumbnail" + else + rm -f "$tmp" "$thumbnail" + fi + + rmdir "$lock" 2>/dev/null || true + else + for ((i = 0; i < 3000; i++)); do + [[ -f $thumbnail ]] && return + [[ -d $lock ]] || break + sleep 0.01 + done + fi +} + thumbnail_for() { local image="$1" local signature hash thumbnail @@ -131,14 +165,25 @@ thumbnail_for() { hash=$(awk -F '\t' -v path="$image" -v sig="$signature" '$1 == path && $2 == sig { print $3; exit }' "$index_file" 2>/dev/null) if [[ -z $hash ]]; then - hash=$(md5sum "$image" | cut -d ' ' -f 1) + hash=$(printf '%s\t%s' "$image" "$signature" | md5sum | cut -d ' ' -f 1) printf '%s\t%s\t%s\n' "$image" "$signature" "$hash" >>"$index_file" fi thumbnail="$cache_dir/$hash.jpg" if [[ ! -f $thumbnail ]]; then - magick "${image}[0]" -auto-orient -resize '1536x864^' -gravity center -extent '1536x864' -strip -quality 82 "$thumbnail" + if [[ $lazy_thumbnails == true && $cache_only != true ]]; then + rows_cacheable=false + + if [[ $prepare_only != true ]]; then + generate_thumbnail "$image" "$thumbnail" >/dev/null 2>&1 & + fi + + printf '%s' "$image" + return + fi + + generate_thumbnail "$image" "$thumbnail" fi [[ -f $thumbnail ]] && printf '%s' "$thumbnail" @@ -150,6 +195,9 @@ else for image in "${image_files[@]}"; do thumbnail=$(thumbnail_for "$image") [[ -n $thumbnail ]] || continue + if [[ $lazy_thumbnails == true && $cache_only != true && $thumbnail == "$image" ]]; then + rows_cacheable=false + fi if [[ -z $rows ]]; then rows="$image"$'\t'"$thumbnail" @@ -158,8 +206,12 @@ else fi done - printf '%s' "$rows" >"$rows_cache_file" - printf '%s' "$rows_signature" >"$rows_signature_file" + if [[ $rows_cacheable == true ]]; then + printf '%s' "$rows" >"$rows_cache_file" + printf '%s' "$rows_signature" >"$rows_signature_file" + else + rm -f "$rows_cache_file" "$rows_signature_file" + fi fi rows_payload=${rows//$'\t'/$'\f'} @@ -173,7 +225,7 @@ if [[ -f $colors_file ]]; then colors_payload=${colors_payload//$'\n'/$'\v'} fi -if [[ $cache_only == true ]]; then +if [[ $cache_only == true || $prepare_only == true ]]; then exit 0 fi diff --git a/bin/omarchy-theme-switcher b/bin/omarchy-theme-switcher index 2b3209ec..4a1e55e5 100755 --- a/bin/omarchy-theme-switcher +++ b/bin/omarchy-theme-switcher @@ -93,5 +93,6 @@ exec omarchy-menu-images \ --print-name \ --show-labels \ --filterable \ + --lazy-thumbnails \ --selected "$selected_preview" \ "$preview_dir" From cb87adfc99d01e1599ed2ed16b3d92cfdee70909 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 17:53:40 -0400 Subject: [PATCH 25/27] Toggle image selector closed when already open --- default/quickshell/background-switcher.qml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/default/quickshell/background-switcher.qml b/default/quickshell/background-switcher.qml index 718dc6a6..adc043b0 100644 --- a/default/quickshell/background-switcher.qml +++ b/default/quickshell/background-switcher.qml @@ -190,6 +190,22 @@ ShellRoot { root.opened = false } + function closeSelector(nextDoneFile) { + requestSerial += 1 + + if (requestActive) + finishDoneFile(doneFile) + + if (nextDoneFile && nextDoneFile !== doneFile) + finishDoneFile(nextDoneFile) + + requestActive = false + selectionFile = "" + doneFile = "" + filterText = "" + root.opened = false + } + function loadRows(rows) { var paths = rows.split("\n") for (var i = 0; i < paths.length; i++) { @@ -295,6 +311,12 @@ ShellRoot { parser: SplitParser { onRead: function(message) { var fields = message.split("\t") + if (root.opened) { + root.closeSelector(fields[3] || "") + clientSocket.connected = false + return + } + 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 } From f14d82b2f3301296e867e522ebe515dc85b687f6 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 18:02:45 -0400 Subject: [PATCH 26/27] Start image selector on demand --- default/hypr/autostart.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/default/hypr/autostart.lua b/default/hypr/autostart.lua index 6d2812c9..7eed5fbb 100644 --- a/default/hypr/autostart.lua +++ b/default/hypr/autostart.lua @@ -4,7 +4,6 @@ hl.on("hyprland.start", function() hl.exec_cmd("! omarchy-toggle-enabled waybar-off && uwsm-app -- waybar") hl.exec_cmd("uwsm-app -- fcitx5 --disable notificationitem") hl.exec_cmd("uwsm-app -- swaybg -i ~/.config/omarchy/current/background -m fill") - hl.exec_cmd("uwsm-app -- quickshell -p ~/.local/share/omarchy/default/quickshell/background-switcher.qml") hl.exec_cmd("/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1") hl.exec_cmd("omarchy-first-run") hl.exec_cmd("omarchy-powerprofiles-init") From d54e8b542915fc34a28cb5c30f1fa284ff7463b5 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 11 May 2026 18:10:58 -0400 Subject: [PATCH 27/27] Use theme colors in image selector --- default/quickshell/background-switcher.qml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/default/quickshell/background-switcher.qml b/default/quickshell/background-switcher.qml index adc043b0..65e6915b 100644 --- a/default/quickshell/background-switcher.qml +++ b/default/quickshell/background-switcher.qml @@ -47,6 +47,10 @@ ShellRoot { return String(value || "").replace(/\v/g, "\n").replace(/\f/g, "\t") } + function withAlpha(color, alpha) { + return Qt.rgba(color.r, color.g, color.b, alpha) + } + function currentPath() { if (imageModel.count === 0 || !itemMatches(selectedIndex)) return "" return imageModel.get(selectedIndex).filePath @@ -365,7 +369,7 @@ ShellRoot { Rectangle { anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.55) + color: root.withAlpha(root.background, 0.72) } MouseArea { @@ -483,7 +487,7 @@ ShellRoot { antialiasing: true preferredRendererType: Shape.CurveRenderer ShapePath { - fillColor: "#000000" + fillColor: root.background strokeColor: "transparent" startX: item.topLeft; startY: 0 PathLine { x: item.topRight; y: 0 } @@ -516,7 +520,7 @@ ShellRoot { Rectangle { anchors.fill: parent - color: Qt.rgba(0, 0, 0, item.selected ? 0 : 0.42) + color: root.withAlpha(root.background, item.selected ? 0 : 0.42) Behavior on color { ColorAnimation { duration: 120 } } } } @@ -527,7 +531,7 @@ ShellRoot { preferredRendererType: Shape.CurveRenderer ShapePath { fillColor: "transparent" - strokeColor: item.selected ? root.accent : Qt.rgba(0, 0, 0, 0.65) + strokeColor: item.selected ? root.accent : root.withAlpha(root.foreground, 0.28) strokeWidth: item.selected ? 3 : 1 startX: item.topLeft; startY: 0 PathLine { x: item.topRight; y: 0 } @@ -554,9 +558,9 @@ ShellRoot { anchors.horizontalCenter: carousel.horizontalCenter width: root.expandedWidth text: root.currentLabel() - color: "#ffffff" + color: root.foreground style: Text.Outline - styleColor: Qt.rgba(0, 0, 0, 0.7) + styleColor: root.withAlpha(root.background, 0.7) font.pixelSize: 24 font.weight: Font.DemiBold horizontalAlignment: Text.AlignHCenter @@ -570,10 +574,10 @@ ShellRoot { anchors.horizontalCenter: carousel.horizontalCenter width: root.expandedWidth text: root.filterText ? ("Filter: " + root.filterText + " (" + root.matchingCount() + ")") : "Type to filter" - color: "#ffffff" + color: root.foreground opacity: root.filterText ? 0.85 : 0.55 style: Text.Outline - styleColor: Qt.rgba(0, 0, 0, 0.7) + styleColor: root.withAlpha(root.background, 0.7) font.pixelSize: 14 horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight