Faster theme and bg changes
This commit is contained in:
+12
-2
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Open a generic image selector menu
|
||||
# omarchy:args=[--selected <image>] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--cache-only] <image-dir>...
|
||||
# omarchy:args=[--selected <image>] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--preload] [--cache-only] <image-dir>...
|
||||
|
||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
||||
|
||||
@@ -11,11 +11,12 @@ show_labels=false
|
||||
filterable=false
|
||||
lazy_thumbnails=false
|
||||
prepare_only=false
|
||||
preload=false
|
||||
cache_only=false
|
||||
image_dirs=()
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-menu-images [--selected <image>] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--cache-only] <image-dir>..."
|
||||
echo "Usage: omarchy-menu-images [--selected <image>] [--print-name] [--show-labels] [--filterable] [--lazy-thumbnails] [--preload] [--cache-only] <image-dir>..."
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -49,6 +50,10 @@ while [[ $# -gt 0 ]]; do
|
||||
prepare_only=true
|
||||
shift
|
||||
;;
|
||||
--preload)
|
||||
preload=true
|
||||
shift
|
||||
;;
|
||||
--cache-only)
|
||||
cache_only=true
|
||||
shift
|
||||
@@ -225,6 +230,11 @@ fi
|
||||
# ImagePicker plugin Qt.atob()s on the other side.
|
||||
rows_b64=$(printf '%s' "$rows" | base64 -w 0)
|
||||
|
||||
if [[ $preload == true ]]; then
|
||||
omarchy-shell-ipc image-selector preload "$rows_b64" "$selected_list_image" "$show_labels" "$filterable" >/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
send_builtin_ipc_request() {
|
||||
perl -MCwd=abs_path -MEncode=encode,decode -MSocket \
|
||||
-e '
|
||||
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Send a fire-and-forget IPC call to a running omarchy-shell
|
||||
# omarchy:hidden=true
|
||||
|
||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
||||
SHELL_QML="$OMARCHY_PATH/default/quickshell/omarchy-shell/shell.qml"
|
||||
|
||||
if (( $# < 2 )); then
|
||||
echo "Usage: omarchy-shell-ipc-fast <target> <function> [args...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
perl -MCwd=abs_path -MEncode=encode,decode -MSocket \
|
||||
-e '
|
||||
sub read_qstring {
|
||||
my ($data, $offset) = @_;
|
||||
return ("", $offset) if $offset + 4 > length($data);
|
||||
my $length = unpack("N", substr($data, $offset, 4));
|
||||
$offset += 4;
|
||||
return ("", $offset) if $length == 0xffffffff || $offset + $length > length($data);
|
||||
return (decode("UTF-16BE", substr($data, $offset, $length)), $offset + $length);
|
||||
}
|
||||
|
||||
sub qstring {
|
||||
my $encoded = encode("UTF-16BE", $_[0] // "");
|
||||
return pack("N", length($encoded)) . $encoded;
|
||||
}
|
||||
|
||||
my $shell_qml = abs_path(shift @ARGV);
|
||||
my $target = shift @ARGV;
|
||||
my $function = shift @ARGV;
|
||||
my @args = @ARGV;
|
||||
my $runtime_dir = $ENV{"XDG_RUNTIME_DIR"} || "/run/user/$<";
|
||||
my $payload = chr(3) . qstring($target) . qstring($function) . pack("N", scalar @args) . join("", map { qstring($_) } @args);
|
||||
my @candidates;
|
||||
|
||||
for my $lock_path (glob("$runtime_dir/quickshell/by-id/*/instance.lock")) {
|
||||
my $data;
|
||||
next unless open(my $lock, "<:raw", $lock_path);
|
||||
{ local $/; $data = <$lock>; }
|
||||
close($lock);
|
||||
my (undef, $offset) = read_qstring($data, 0);
|
||||
my ($path) = read_qstring($data, $offset);
|
||||
next unless $path && abs_path($path) eq $shell_qml;
|
||||
(my $socket_path = $lock_path) =~ s{/instance\.lock$}{/ipc.sock};
|
||||
push @candidates, [(stat($lock_path))[9] || 0, $socket_path];
|
||||
}
|
||||
|
||||
for my $candidate (sort { $b->[0] <=> $a->[0] } @candidates) {
|
||||
socket(my $client, AF_UNIX, SOCK_STREAM, 0) || next;
|
||||
if (connect($client, sockaddr_un($candidate->[1]))) {
|
||||
syswrite($client, $payload);
|
||||
close($client);
|
||||
exit 0;
|
||||
}
|
||||
close($client);
|
||||
}
|
||||
|
||||
exit 1;
|
||||
' "$SHELL_QML" "$@"
|
||||
@@ -19,3 +19,7 @@ fi
|
||||
|
||||
# Create symlink to the new background
|
||||
ln -nsf "$BACKGROUND" "$CURRENT_BACKGROUND_LINK"
|
||||
|
||||
# Update the live shell background immediately when it is running. The
|
||||
# background plugin also polls this symlink, but IPC avoids the visible delay.
|
||||
omarchy-shell-ipc-fast background set "$BACKGROUND" >/dev/null 2>&1 || true
|
||||
|
||||
+50
-7
@@ -11,6 +11,7 @@ fi
|
||||
|
||||
CURRENT_THEME_PATH="$HOME/.config/omarchy/current/theme"
|
||||
NEXT_THEME_PATH="$HOME/.config/omarchy/current/next-theme"
|
||||
CURRENT_BACKGROUND_LINK="$HOME/.config/omarchy/current/background"
|
||||
USER_THEMES_PATH="$HOME/.config/omarchy/themes"
|
||||
OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes"
|
||||
|
||||
@@ -28,6 +29,41 @@ run_parallel() {
|
||||
done
|
||||
}
|
||||
|
||||
set_theme_background() {
|
||||
local backgrounds=()
|
||||
local current_background index new_background next_index
|
||||
|
||||
mapfile -d '' -t backgrounds < <(
|
||||
find -L "$HOME/.config/omarchy/backgrounds/$THEME_NAME/" "$CURRENT_THEME_PATH/backgrounds/" -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
|
||||
)
|
||||
|
||||
if (( ${#backgrounds[@]} == 0 )); then
|
||||
omarchy-notification-send "No background was found for theme" -t 2000
|
||||
return
|
||||
fi
|
||||
|
||||
current_background=$(readlink "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true)
|
||||
index=-1
|
||||
for i in "${!backgrounds[@]}"; do
|
||||
if [[ ${backgrounds[$i]} == $current_background ]]; then
|
||||
index=$i
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if (( index == -1 )); then
|
||||
new_background="${backgrounds[0]}"
|
||||
else
|
||||
next_index=$(((index + 1) % ${#backgrounds[@]}))
|
||||
new_background="${backgrounds[$next_index]}"
|
||||
fi
|
||||
|
||||
ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK"
|
||||
omarchy-shell-ipc-fast background setInstant "$new_background" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
THEME_NAME=$(echo "$1" | sed -E 's/<[^>]+>//g' | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
|
||||
|
||||
if [[ ! -d $OMARCHY_THEMES_PATH/$THEME_NAME ]] && [[ ! -d $USER_THEMES_PATH/$THEME_NAME ]]; then
|
||||
@@ -58,8 +94,17 @@ mv "$NEXT_THEME_PATH" "$CURRENT_THEME_PATH"
|
||||
# Store theme name for reference
|
||||
echo "$THEME_NAME" >"$HOME/.config/omarchy/current/theme.name"
|
||||
|
||||
# Make the running shell pick up the new palette immediately instead of waiting
|
||||
# for file-watch debounce while the rest of the theme hooks run.
|
||||
colors_payload=$([[ -f $CURRENT_THEME_PATH/colors.toml ]] && base64 -w 0 "$CURRENT_THEME_PATH/colors.toml")
|
||||
shell_payload=$([[ -f $CURRENT_THEME_PATH/shell.toml ]] && base64 -w 0 "$CURRENT_THEME_PATH/shell.toml")
|
||||
omarchy-shell-ipc-fast shell applyTheme "$colors_payload" "$shell_payload" >/dev/null 2>&1 || true
|
||||
|
||||
if [[ $OMARCHY_THEME_SKIP_BACKGROUND != "1" ]]; then
|
||||
set_theme_background
|
||||
fi
|
||||
|
||||
post_theme_commands=(
|
||||
omarchy-restart-shell
|
||||
omarchy-restart-terminal
|
||||
omarchy-restart-hyprctl
|
||||
omarchy-restart-btop
|
||||
@@ -73,15 +118,13 @@ post_theme_commands=(
|
||||
omarchy-theme-set-keyboard
|
||||
)
|
||||
|
||||
# Change background with theme while restarting/retheming independent components.
|
||||
if [[ $OMARCHY_THEME_SKIP_BACKGROUND != "1" ]]; then
|
||||
post_theme_commands+=(omarchy-theme-bg-next)
|
||||
fi
|
||||
|
||||
run_parallel "${post_theme_commands[@]}"
|
||||
|
||||
# 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.
|
||||
# Warm selector caches after the theme is applied. The shell hot-reloads theme
|
||||
# colors/backgrounds, so keep the running instance alive and preload the picker
|
||||
# rows/selection to avoid first-open carousel settling after a theme change.
|
||||
omarchy-theme-switcher --preload >/dev/null 2>&1
|
||||
omarchy-theme-bg-cache >/dev/null 2>&1 &
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
# omarchy:summary=Open the Omarchy theme switcher
|
||||
|
||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
||||
preload=false
|
||||
|
||||
if [[ $1 == "--preload" ]]; then
|
||||
preload=true
|
||||
shift
|
||||
fi
|
||||
|
||||
USER_THEMES_PATH="$HOME/.config/omarchy/themes"
|
||||
OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes"
|
||||
CACHE_PATH="${XDG_CACHE_HOME:-$HOME/.cache}/omarchy/theme-selector"
|
||||
@@ -104,10 +111,16 @@ for extension in png jpg jpeg webp gif bmp; do
|
||||
break
|
||||
fi
|
||||
done
|
||||
exec omarchy-menu-images \
|
||||
--print-name \
|
||||
--show-labels \
|
||||
--filterable \
|
||||
--lazy-thumbnails \
|
||||
--selected "$selected_preview" \
|
||||
"$preview_dir"
|
||||
menu_args=(
|
||||
--print-name
|
||||
--show-labels
|
||||
--filterable
|
||||
--lazy-thumbnails
|
||||
--selected "$selected_preview"
|
||||
)
|
||||
|
||||
if [[ $preload == true ]]; then
|
||||
menu_args+=(--preload)
|
||||
fi
|
||||
|
||||
exec omarchy-menu-images "${menu_args[@]}" "$preview_dir"
|
||||
|
||||
@@ -170,6 +170,11 @@ QtObject {
|
||||
shellValues = parsed
|
||||
}
|
||||
|
||||
function reloadTheme() {
|
||||
colorsFile.reload()
|
||||
shellFile.reload()
|
||||
}
|
||||
|
||||
// `omarchy-theme-set` recreates the theme/ directory via rm+mv, which kills
|
||||
// the inotify watch on colors.toml. Use theme.name (overwritten in place) as
|
||||
// a tripwire that forces a fresh reload after each swap.
|
||||
@@ -194,6 +199,6 @@ QtObject {
|
||||
path: Quickshell.env("HOME") + "/.config/omarchy/current/theme.name"
|
||||
watchChanges: true
|
||||
printErrors: false
|
||||
onFileChanged: { colorsFile.reload(); shellFile.reload() }
|
||||
onFileChanged: root.reloadTheme()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,20 +30,23 @@ Item {
|
||||
if (!readlinkProc.running) readlinkProc.running = true
|
||||
}
|
||||
|
||||
function setBackground(path) {
|
||||
function setBackground(path, instant) {
|
||||
path = String(path || "").trim()
|
||||
if (!path || path === currentBackground) return
|
||||
currentBackground = path
|
||||
backgroundVersion += 1
|
||||
|
||||
if (!displayedBackground) {
|
||||
revealAnimation.stop()
|
||||
finishingTransition = false
|
||||
|
||||
if (instant || !displayedBackground) {
|
||||
oldBackground = ""
|
||||
incomingBackground = ""
|
||||
displayedBackground = path
|
||||
revealProgress = 1
|
||||
return
|
||||
}
|
||||
|
||||
revealAnimation.stop()
|
||||
finishingTransition = false
|
||||
oldBackground = displayedBackground
|
||||
incomingBackground = path
|
||||
revealProgress = 0
|
||||
@@ -73,7 +76,23 @@ Item {
|
||||
id: readlinkProc
|
||||
command: ["readlink", "-f", root.currentBackgroundLink]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.setBackground(String(text || "").trim())
|
||||
onStreamFinished: root.setBackground(String(text || "").trim(), false)
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "background"
|
||||
|
||||
function refresh(): void {
|
||||
root.refreshBackground()
|
||||
}
|
||||
|
||||
function set(path: string): void {
|
||||
root.setBackground(path, false)
|
||||
}
|
||||
|
||||
function setInstant(path: string): void {
|
||||
root.setBackground(path, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ Item {
|
||||
property bool opened: false
|
||||
property bool showLabels: false
|
||||
property bool filterable: false
|
||||
property bool layoutSettled: false
|
||||
property bool requestActive: false
|
||||
property int requestSerial: 0
|
||||
property int applySerial: 0
|
||||
@@ -48,7 +49,7 @@ Item {
|
||||
property int skewOffset: 28
|
||||
property int bottomChromeHeight: showLabels ? (filterable ? 104 : 74) : (filterable ? 60 : 30)
|
||||
|
||||
onOpenedChanged: if (opened && imagesLoaded) focusPicker()
|
||||
onOpenedChanged: if (!opened) layoutSettled = false
|
||||
|
||||
function fileUrl(path) {
|
||||
return "file://" + path.split("/").map(encodeURIComponent).join("/")
|
||||
@@ -64,10 +65,19 @@ Item {
|
||||
}
|
||||
|
||||
function focusPicker() {
|
||||
if (root.opened && root.imagesLoaded)
|
||||
if (root.opened && root.imagesLoaded && root.layoutSettled)
|
||||
carousel.forceActiveFocus()
|
||||
}
|
||||
|
||||
function revealWhenSettled(serial) {
|
||||
Qt.callLater(function() {
|
||||
if (serial === root.requestSerial && root.opened && root.imagesLoaded && root.imageArray.length > 0) {
|
||||
root.layoutSettled = true
|
||||
root.focusPicker()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Decode a base64-encoded UTF-8 string sent via IPC. Used for fields that
|
||||
// would otherwise carry embedded newlines or tabs (image rows, raw colors
|
||||
// JSON) which bash IPC arguments can't reliably round-trip.
|
||||
@@ -237,7 +247,7 @@ Item {
|
||||
root.opened = false
|
||||
}
|
||||
|
||||
function loadRows(rows) {
|
||||
function loadRows(rows, reveal) {
|
||||
var newImages = []
|
||||
var seen = {}
|
||||
var paths = rows.split("\n")
|
||||
@@ -259,11 +269,14 @@ Item {
|
||||
}
|
||||
|
||||
root.loadedImageRows = rows
|
||||
root.selectedIndex = root.indexForSelectedImage(newImages)
|
||||
root.imageArray = newImages
|
||||
root.select(root.selectedImageIndex(), true)
|
||||
root.imagesLoaded = true
|
||||
root.opened = true
|
||||
root.focusPicker()
|
||||
|
||||
if (reveal !== false) {
|
||||
root.opened = true
|
||||
root.revealWhenSettled(root.requestSerial)
|
||||
}
|
||||
}
|
||||
|
||||
function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextShowLabels, nextFilterable) {
|
||||
@@ -281,24 +294,26 @@ Item {
|
||||
showLabels = nextShowLabels === true || nextShowLabels === "true"
|
||||
filterable = nextFilterable === true || nextFilterable === "true"
|
||||
filterText = ""
|
||||
layoutSettled = false
|
||||
|
||||
if (imageRows && imageRows === loadedImageRows && imageArray.length > 0) {
|
||||
root.select(root.selectedImageIndex(), true)
|
||||
imagesLoaded = true
|
||||
opened = true
|
||||
root.focusPicker()
|
||||
root.revealWhenSettled(requestSerial)
|
||||
return
|
||||
}
|
||||
|
||||
if (imageRows) {
|
||||
var rowsToLoad = imageRows
|
||||
var rowsSerial = requestSerial
|
||||
imageArray = []
|
||||
selectedIndex = 0
|
||||
imagesLoaded = true
|
||||
opened = true
|
||||
root.focusPicker()
|
||||
Qt.callLater(function() {
|
||||
if (rowsSerial === root.requestSerial)
|
||||
root.loadRows(rowsToLoad)
|
||||
root.loadRows(rowsToLoad, true)
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -313,15 +328,19 @@ Item {
|
||||
|
||||
property var imageArray: []
|
||||
|
||||
function selectedImageIndex() {
|
||||
for (var i = 0; i < imageArray.length; i++) {
|
||||
if (imageArray[i].filePath === selectedImage)
|
||||
function indexForSelectedImage(images) {
|
||||
for (var i = 0; i < images.length; i++) {
|
||||
if (images[i].filePath === selectedImage)
|
||||
return i
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function selectedImageIndex() {
|
||||
return indexForSelectedImage(imageArray)
|
||||
}
|
||||
|
||||
Process {
|
||||
id: loadImagesProc
|
||||
property int requestSerial: 0
|
||||
@@ -330,7 +349,7 @@ Item {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
if (loadImagesProc.requestSerial === root.requestSerial)
|
||||
root.loadRows(String(text || ""))
|
||||
root.loadRows(String(text || ""), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,6 +378,23 @@ Item {
|
||||
cancel()
|
||||
}
|
||||
|
||||
function preloadRows(nextImageRows, nextSelectedImage, nextShowLabels, nextFilterable) {
|
||||
requestSerial += 1
|
||||
imageRows = nextImageRows
|
||||
selectedImage = nextSelectedImage
|
||||
showLabels = nextShowLabels === true || nextShowLabels === "true"
|
||||
filterable = nextFilterable === true || nextFilterable === "true"
|
||||
filterText = ""
|
||||
layoutSettled = false
|
||||
|
||||
if (imageRows && imageRows === loadedImageRows && imageArray.length > 0) {
|
||||
selectedIndex = selectedImageIndex()
|
||||
imagesLoaded = true
|
||||
} else if (imageRows) {
|
||||
loadRows(imageRows, false)
|
||||
}
|
||||
}
|
||||
|
||||
// IPC surface. All arguments are strings (Quickshell IPC marshalling).
|
||||
// imageRows can contain newlines/tabs, so the CLI caller base64-encodes
|
||||
// it; everything else passes through verbatim. The two boolean-like
|
||||
@@ -379,6 +415,15 @@ Item {
|
||||
return "ok"
|
||||
}
|
||||
|
||||
function preload(imageRowsB64: string,
|
||||
selectedImage: string,
|
||||
showLabels: string,
|
||||
filterable: string): string {
|
||||
var rows = root.decodeBase64(imageRowsB64)
|
||||
root.preloadRows(rows, selectedImage, showLabels, filterable)
|
||||
return "ok"
|
||||
}
|
||||
|
||||
function cancel(doneFile: string): void {
|
||||
root.closeSelector(doneFile || "")
|
||||
}
|
||||
@@ -426,7 +471,7 @@ Item {
|
||||
|
||||
Item {
|
||||
id: card
|
||||
visible: root.opened && root.imagesLoaded
|
||||
visible: root.opened && root.imagesLoaded && root.layoutSettled && root.imageArray.length > 0
|
||||
width: Math.min(parent.width - 80, root.expandedWidth + 13 * (root.sliceWidth + root.sliceSpacing) + 40)
|
||||
height: root.expandedHeight + 30 + root.bottomChromeHeight
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -787,6 +787,21 @@ ShellRoot {
|
||||
return "ok"
|
||||
}
|
||||
|
||||
function applyTheme(colorsB64: string, shellB64: string): string {
|
||||
var colorsRaw = ""
|
||||
var shellRaw = ""
|
||||
try { colorsRaw = Qt.atob(String(colorsB64 || "")) } catch (e) { colorsRaw = "" }
|
||||
try { shellRaw = Qt.atob(String(shellB64 || "")) } catch (e2) { shellRaw = "" }
|
||||
NoctaliaCommons.Color.loadColors(colorsRaw)
|
||||
NoctaliaCommons.Color.loadShell(shellRaw)
|
||||
return "ok"
|
||||
}
|
||||
|
||||
function reloadTheme(): string {
|
||||
NoctaliaCommons.Color.reloadTheme()
|
||||
return "ok"
|
||||
}
|
||||
|
||||
function rescanPlugins(): void {
|
||||
shell.pluginRegistry.rescan()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user