Extract generic switcher

This commit is contained in:
David Heinemeier Hansson
2026-05-11 15:28:52 -04:00
committed by Ryan Hughes
parent 1e646be7ee
commit 008f0cf7dd
4 changed files with 162 additions and 42 deletions
+94
View File
@@ -0,0 +1,94 @@
#!/bin/bash
# omarchy:summary=Open a generic image selector menu
# omarchy:group=menu
# omarchy:name=images
# omarchy:args=[--selected <image>] [--namespace <namespace>] [--colors-file <path>] <image-dir>...
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 <image>] [--namespace <namespace>] [--colors-file <path>] <image-dir>..."
exit 0
;;
*)
image_dirs+=("$1")
shift
;;
esac
done
if (( ${#image_dirs[@]} == 0 )); then
echo "Usage: omarchy-menu-images [--selected <image>] [--namespace <namespace>] [--colors-file <path>] <image-dir>..." >&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
+6 -10
View File
@@ -6,15 +6,11 @@
# omarchy:aliases=omarchy wallpaper # omarchy:aliases=omarchy wallpaper
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy} 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) 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-menu-images \
OMARCHY_STOCK_BACKGROUNDS_DIR="$HOME/.config/omarchy/current/theme/backgrounds" \ --selected "$current_background" \
OMARCHY_USER_BACKGROUNDS_DIR="$HOME/.config/omarchy/backgrounds/$theme_name" \ --namespace "omarchy-wallpaper-switcher" \
quickshell -p "$OMARCHY_PATH/default/quickshell/wallpaper-switcher.qml" >/dev/null "$HOME/.config/omarchy/current/theme/backgrounds" \
"$HOME/.config/omarchy/backgrounds/$theme_name"
if [[ -s $selection_file ]]; then
cat "$selection_file"
fi
+3
View File
@@ -21,3 +21,6 @@ hl.window_rule({ match = { tag = "pop" }, rounding = 8 })
-- Prevent idle while open. -- Prevent idle while open.
hl.window_rule({ match = { tag = "noidle" }, idle_inhibit = "always" }) hl.window_rule({ match = { tag = "noidle" }, idle_inhibit = "always" })
-- Image selector.
hl.layer_rule({ match = { namespace = "omarchy-image-selector" }, no_anim = true })
+59 -32
View File
@@ -8,11 +8,13 @@ import QtQuick.Shapes
ShellRoot { ShellRoot {
id: root id: root
property string stockBackgroundsDir: Quickshell.env("OMARCHY_STOCK_BACKGROUNDS_DIR") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/backgrounds") 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 userBackgroundsDir: Quickshell.env("OMARCHY_USER_BACKGROUNDS_DIR") property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_WALLPAPER_SELECTION_FILE")
property string currentBackground: Quickshell.env("HOME") + "/.config/omarchy/current/background" property string selectedImage: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTED")
property string selectionFile: Quickshell.env("OMARCHY_WALLPAPER_SELECTION_FILE") 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 int selectedIndex: 0
property bool imagesLoaded: false
property color accent: "#798186" property color accent: "#798186"
property color background: "#101315" property color background: "#101315"
property color foreground: "#cacccc" property color foreground: "#cacccc"
@@ -31,18 +33,18 @@ ShellRoot {
} }
function currentPath() { function currentPath() {
if (wallpaperModel.count === 0) return "" if (imageModel.count === 0) return ""
return wallpaperModel.get(selectedIndex).filePath return imageModel.get(selectedIndex).filePath
} }
function select(index) { function select(index) {
if (wallpaperModel.count === 0) return if (imageModel.count === 0) return
if (index < 0) index = wallpaperModel.count - 1 if (index < 0) index = imageModel.count - 1
else if (index >= wallpaperModel.count) index = 0 else if (index >= imageModel.count) index = 0
selectedIndex = index selectedIndex = index
list.currentIndex = index list.currentIndex = index
list.positionViewAtIndex(index, ListView.Center) list.centerSelected()
} }
function applySelected() { function applySelected() {
@@ -53,46 +55,61 @@ ShellRoot {
} }
ListModel { ListModel {
id: wallpaperModel id: imageModel
onCountChanged: { onCountChanged: {
if (count > 0 && list.currentIndex < 0) if (count > 0 && list.currentIndex < 0)
root.select(0) root.select(0)
} }
} }
function addBackground(path) { function addImage(path, thumbnailPath) {
if (!path) return if (!path) return
var fileName = path.split("/").pop() var fileName = path.split("/").pop()
for (var i = wallpaperModel.count - 1; i >= 0; i--) { for (var i = imageModel.count - 1; i >= 0; i--) {
if (wallpaperModel.get(i).fileName === fileName) if (imageModel.get(i).fileName === fileName)
wallpaperModel.remove(i) 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 { Process {
id: loadBackgroundsProc id: loadImagesProc
property string output: "" 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 { stdout: SplitParser {
onRead: function(data) { onRead: function(data) {
loadBackgroundsProc.output += data + "\n" loadImagesProc.output += data + "\n"
} }
} }
onExited: { onExited: {
var paths = output.split("\n") var paths = output.split("\n")
for (var i = 0; i < paths.length; i++) for (var i = 0; i < paths.length; i++) {
root.addBackground(paths[i].trim()) var row = paths[i].trim()
root.select(0) 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 { FileView {
path: Quickshell.env("HOME") + "/.config/omarchy/current/theme/wallpaper-switcher-colors.json" path: root.colorsFile
watchChanges: true watchChanges: true
onLoaded: root.loadColors(text()) onLoaded: root.loadColors(text())
onFileChanged: { reload(); root.loadColors(text()) } onFileChanged: { reload(); root.loadColors(text()) }
@@ -114,9 +131,10 @@ ShellRoot {
PanelWindow { PanelWindow {
id: panel id: panel
visible: root.imagesLoaded
anchors { top: true; bottom: true; left: true; right: true } anchors { top: true; bottom: true; left: true; right: true }
color: "transparent" color: "transparent"
WlrLayershell.namespace: "omarchy-wallpaper-switcher" WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
@@ -148,19 +166,27 @@ ShellRoot {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
width: root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing) width: root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing)
orientation: ListView.Horizontal orientation: ListView.Horizontal
model: wallpaperModel model: imageModel
spacing: root.sliceSpacing spacing: root.sliceSpacing
clip: false clip: false
focus: true focus: true
currentIndex: 0 currentIndex: 0
preferredHighlightBegin: (width - root.expandedWidth) / 2 preferredHighlightBegin: (width - root.expandedWidth) / 2
preferredHighlightEnd: (width + root.expandedWidth) / 2 preferredHighlightEnd: (width + root.expandedWidth) / 2
highlightRangeMode: ListView.StrictlyEnforceRange highlightRangeMode: ListView.NoHighlightRange
highlightMoveDuration: 120 highlightMoveDuration: 120
highlight: Item {} highlight: Item {}
header: Item { width: (list.width - root.expandedWidth) / 2; height: 1 } header: Item { width: (list.width - root.expandedWidth) / 2; height: 1 }
footer: 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.priority: Keys.BeforeItem
Keys.onPressed: function(event) { Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
@@ -185,6 +211,7 @@ ShellRoot {
required property int index required property int index
required property string filePath required property string filePath
required property string fileName required property string fileName
required property string thumbnailPath
readonly property bool selected: index === root.selectedIndex readonly property bool selected: index === root.selectedIndex
@@ -192,6 +219,8 @@ ShellRoot {
height: list.height height: list.height
z: selected ? 100 : 50 - Math.min(Math.abs(index - root.selectedIndex), 40) 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 skAbs: Math.abs(root.skewOffset)
readonly property real topLeft: root.skewOffset >= 0 ? skAbs : 0 readonly property real topLeft: root.skewOffset >= 0 ? skAbs : 0
readonly property real topRight: root.skewOffset >= 0 ? width : width - skAbs readonly property real topRight: root.skewOffset >= 0 ? width : width - skAbs
@@ -255,13 +284,11 @@ ShellRoot {
Image { Image {
id: image id: image
anchors.fill: parent anchors.fill: parent
source: root.fileUrl(item.filePath) source: root.fileUrl(item.thumbnailPath)
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
asynchronous: true asynchronous: false
cache: true cache: true
smooth: true smooth: true
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 120 } }
} }
Rectangle { Rectangle {