Make the theme picker open instantly (#13403)
* Run menu summon actions in-process A menu action that only summons another shell plugin spawned bash and a qs ipc client to ask this same shell to do it, about 60ms of the path. Call shell.summon directly instead, and fall back to bash when the call is refused or the action is anything more than a bare summon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Open the theme picker from rows held in the shell Opening the theme picker ran omarchy-theme-switcher to rebuild its index and then made a second IPC call, about 170ms before the picker mapped. The picker now holds the theme rows itself, opens from them at once, and refreshes them behind the open via omarchy-theme-switcher --print-rows. It applies the chosen theme with omarchy-theme-set directly, so omarchy-theme-set no longer preloads the picker. From the keybinding to the overlay mapped drops from ~245ms to ~83ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the image picker surface mapped between opens Each open mapped a fresh surface, which rendered its first frames before Hyprland sent its fractional scale: the pixel ratio stepped 2, 1, then 1.6, so the picker flashed blurry for ~130ms and re-uploaded every thumbnail texture. Keep the surface and park it transparent and input-less on the bottom layer while closed, since anything on the overlay layer blocks direct scanout for fullscreen apps. It follows the focused monitor on each open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e558224acf
commit
24f1243120
14 files changed
+232
-27
No files matched your search
@@ -10,6 +10,7 @@ import qs.Ui
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var shell: null
|
||||
readonly property string home: Quickshell.env("HOME")
|
||||
readonly property string stateHome: home + "/.local/state"
|
||||
readonly property string currentBackgroundLink: stateHome + "/omarchy/current/background"
|
||||
@@ -111,7 +112,12 @@ Item {
|
||||
}
|
||||
|
||||
function openThemeSwitcher() {
|
||||
if (!themeSwitchProc.running) themeSwitchProc.running = true
|
||||
var payload = JSON.stringify({ source: "themes" })
|
||||
|
||||
// A cloned background may not summon the picker in-process, so it takes
|
||||
// the IPC route instead.
|
||||
if (!root.shell || !root.shell.summon("omarchy.image-picker", payload))
|
||||
Util.execArgv(["omarchy-shell", "shell", "summon", "omarchy.image-picker", payload])
|
||||
}
|
||||
|
||||
Process {
|
||||
@@ -120,12 +126,6 @@ Item {
|
||||
onExited: root.refreshBackground()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: themeSwitchProc
|
||||
command: ["bash", "-c", "theme=$(omarchy-theme-switcher); [[ -n $theme ]] && omarchy-theme-set \"$theme\" >/dev/null 2>&1 &"]
|
||||
onExited: root.refreshBackground()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: readlinkProc
|
||||
command: ["readlink", "-f", root.currentBackgroundLink]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
@@ -30,6 +31,14 @@ Item {
|
||||
property string doneFile: ""
|
||||
property string filterText: ""
|
||||
property var doneFilesToRelease: []
|
||||
// Themes open from rows the shell already holds, so the picker shows without
|
||||
// waiting on omarchy-theme-switcher; each open refreshes them behind it.
|
||||
property string themeRows: ""
|
||||
property bool themeMode: false
|
||||
property bool themeOpenPending: false
|
||||
// The surface outlives each open, so it no longer lands on the focused
|
||||
// output by itself. Unset until the first open lets the compositor choose.
|
||||
property var targetScreen: null
|
||||
// Bound to the central [image-picker] section in shell.toml via Color.qml.
|
||||
// `dimColor` tints unselected slices and text outlines on top of the scrim;
|
||||
// it intentionally tracks the foundational background, not a surface role.
|
||||
@@ -151,6 +160,14 @@ Item {
|
||||
|
||||
function applySelected() {
|
||||
var path = currentPath()
|
||||
|
||||
if (themeMode) {
|
||||
themeMode = false
|
||||
root.opened = false
|
||||
if (path) Util.execArgv(["omarchy-theme-set", nameForPath(path)])
|
||||
return
|
||||
}
|
||||
|
||||
if (!path || !selectionFile) {
|
||||
cancel()
|
||||
return
|
||||
@@ -168,6 +185,8 @@ Item {
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
themeOpenPending = false
|
||||
|
||||
if (requestActive)
|
||||
finishDoneFile(doneFile)
|
||||
|
||||
@@ -179,6 +198,7 @@ Item {
|
||||
|
||||
function closeSelector(nextDoneFile) {
|
||||
requestSerial += 1
|
||||
themeOpenPending = false
|
||||
|
||||
if (requestActive)
|
||||
finishDoneFile(doneFile)
|
||||
@@ -212,6 +232,9 @@ Item {
|
||||
finishDoneFile(doneFile)
|
||||
|
||||
requestSerial += 1
|
||||
themeMode = false
|
||||
themeOpenPending = false
|
||||
targetScreen = focusedScreen() || targetScreen
|
||||
|
||||
imageDirs = nextImageDirs
|
||||
imageRows = nextImageRows
|
||||
@@ -255,6 +278,76 @@ Item {
|
||||
|
||||
property var imageArray: []
|
||||
|
||||
function focusedScreen() {
|
||||
var monitor = Hyprland.focusedMonitor
|
||||
var name = monitor ? String(monitor.name || "") : ""
|
||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === name) return Quickshell.screens[i]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function currentThemePreview() {
|
||||
var name = String(themeNameFile.text() || "").trim()
|
||||
var images = ImagePickerModel.loadRows(themeRows)
|
||||
for (var i = 0; i < images.length; i++) {
|
||||
if (nameForPath(images[i].filePath) === name) return images[i].filePath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function openThemes() {
|
||||
if (themeRows) {
|
||||
openThemeRows()
|
||||
} else {
|
||||
// First open before the startup refresh has landed.
|
||||
themeOpenPending = true
|
||||
}
|
||||
refreshThemeRows()
|
||||
}
|
||||
|
||||
function openThemeRows() {
|
||||
openSelector("", themeRows, currentThemePreview(), "", "", true, true)
|
||||
themeMode = true
|
||||
}
|
||||
|
||||
function refreshThemeRows() {
|
||||
if (!themeRowsProc.running) themeRowsProc.running = true
|
||||
}
|
||||
|
||||
function updateThemeRows(rows) {
|
||||
var changed = rows !== themeRows
|
||||
themeRows = rows
|
||||
|
||||
if (themeOpenPending) {
|
||||
themeOpenPending = false
|
||||
if (rows) openThemeRows()
|
||||
} else if (changed && rows && themeMode && opened) {
|
||||
// A theme was added or removed since the rows were last read. Keep the
|
||||
// user's place in the carousel rather than jumping back to the current.
|
||||
selectedImage = currentPath() || currentThemePreview()
|
||||
imageRows = rows
|
||||
loadRows(rows, false)
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: themeNameFile
|
||||
path: root.stateHome + "/omarchy/current/theme.name"
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: themeRowsProc
|
||||
command: [root.omarchyPath + "/bin/omarchy-theme-switcher", "--print-rows"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.updateThemeRows(String(text || "").trim())
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: refreshThemeRows()
|
||||
|
||||
function startImageScan(serial, dirs) {
|
||||
if (loadImagesProc.running) {
|
||||
loadImagesProc.queuedSerial = serial
|
||||
@@ -309,6 +402,10 @@ Item {
|
||||
if (payload) {
|
||||
try { args = JSON.parse(payload) || {} } catch (e) { args = {} }
|
||||
}
|
||||
if (args.source === "themes") {
|
||||
openThemes()
|
||||
return
|
||||
}
|
||||
var dirs = String(args.imageDirs || imageDirs)
|
||||
var rows = String(args.imageRows || "")
|
||||
var sel = String(args.selectedImage || selectedImage)
|
||||
@@ -362,14 +459,23 @@ Item {
|
||||
PanelWindow {
|
||||
id: panel
|
||||
|
||||
visible: root.opened
|
||||
// Stay mapped between opens. A fresh surface renders its first frames
|
||||
// before the compositor sends its fractional scale, so the picker flashed
|
||||
// blurry, and it had to re-upload every thumbnail texture. Closed, it
|
||||
// waits transparent and input-less on the bottom layer: anything left on
|
||||
// the overlay layer would block direct scanout for fullscreen apps.
|
||||
visible: true
|
||||
screen: root.targetScreen
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
mask: root.opened ? null : closedMask
|
||||
WlrLayershell.namespace: "omarchy-image-selector"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.layer: root.opened ? WlrLayer.Overlay : WlrLayer.Bottom
|
||||
WlrLayershell.keyboardFocus: root.opened && root.imagesLoaded ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
Region { id: closedMask }
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: root.opened && root.imagesLoaded
|
||||
|
||||
@@ -138,6 +138,9 @@ Item {
|
||||
var command = String(action || "")
|
||||
if (!command) return
|
||||
|
||||
var summon = MenuModel.summonAction(command)
|
||||
if (summon && root.shell && root.shell.summon(summon.id, summon.payload)) return
|
||||
|
||||
Util.execDetached(command)
|
||||
}
|
||||
|
||||
|
||||
@@ -490,8 +490,19 @@ function guardScript(items) {
|
||||
return guards ? guardPrelude(guards) + guards : ""
|
||||
}
|
||||
|
||||
// An action that only asks this shell to summon one of its own plugins can be
|
||||
// run in-process, skipping the bash and qs ipc spawns. Anything more than the
|
||||
// bare call, such as a second command or a shell-expanded payload, is left to
|
||||
// bash.
|
||||
function summonAction(action) {
|
||||
var match = /^omarchy-shell shell summon ([A-Za-z0-9._-]+)(?: '([^']*)')?$/.exec(String(action || ""))
|
||||
if (!match) return null
|
||||
return { id: match[1], payload: match[2] || "{}" }
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
summonAction: summonAction,
|
||||
guardReaders: GUARD_READERS,
|
||||
guardScript: guardScript,
|
||||
stripJsonc: stripJsonc,
|
||||
|
||||
Reference in new issue
Block a user