Performance improve pickers

This commit is contained in:
David Heinemeier Hansson
2026-05-17 11:40:05 +02:00
parent b9670a1ec7
commit 523803e99e
3 changed files with 160 additions and 66 deletions
+77 -13
View File
@@ -106,22 +106,34 @@ 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_fast_signature_file="$cache_dir/$cache_key.fast-signature"
rows_signature="v2"$'\n'
rows_fast_signature="v1"$'\n'
rows_cacheable=true
rows_cache_hit=false
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
[[ -d $dir ]] && rows_fast_signature+="$dir:$(stat -Lc '%Y' "$dir")"$'\n'
done
if [[ -f $rows_cache_file && -f $rows_fast_signature_file ]] && cmp -s "$rows_fast_signature_file" <(printf '%s' "$rows_fast_signature"); then
rows=$(<"$rows_cache_file")
rows_cache_hit=true
else
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
fi
generate_thumbnail() {
local image="$1"
local thumbnail="$2"
@@ -177,9 +189,10 @@ thumbnail_for() {
[[ -f $thumbnail ]] && printf '%s' "$thumbnail"
}
if [[ -f $rows_cache_file && -f $rows_signature_file ]] && cmp -s "$rows_signature_file" <(printf '%s' "$rows_signature"); then
if [[ $rows_cache_hit != true && -f $rows_cache_file && -f $rows_signature_file ]] && cmp -s "$rows_signature_file" <(printf '%s' "$rows_signature"); then
rows=$(<"$rows_cache_file")
else
printf '%s' "$rows_fast_signature" >"$rows_fast_signature_file"
elif [[ $rows_cache_hit != true ]]; then
for image in "${image_files[@]}"; do
thumbnail=$(thumbnail_for "$image")
[[ -n $thumbnail ]] || continue
@@ -197,8 +210,9 @@ else
if [[ $rows_cacheable == true ]]; then
printf '%s' "$rows" >"$rows_cache_file"
printf '%s' "$rows_signature" >"$rows_signature_file"
printf '%s' "$rows_fast_signature" >"$rows_fast_signature_file"
else
rm -f "$rows_cache_file" "$rows_signature_file"
rm -f "$rows_cache_file" "$rows_signature_file" "$rows_fast_signature_file"
fi
fi
@@ -211,7 +225,57 @@ fi
# ImagePicker plugin Qt.atob()s on the other side.
rows_b64=$(printf '%s' "$rows" | base64 -w 0)
if ! omarchy-shell-ipc image-selector open \
send_builtin_ipc_request() {
perl -MCwd=abs_path -MEncode=encode,decode -MSocket \
-e '
sub read_qstring {
my ($data, $offset) = @_;
return ("", $offset) if $offset + 4 > length($data);
my $length = unpack("N", substr($data, $offset, 4));
$offset += 4;
return ("", $offset) if $length == 0xffffffff || $offset + $length > length($data);
return (decode("UTF-16BE", substr($data, $offset, $length)), $offset + $length);
}
sub qstring {
my $encoded = encode("UTF-16BE", $_[0] // "");
return pack("N", length($encoded)) . $encoded;
}
my $shell_qml = abs_path(shift @ARGV);
my @open_args = @ARGV;
my $runtime_dir = $ENV{"XDG_RUNTIME_DIR"} || "/run/user/$<";
my $payload = chr(3) . qstring("image-selector") . qstring("open") . pack("N", scalar @open_args) . join("", map { qstring($_) } @open_args);
my @candidates;
for my $lock_path (glob("$runtime_dir/quickshell/by-id/*/instance.lock")) {
my $data;
next unless open(my $lock, "<:raw", $lock_path);
{ local $/; $data = <$lock>; }
close($lock);
my (undef, $offset) = read_qstring($data, 0);
my ($path) = read_qstring($data, $offset);
next unless $path && abs_path($path) eq $shell_qml;
(my $socket_path = $lock_path) =~ s{/instance\.lock$}{/ipc.sock};
push @candidates, [(stat($lock_path))[9] || 0, $socket_path];
}
for my $candidate (sort { $b->[0] <=> $a->[0] } @candidates) {
socket(my $client, AF_UNIX, SOCK_STREAM, 0) || next;
if (connect($client, sockaddr_un($candidate->[1]))) {
syswrite($client, $payload);
close($client);
exit 0;
}
close($client);
}
exit 1;
' "$OMARCHY_PATH/default/quickshell/omarchy-shell/shell.qml" \
"" "$rows_b64" "$selected_list_image" "$selection_file" "$done_file" "$show_labels" "$filterable"
}
if ! send_builtin_ipc_request && ! omarchy-shell-ipc image-selector open \
"" \
"$rows_b64" \
"$selected_list_image" \
+24 -9
View File
@@ -8,6 +8,7 @@ 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"
fast_signature_file="$CACHE_PATH/fast-signature"
mkdir -p "$preview_dir"
@@ -41,23 +42,36 @@ add_theme_preview() {
ln -s "$preview" "$preview_dir/$theme_name.$extension"
}
theme_signature=""
fast_signature="v1"$'\n'
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'
fast_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
fast_signature+="$theme_path:$(stat -Lc '%Y' "$theme_path")"$'\n'
done < <(find -L "$theme_dir" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -print0 2>/dev/null | sort -z)
fi
done
if [[ ! -f $signature_file ]] || ! cmp -s "$signature_file" <(printf '%s' "$theme_signature"); then
if [[ ! -f $fast_signature_file ]] || ! cmp -s "$fast_signature_file" <(printf '%s' "$fast_signature"); then
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
fi
if [[ ! -f $fast_signature_file ]] || ! cmp -s "$fast_signature_file" <(printf '%s' "$fast_signature"); then
rm -rf "$preview_dir"
mkdir -p "$preview_dir"
@@ -79,6 +93,7 @@ if [[ ! -f $signature_file ]] || ! cmp -s "$signature_file" <(printf '%s' "$them
done < <(find -L "$OMARCHY_THEMES_PATH" -mindepth 1 -maxdepth 1 -type d -print 2>/dev/null | sort)
printf '%s' "$theme_signature" >"$signature_file"
printf '%s' "$fast_signature" >"$fast_signature_file"
fi
current_theme=$(cat "$HOME/.config/omarchy/current/theme.name" 2>/dev/null)
@@ -21,6 +21,7 @@ Item {
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 loadedImageRows: ""
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 int selectedIndex: 0
@@ -47,6 +48,8 @@ Item {
property int skewOffset: 28
property int bottomChromeHeight: showLabels ? (filterable ? 104 : 74) : (filterable ? 60 : 30)
onOpenedChanged: if (opened && imagesLoaded) focusPicker()
function fileUrl(path) {
return "file://" + path.split("/").map(encodeURIComponent).join("/")
}
@@ -61,8 +64,8 @@ Item {
}
function focusPicker() {
if (pickerWindowLoader.item && typeof pickerWindowLoader.item.focusCarousel === "function")
pickerWindowLoader.item.focusCarousel()
if (root.opened && root.imagesLoaded)
carousel.forceActiveFocus()
}
// Decode a base64-encoded UTF-8 string sent via IPC. Used for fields that
@@ -255,6 +258,7 @@ Item {
})
}
root.loadedImageRows = rows
root.imageArray = newImages
root.select(root.selectedImageIndex(), true)
root.imagesLoaded = true
@@ -277,16 +281,34 @@ Item {
showLabels = nextShowLabels === true || nextShowLabels === "true"
filterable = nextFilterable === true || nextFilterable === "true"
filterText = ""
if (imageRows && imageRows === loadedImageRows && imageArray.length > 0) {
root.select(root.selectedImageIndex(), true)
imagesLoaded = true
opened = true
root.focusPicker()
return
}
if (imageRows) {
var rowsToLoad = imageRows
var rowsSerial = requestSerial
imagesLoaded = true
opened = true
root.focusPicker()
Qt.callLater(function() {
if (rowsSerial === root.requestSerial)
root.loadRows(rowsToLoad)
})
return
}
imageArray = []
selectedIndex = 0
imagesLoaded = false
opened = false
if (imageRows) {
loadRows(imageRows)
} else {
loadImagesProc.requestSerial = requestSerial
loadImagesProc.running = true
}
loadImagesProc.requestSerial = requestSerial
loadImagesProc.running = true
}
property var imageArray: []
@@ -379,38 +401,32 @@ Item {
onExited: root.releaseNextDoneFile()
}
LazyLoader {
id: pickerWindowLoader
active: root.opened && root.imagesLoaded
onItemChanged: root.focusPicker()
PanelWindow {
id: panel
PanelWindow {
id: panel
visible: true
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
WlrLayershell.namespace: "omarchy-image-selector"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: root.opened && root.imagesLoaded ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
function focusCarousel() {
carousel.forceActiveFocus()
}
Rectangle {
anchors.fill: parent
visible: root.opened && root.imagesLoaded
color: root.withAlpha(root.background, 0.5)
}
visible: true
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
WlrLayershell.namespace: "omarchy-image-selector"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
exclusionMode: ExclusionMode.Ignore
MouseArea {
anchors.fill: parent
enabled: root.opened && root.imagesLoaded
onClicked: root.cancel()
}
Rectangle {
anchors.fill: parent
color: root.withAlpha(root.background, 0.5)
}
MouseArea {
anchors.fill: parent
onClicked: root.cancel()
}
Item {
id: card
Item {
id: card
visible: root.opened && root.imagesLoaded
width: Math.min(parent.width - 80, root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing) + 40)
height: root.expandedHeight + 30 + root.bottomChromeHeight
anchors.centerIn: parent
@@ -477,6 +493,8 @@ Item {
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
property bool sourceActivated: nearby
onNearbyChanged: if (nearby) sourceActivated = true
visible: nearby
x: selected ? carousel.previewX : (relativeIndex < 0 ? carousel.previewX + relativeIndex * carousel.itemStep : carousel.previewX + root.expandedWidth + root.sliceSpacing + (relativeIndex - 1) * carousel.itemStep)
@@ -527,14 +545,12 @@ Item {
Image {
id: image
anchors.fill: parent
// Keep a stable source while delegates move in and out of the
// nearby window. Clearing/reassigning the source on every
// selection change makes Qt tear down and reload textures,
// which shows up as flicker in both the theme and background
// selectors.
source: item.thumbnailPath ? root.fileUrl(item.thumbnailPath) : ""
// Load only the initial/visited nearby images, but keep the
// source once activated so Qt does not tear textures down as
// selection moves through the carousel.
source: item.sourceActivated && item.thumbnailPath ? root.fileUrl(item.thumbnailPath) : ""
fillMode: Image.PreserveAspectCrop
asynchronous: false
asynchronous: true
cache: true
smooth: true
}
@@ -602,7 +618,6 @@ Item {
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
}
}
}