launcher: show icons for apps installed mid-session
The launcher resolves each entry's icon through Quickshell.iconPath(), which bottoms out in Qt's QIconLoader / QIcon::fromTheme. That engine caches the theme's directory listing — and null results — for the life of the process, and only re-scans when the icon theme or search paths change. Nothing in Quickshell 0.3 exposes a way to invalidate it, and a QML reload doesn't reset it either (QIconLoader is a QtGui process global, not tied to the QML engine). So when a user installs a new app while the shell is running, its .desktop file is picked up live (DesktopEntries is file-watched and the name appears immediately), but its icon resolves to the cached miss and renders blank until the whole shell is restarted. That's a poor "I just installed this" experience. Fix it without a restart by keeping our own on-disk icon index as a fallback. When themed lookup returns empty, iconSource() consults a name->path map built by scanning the XDG icon dirs and /usr/share/ pixmaps (SVGs first, so scalable wins). The scan runs on startup and on a debounced DesktopEntries change — the same signal that already makes names appear — and swapping the iconIndex property re-evaluates every iconSource() binding, so freshly installed icons show up live. Icons Qt can already resolve are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4823e05c00
commit
efd8352dfb
@@ -31,6 +31,13 @@ Item {
|
||||
property bool deleteConfirmOpen: false
|
||||
property var deleteEntry: null
|
||||
|
||||
// Maps an icon name to a file on disk (e.g. "omacut" -> ".../apps/omacut.svg").
|
||||
// Used as a fallback for icons that Qt's themed lookup misses because they were
|
||||
// installed after this process started (its icon cache never re-scans). Refreshed
|
||||
// whenever the app list changes, so newly installed apps get their icon live.
|
||||
property var iconIndex: ({})
|
||||
property var pendingIconIndex: ({})
|
||||
|
||||
// Bound to the central [launcher] section in shell.toml via Color.qml.
|
||||
// Each color already includes its alpha companion (composed in the
|
||||
// singleton), so consumers can drop them straight into a Rectangle.
|
||||
@@ -93,7 +100,13 @@ Item {
|
||||
if (value.length === 0) return Quickshell.iconPath("application-x-executable", true)
|
||||
if (value.indexOf("file://") === 0 || value.indexOf("image://") === 0) return value
|
||||
if (value.charAt(0) === "/") return Util.fileUrl(value)
|
||||
return Quickshell.iconPath(value, true)
|
||||
var themed = Quickshell.iconPath(value, true)
|
||||
if (themed.length > 0) return themed
|
||||
// Qt's themed lookup missed (commonly a just-installed app whose icon landed
|
||||
// after our icon cache warmed). Fall back to the on-disk index we scanned.
|
||||
var found = root.iconIndex[value]
|
||||
if (found) return Util.fileUrl(found)
|
||||
return Quickshell.iconPath("application-x-executable", true)
|
||||
}
|
||||
|
||||
function entryName(entry) {
|
||||
@@ -149,6 +162,33 @@ Item {
|
||||
if (root.opened) root.rebuildDisplay()
|
||||
}
|
||||
|
||||
function iconIndexScanCommand() {
|
||||
// List app icons across the XDG icon dirs and /usr/share/pixmaps as
|
||||
// "<path>" lines. SVGs are emitted before PNGs so the parser, which keeps
|
||||
// the first hit per name, prefers a scalable icon over a fixed-size one.
|
||||
return [
|
||||
'dirs="$HOME/.icons $HOME/.local/share/icons";',
|
||||
'IFS=":"; for d in ${XDG_DATA_DIRS:-/usr/local/share:/usr/share}; do dirs="$dirs $d/icons"; done; unset IFS;',
|
||||
'for ext in svg png; do',
|
||||
' for base in $dirs; do',
|
||||
' [ -d "$base" ] && find "$base" -path "*/apps/*" -name "*.$ext" 2>/dev/null;',
|
||||
' done;',
|
||||
' find /usr/share/pixmaps -maxdepth 1 -name "*.$ext" 2>/dev/null;',
|
||||
'done'
|
||||
].join(' ')
|
||||
}
|
||||
|
||||
function indexIconLine(path) {
|
||||
var value = String(path || "").trim()
|
||||
if (value.length === 0) return
|
||||
var slash = value.lastIndexOf("/")
|
||||
var file = slash >= 0 ? value.slice(slash + 1) : value
|
||||
var dot = file.lastIndexOf(".")
|
||||
var name = dot > 0 ? file.slice(0, dot) : file
|
||||
if (name.length > 0 && root.pendingIconIndex[name] === undefined)
|
||||
root.pendingIconIndex[name] = value
|
||||
}
|
||||
|
||||
function hiddenEntryScanCommand() {
|
||||
var desktop = [Quickshell.env("XDG_CURRENT_DESKTOP"), Quickshell.env("XDG_SESSION_DESKTOP"), Quickshell.env("DESKTOP_SESSION")].filter(function(v) { return String(v || "").length > 0 }).join(":")
|
||||
var script = root.omarchyPath + "/shell/plugins/launcher/hidden-entries.sh"
|
||||
@@ -276,6 +316,24 @@ Item {
|
||||
onExited: root.loadDesktopHiddenEntries(hiddenEntryOutput.text)
|
||||
}
|
||||
|
||||
Process {
|
||||
id: iconIndexScan
|
||||
command: ["bash", "-lc", root.iconIndexScanCommand()]
|
||||
stdout: SplitParser { onRead: function(line) { root.indexIconLine(line) } }
|
||||
onStarted: root.pendingIconIndex = ({})
|
||||
// Swapping the property re-evaluates every iconSource() binding, so
|
||||
// newly found icons appear without rebuilding the list.
|
||||
onExited: root.iconIndex = root.pendingIconIndex
|
||||
}
|
||||
|
||||
// Coalesces bursts of app-list changes (a package install touches many
|
||||
// entries) into a single rescan.
|
||||
Timer {
|
||||
id: iconIndexDebounce
|
||||
interval: 750
|
||||
onTriggered: if (!iconIndexScan.running) iconIndexScan.running = true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: hiddenEntryOutput
|
||||
property string text: ""
|
||||
@@ -321,11 +379,15 @@ Item {
|
||||
target: DesktopEntries.applications
|
||||
function onValuesChanged() {
|
||||
hiddenEntryScan.running = true
|
||||
iconIndexDebounce.restart()
|
||||
if (root.opened) root.rebuildDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: hiddenEntryScan.running = true
|
||||
Component.onCompleted: {
|
||||
hiddenEntryScan.running = true
|
||||
iconIndexScan.running = true
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: panel
|
||||
|
||||
Reference in New Issue
Block a user