From 2b08e800e80f3b274ea74a1be6bb408a65c331ca Mon Sep 17 00:00:00 2001 From: null <33122401+TheSynt4x@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:40:15 +0100 Subject: [PATCH] feat: improve icon resolution and align switcher fallback styling (#1823) - Implement deep search icon resolution in DesktopService with runtime caching. - Update Paths.getAppIcon to utilize enhanced resolution for mismatched app IDs. - Align Workspace Switcher fallback icons with AppsDock visual style. - Synchronize fallback text logic between Switcher and Dock using app names. --- quickshell/Common/Paths.qml | 8 +- .../DankBar/Widgets/WorkspaceSwitcher.qml | 81 ++++++++++++- quickshell/Services/DesktopService.qml | 110 ++++++++++++------ 3 files changed, 155 insertions(+), 44 deletions(-) diff --git a/quickshell/Common/Paths.qml b/quickshell/Common/Paths.qml index 83fe3857..ffc23224 100644 --- a/quickshell/Common/Paths.qml +++ b/quickshell/Common/Paths.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import Quickshell import QtCore +import qs.Services Singleton { id: root @@ -83,7 +84,12 @@ Singleton { if (desktopEntry && desktopEntry.icon) { return Quickshell.iconPath(desktopEntry.icon, true); } - return Quickshell.iconPath(appId, true); + + const icon = Quickshell.iconPath(appId, true); + if (icon && icon !== "") + return icon; + + return DesktopService.resolveIconPath(appId); } function getAppName(appId: string, desktopEntry: var): string { diff --git a/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml b/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml index 00016006..28e48557 100644 --- a/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml +++ b/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml @@ -290,6 +290,7 @@ Item { const moddedId = Paths.moddedAppId(keyBase); const desktopEntry = DesktopEntries.heuristicLookup(moddedId); const icon = Paths.getAppIcon(keyBase, desktopEntry); + const appName = Paths.getAppName(keyBase, desktopEntry); byApp[key] = { "type": "icon", "icon": icon, @@ -298,7 +299,7 @@ Item { "active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)), "count": 1, "windowId": w.address || w.id, - "fallbackText": w.appId || w.class || w.title || "" + "fallbackText": appName || "" }; } else { byApp[key].count++; @@ -1473,9 +1474,44 @@ Item { IconImage { id: rowAppIcon anchors.fill: parent - source: modelData.icon + source: modelData.icon || "" opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 - visible: !modelData.isQuickshell && !modelData.isSteamApp + visible: !modelData.isQuickshell && !modelData.isSteamApp && status === Image.Ready + } + + Rectangle { + anchors.fill: parent + visible: !modelData.isQuickshell && !modelData.isSteamApp && rowAppIcon.status !== Image.Ready + color: Theme.surfaceContainer + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: Theme.primarySelected + opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 + + StyledText { + anchors.centerIn: parent + text: (modelData.fallbackText || "?").charAt(0).toUpperCase() + font.pixelSize: parent.width * 0.45 + color: Theme.primary + font.weight: Font.Bold + } + } + + Rectangle { + anchors.fill: parent + visible: !modelData.isQuickshell && modelData.isSteamApp && rowSteamIcon.status !== Image.Ready + color: Theme.surfaceContainer + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: Theme.primarySelected + opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 + + DankIcon { + anchors.centerIn: parent + size: parent.width * 0.7 + name: "sports_esports" + color: Theme.primary + } } IconImage { @@ -1592,9 +1628,44 @@ Item { IconImage { id: colAppIcon anchors.fill: parent - source: modelData.icon + source: modelData.icon || "" opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 - visible: !modelData.isQuickshell && !modelData.isSteamApp + visible: !modelData.isQuickshell && !modelData.isSteamApp && status === Image.Ready + } + + Rectangle { + anchors.fill: parent + visible: !modelData.isQuickshell && !modelData.isSteamApp && colAppIcon.status !== Image.Ready + color: Theme.surfaceContainer + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: Theme.primarySelected + opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 + + StyledText { + anchors.centerIn: parent + text: (modelData.fallbackText || "?").charAt(0).toUpperCase() + font.pixelSize: parent.width * 0.45 + color: Theme.primary + font.weight: Font.Bold + } + } + + Rectangle { + anchors.fill: parent + visible: !modelData.isQuickshell && modelData.isSteamApp && colSteamIcon.status !== Image.Ready + color: Theme.surfaceContainer + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: Theme.primarySelected + opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 + + DankIcon { + anchors.centerIn: parent + size: parent.width * 0.7 + name: "sports_esports" + color: Theme.primary + } } IconImage { diff --git a/quickshell/Services/DesktopService.qml b/quickshell/Services/DesktopService.qml index b243de28..5afd4c1d 100644 --- a/quickshell/Services/DesktopService.qml +++ b/quickshell/Services/DesktopService.qml @@ -8,52 +8,86 @@ import Quickshell.Io Singleton { id: root + + property var _cache: ({}) + function resolveIconPath(moddedAppId) { - const entry = DesktopEntries.heuristicLookup(moddedAppId) - const appIds = [moddedAppId, moddedAppId.toLowerCase()]; + if (!moddedAppId) + return ""; - const lastPart = moddedAppId.split('.').pop(); - if (lastPart && lastPart !== moddedAppId) { - appIds.push(lastPart); + if (_cache[moddedAppId] !== undefined) + return _cache[moddedAppId]; - const firstChar = lastPart.charAt(0); - const rest = lastPart.slice(1); - let toggled; + const result = (function() { + // 1. Try heuristic lookup (standard) + const entry = DesktopEntries.heuristicLookup(moddedAppId); + let icon = Quickshell.iconPath(entry?.icon, true); + if (icon && icon !== "") + return icon; - if (firstChar === firstChar.toLowerCase()) { - toggled = firstChar.toUpperCase() + rest; - } else { - toggled = firstChar.toLowerCase() + rest; - } + // 2. Try the appId itself as an icon name + icon = Quickshell.iconPath(moddedAppId, true); + if (icon && icon !== "") + return icon; - if (toggled !== lastPart) { - appIds.push(toggled); - } - } - for (const appId of appIds){ - let icon = Quickshell.iconPath(entry?.icon, true) - if (icon && icon !== "") return icon + // 3. Try variations of the appId (lowercase, last part) + const appIds = [moddedAppId.toLowerCase()]; + const lastPart = moddedAppId.split('.').pop(); + if (lastPart && lastPart !== moddedAppId) { + appIds.push(lastPart); + appIds.push(lastPart.toLowerCase()); + } - let execPath = entry?.execString?.replace(/\/bin.*/, "") - if (!execPath) continue + for (const id of appIds) { + icon = Quickshell.iconPath(id, true); + if (icon && icon !== "") + return icon; + } - //Check that the app is installed with nix/guix - if (execPath.startsWith("/nix/store/") || execPath.startsWith("/gnu/store/")) { - const basePath = execPath - const sizes = ["256x256", "128x128", "64x64", "48x48", "32x32", "24x24", "16x16"] + // 4. Deep search in all desktop entries (if the above fail) + // This is slow-ish but only happens once for failed icons + const strippedId = moddedAppId.replace(/-bin$/, "").toLowerCase(); + const allEntries = DesktopEntries.applications.values; + for (let i = 0; i < allEntries.length; i++) { + const e = allEntries[i]; + const eId = (e.id || "").toLowerCase(); + const eName = (e.name || "").toLowerCase(); + const eExec = (e.execString || "").toLowerCase(); - let iconPath = `${basePath}/share/icons/hicolor/scalable/apps/${appId}.svg` - icon = Quickshell.iconPath(iconPath, true) - if (icon && icon !== "") return icon + if (eId.includes(strippedId) || eName.includes(strippedId) || eExec.includes(strippedId)) { + icon = Quickshell.iconPath(e.icon, true); + if (icon && icon !== "") + return icon; + } + } - for (const size of sizes) { - iconPath = `${basePath}/share/icons/hicolor/${size}/apps/${appId}.png` - icon = Quickshell.iconPath(iconPath, true) - if (icon && icon !== "") return icon - } - } + // 5. Nix/Guix specific store check (as a last resort) + for (const appId of appIds) { + let execPath = entry?.execString?.replace(/\/bin.*/, ""); + if (!execPath) + continue; + + if (execPath.startsWith("/nix/store/") || execPath.startsWith("/gnu/store/")) { + const basePath = execPath; + const sizes = ["256x256", "128x128", "64x64", "48x48", "32x32", "24x24", "16x16"]; + + let iconPath = `${basePath}/share/icons/hicolor/scalable/apps/${appId}.svg`; + icon = Quickshell.iconPath(iconPath, true); + if (icon && icon !== "") + return icon; + + for (const size of sizes) { + iconPath = `${basePath}/share/icons/hicolor/${size}/apps/${appId}.png`; + icon = Quickshell.iconPath(iconPath, true); + if (icon && icon !== "") + return icon; + } + } + } + + })(); + + _cache[moddedAppId] = result; + return result; } - - return "" -} }