diff --git a/quickshell/Common/Paths.qml b/quickshell/Common/Paths.qml index 107e3a2d..7805a7f5 100644 --- a/quickshell/Common/Paths.qml +++ b/quickshell/Common/Paths.qml @@ -46,22 +46,20 @@ Singleton { } function moddedAppId(appId: string): string { - if (appId === "Spotify") - return "spotify"; - if (appId === "beepertexts") - return "beeper"; - if (appId === "home assistant desktop") - return "homeassistant-desktop"; - if (appId.includes("com.transmissionbt.transmission")) { - if (DesktopEntries.heuristicLookup("transmission-gtk")) - return "transmission-gtk"; - if (DesktopEntries.heuristicLookup("transmission")) - return "transmission"; - return "transmission-gtk"; + const subs = SettingsData.appIdSubstitutions || []; + for (let i = 0; i < subs.length; i++) { + const sub = subs[i]; + if (sub.type === "exact" && appId === sub.pattern) { + return sub.replacement; + } else if (sub.type === "contains" && appId.includes(sub.pattern)) { + return sub.replacement; + } else if (sub.type === "regex") { + const match = appId.match(new RegExp(sub.pattern)); + if (match) { + return sub.replacement.replace(/\$(\d+)/g, (_, n) => match[n] || ""); + } + } } - const steamMatch = appId.match(/^steam_app_(\d+)$/); - if (steamMatch) - return `steam_icon_${steamMatch[1]}`; return appId; } @@ -71,7 +69,7 @@ Singleton { } const moddedId = moddedAppId(appId); - if (moddedId.startsWith("steam_icon_")) { + if (moddedId !== appId) { return Quickshell.iconPath(moddedId, true); } diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 321fdc64..5a4afec9 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -221,6 +221,7 @@ Singleton { property bool keyboardLayoutNameCompactMode: false property bool runningAppsCurrentWorkspace: false property bool runningAppsGroupByApp: false + property var appIdSubstitutions: [] property string centeringMode: "index" property string clockDateFormat: "" property string lockDateFormat: "" @@ -1842,6 +1843,40 @@ Singleton { return workspaceNameIcons[workspaceName] || null; } + function addAppIdSubstitution(pattern, replacement, type) { + var subs = JSON.parse(JSON.stringify(appIdSubstitutions)); + subs.push({ pattern: pattern, replacement: replacement, type: type }); + appIdSubstitutions = subs; + saveSettings(); + } + + function updateAppIdSubstitution(index, pattern, replacement, type) { + var subs = JSON.parse(JSON.stringify(appIdSubstitutions)); + if (index < 0 || index >= subs.length) + return; + subs[index] = { pattern: pattern, replacement: replacement, type: type }; + appIdSubstitutions = subs; + saveSettings(); + } + + function removeAppIdSubstitution(index) { + var subs = JSON.parse(JSON.stringify(appIdSubstitutions)); + if (index < 0 || index >= subs.length) + return; + subs.splice(index, 1); + appIdSubstitutions = subs; + saveSettings(); + } + + function getDefaultAppIdSubstitutions() { + return Spec.SPEC.appIdSubstitutions.def; + } + + function resetAppIdSubstitutions() { + appIdSubstitutions = JSON.parse(JSON.stringify(Spec.SPEC.appIdSubstitutions.def)); + saveSettings(); + } + function getRegistryThemeVariant(themeId, defaultVariant) { var stored = registryThemeVariants[themeId]; if (typeof stored === "string") diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 3b2fd9e8..341bbecf 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -115,6 +115,13 @@ var SPEC = { keyboardLayoutNameCompactMode: { def: false }, runningAppsCurrentWorkspace: { def: false }, runningAppsGroupByApp: { def: false }, + appIdSubstitutions: { def: [ + { pattern: "Spotify", replacement: "spotify", type: "exact" }, + { pattern: "beepertexts", replacement: "beeper", type: "exact" }, + { pattern: "home assistant desktop", replacement: "homeassistant-desktop", type: "exact" }, + { pattern: "com.transmissionbt.transmission", replacement: "transmission-gtk", type: "contains" }, + { pattern: "^steam_app_(\\d+)$", replacement: "steam_icon_$1", type: "regex" } + ]}, centeringMode: { def: "index" }, clockDateFormat: { def: "" }, lockDateFormat: { def: "" }, diff --git a/quickshell/Modules/DankBar/Widgets/FocusedApp.qml b/quickshell/Modules/DankBar/Widgets/FocusedApp.qml index 58bc7868..0cd641e0 100644 --- a/quickshell/Modules/DankBar/Widgets/FocusedApp.qml +++ b/quickshell/Modules/DankBar/Widgets/FocusedApp.qml @@ -56,6 +56,13 @@ BasePill { } } + Connections { + target: SettingsData + function onAppIdSubstitutionsChanged() { + root.updateDesktopEntry(); + } + } + function updateDesktopEntry() { if (activeWindow && activeWindow.appId) { const moddedId = Paths.moddedAppId(activeWindow.appId); diff --git a/quickshell/Modules/DankBar/Widgets/RunningApps.qml b/quickshell/Modules/DankBar/Widgets/RunningApps.qml index a5f61126..926f2c5d 100644 --- a/quickshell/Modules/DankBar/Widgets/RunningApps.qml +++ b/quickshell/Modules/DankBar/Widgets/RunningApps.qml @@ -69,6 +69,7 @@ Item { property int _desktopEntriesUpdateTrigger: 0 property int _toplevelsUpdateTrigger: 0 + property int _appIdSubstitutionsTrigger: 0 readonly property var sortedToplevels: { _toplevelsUpdateTrigger; @@ -95,6 +96,13 @@ Item { _desktopEntriesUpdateTrigger++; } } + + Connections { + target: SettingsData + function onAppIdSubstitutionsChanged() { + _appIdSubstitutionsTrigger++; + } + } readonly property var groupedWindows: { if (!SettingsData.runningAppsGroupByApp) { return []; @@ -364,6 +372,7 @@ Item { height: Theme.barIconSize(root.barThickness) source: { root._desktopEntriesUpdateTrigger; + root._appIdSubstitutionsTrigger; if (!appId) return ""; const moddedId = Paths.moddedAppId(appId); @@ -596,6 +605,7 @@ Item { height: Theme.barIconSize(root.barThickness) source: { root._desktopEntriesUpdateTrigger; + root._appIdSubstitutionsTrigger; if (!appId) return ""; const moddedId = Paths.moddedAppId(appId); diff --git a/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml b/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml index ac5388c1..1cfdc362 100644 --- a/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml +++ b/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml @@ -265,7 +265,8 @@ Item { if (!byApp[key]) { const isQuickshell = keyBase === "org.quickshell"; - const desktopEntry = DesktopEntries.heuristicLookup(keyBase); + const moddedId = Paths.moddedAppId(keyBase); + const desktopEntry = DesktopEntries.heuristicLookup(moddedId); const icon = Paths.getAppIcon(keyBase, desktopEntry); byApp[key] = { "type": "icon", @@ -1367,6 +1368,9 @@ Item { function onWorkspaceNameIconsChanged() { delegateRoot.updateAllData(); } + function onAppIdSubstitutionsChanged() { + delegateRoot.updateAllData(); + } } Connections { target: DwlService diff --git a/quickshell/Modules/Dock/DockAppButton.qml b/quickshell/Modules/Dock/DockAppButton.qml index c5744527..8c0197e6 100644 --- a/quickshell/Modules/Dock/DockAppButton.qml +++ b/quickshell/Modules/Dock/DockAppButton.qml @@ -49,6 +49,13 @@ Item { updateDesktopEntry(); } } + + Connections { + target: SettingsData + function onAppIdSubstitutionsChanged() { + updateDesktopEntry(); + } + } property bool isWindowFocused: { if (!appData) { return false; diff --git a/quickshell/Modules/Settings/RunningAppsTab.qml b/quickshell/Modules/Settings/RunningAppsTab.qml index 49f7c5a2..636f8740 100644 --- a/quickshell/Modules/Settings/RunningAppsTab.qml +++ b/quickshell/Modules/Settings/RunningAppsTab.qml @@ -32,6 +32,159 @@ Item { onToggled: checked => SettingsData.set("runningAppsCurrentWorkspace", checked) } } + + SettingsCard { + width: parent.width + iconName: "find_replace" + title: I18n.tr("App ID Substitutions") + settingKey: "appIdSubstitutions" + tags: ["app", "icon", "substitution", "replacement", "pattern", "window", "class", "regex"] + + headerActions: [ + DankActionButton { + buttonSize: 36 + iconName: "restart_alt" + iconSize: 20 + visible: JSON.stringify(SettingsData.appIdSubstitutions) !== JSON.stringify(SettingsData.getDefaultAppIdSubstitutions()) + backgroundColor: Theme.surfaceContainer + iconColor: Theme.surfaceVariantText + onClicked: SettingsData.resetAppIdSubstitutions() + }, + DankActionButton { + buttonSize: 36 + iconName: "add" + iconSize: 20 + backgroundColor: Theme.surfaceContainer + iconColor: Theme.primary + onClicked: SettingsData.addAppIdSubstitution("", "", "exact") + } + ] + + Column { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Map window class names to icon names for proper icon display") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + bottomPadding: Theme.spacingS + } + + Repeater { + model: SettingsData.appIdSubstitutions + + delegate: Rectangle { + id: subItem + width: parent.width + height: subColumn.implicitHeight + Theme.spacingM + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainer, 0.5) + + Column { + id: subColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingS + + Column { + width: (parent.width - deleteBtn.width - Theme.spacingS) / 2 + spacing: 2 + + StyledText { + text: I18n.tr("Pattern") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + DankTextField { + id: patternField + width: parent.width + text: modelData.pattern + font.pixelSize: Theme.fontSizeSmall + onEditingFinished: SettingsData.updateAppIdSubstitution(index, text, replacementField.text, modelData.type) + } + } + + Column { + width: (parent.width - deleteBtn.width - Theme.spacingS) / 2 + spacing: 2 + + StyledText { + text: I18n.tr("Replacement") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + DankTextField { + id: replacementField + width: parent.width + text: modelData.replacement + font.pixelSize: Theme.fontSizeSmall + onEditingFinished: SettingsData.updateAppIdSubstitution(index, patternField.text, text, modelData.type) + } + } + + Item { + id: deleteBtn + width: 32 + height: 40 + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: deleteArea.containsMouse ? Theme.withAlpha(Theme.error, 0.2) : "transparent" + } + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: deleteArea.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: deleteArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: SettingsData.removeAppIdSubstitution(index) + } + } + } + + Column { + width: 120 + spacing: 2 + + StyledText { + text: I18n.tr("Type") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + DankDropdown { + width: parent.width + compactMode: true + dropdownWidth: 120 + currentValue: modelData.type + options: ["exact", "contains", "regex"] + onValueChanged: value => SettingsData.updateAppIdSubstitution(index, modelData.pattern, modelData.replacement, value) + } + } + } + } + } + + } + } } } } diff --git a/quickshell/Modules/Settings/Widgets/SettingsCard.qml b/quickshell/Modules/Settings/Widgets/SettingsCard.qml index 4d64119b..c4d1c58f 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsCard.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsCard.qml @@ -142,7 +142,7 @@ StyledRect { Row { id: headerActionsRow - anchors.right: caretIcon.left + anchors.right: root.collapsible ? caretIcon.left : parent.right anchors.rightMargin: root.collapsible ? Theme.spacingS : 0 anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingXS @@ -172,6 +172,7 @@ StyledRect { } MouseArea { + visible: root.collapsible anchors.left: caretIcon.left anchors.right: parent.right anchors.top: parent.top diff --git a/quickshell/Modules/WorkspaceOverlays/OverviewWindow.qml b/quickshell/Modules/WorkspaceOverlays/OverviewWindow.qml index 643207c9..43899714 100644 --- a/quickshell/Modules/WorkspaceOverlays/OverviewWindow.qml +++ b/quickshell/Modules/WorkspaceOverlays/OverviewWindow.qml @@ -33,8 +33,8 @@ Item { property var iconToWindowRatio: 0.25 property var iconToWindowRatioCompact: 0.45 - property var entry: DesktopEntries.heuristicLookup(windowData?.class) - property var iconPath: Quickshell.iconPath(entry?.icon ?? windowData?.class ?? "application-x-executable", "image-missing") + property var entry: DesktopEntries.heuristicLookup(Paths.moddedAppId(windowData?.class ?? "")) + property var iconPath: Paths.getAppIcon(windowData?.class ?? "", entry) || Quickshell.iconPath("application-x-executable", "image-missing") property bool compactMode: Theme.fontSizeSmall * 4 > targetWindowHeight || Theme.fontSizeSmall * 4 > targetWindowWidth x: initX diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 9f682b8b..3b88bc83 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -3153,33 +3153,6 @@ "icon": "lock", "description": "If the field is hidden, it will appear as soon as a key is pressed." }, - { - "section": "lockScreenNotificationMode", - "label": "Notification Display", - "tabIndex": 11, - "category": "Lock Screen", - "keywords": [ - "alert", - "control", - "display", - "information", - "lock", - "lockscreen", - "login", - "monitor", - "notif", - "notification", - "notifications", - "output", - "password", - "privacy", - "screen", - "security", - "shown", - "what" - ], - "description": "Control what notification information is shown on the lock screen" - }, { "section": "lockScreenShowPasswordField", "label": "Show Password Field", @@ -3915,6 +3888,35 @@ ], "description": "Timeout for normal priority notifications" }, + { + "section": "lockScreenNotificationMode", + "label": "Notification Display", + "tabIndex": 17, + "category": "Notifications", + "keywords": [ + "alert", + "alerts", + "control", + "display", + "information", + "lock", + "lockscreen", + "login", + "messages", + "monitor", + "notif", + "notification", + "notifications", + "output", + "privacy", + "screen", + "security", + "shown", + "toast", + "what" + ], + "description": "Control what notification information is shown on the lock screen" + }, { "section": "notificationOverlayEnabled", "label": "Notification Overlay", @@ -4036,6 +4038,29 @@ "icon": "tune", "description": "Choose where on-screen displays appear on screen" }, + { + "section": "appIdSubstitutions", + "label": "App ID Substitutions", + "tabIndex": 19, + "category": "Running Apps", + "keywords": [ + "active", + "app", + "apps", + "class", + "icon", + "pattern", + "regex", + "replacement", + "running", + "substitution", + "substitutions", + "tasks", + "window", + "windows" + ], + "icon": "find_replace" + }, { "section": "runningApps", "label": "Running Apps Settings",