From 27bba515952b8e5a82acfde2317e1e443e52cbc1 Mon Sep 17 00:00:00 2001 From: Twilight0 Date: Tue, 23 Jun 2026 21:07:55 +0300 Subject: [PATCH 1/2] grouped-window-list: Match windows against pinned apps to prevent duplicate panel icons When Cinnamon's WindowTracker fails to associate a running window with a `.desktop` launcher (due to missing `StartupWMClass` declarations, case mismatches, or process wrappers common in Electron/wrapper apps), the Grouped Window List (GWL) applet creates a new transient launcher on the panel instead of nesting the app under the existing pinned launcher. This change enhances `getAppFromWindow` by proactively checking the user's pinned favorites array first. It checks for a match between the window's WM_CLASS (both class and instance name) or GTK application ID, and the pinned application properties: 1. Pinned launcher desktop file base name (case-insensitive) 2. Desktop file's `StartupWMClass` key 3. Pinned application command line/executable binary Additionally, this fixes a potential early-startup TypeError crash (`TypeError: currentWorkspace is null`) in `onWindowSkipTaskbarChanged` by inserting a null-check check at the entry point. --- .../applet.js | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index 22303a1360..2ffeeeaae0 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -639,7 +639,80 @@ class GroupedWindowListApplet extends Applet.Applet { this.state.set({lastTitleDisplay: titleDisplay}); } + _matchWindowToPinnedApp(metaWindow) { + if (!this.pinnedFavorites || !this.pinnedFavorites._favorites) { + return null; + } + + let wmClass = metaWindow.get_wm_class(); + let wmInstance = metaWindow.get_wm_class_instance(); + let gtkAppId = metaWindow.get_gtk_application_id ? metaWindow.get_gtk_application_id() : null; + + wmClass = wmClass ? wmClass.toLowerCase() : ""; + wmInstance = wmInstance ? wmInstance.toLowerCase() : ""; + gtkAppId = gtkAppId ? gtkAppId.toLowerCase() : ""; + + // Iterate through pinned favorites + for (let fav of this.pinnedFavorites._favorites) { + if (!fav || !fav.app) continue; + + let favId = fav.id.toLowerCase(); + // Get base name without path and .desktop + let baseName = favId.substring(favId.lastIndexOf('/') + 1).replace('.desktop', ''); + + // 1. Direct match on desktop ID base name (case-insensitive) + if (baseName === wmClass || baseName === wmInstance || (gtkAppId && baseName === gtkAppId)) { + return fav.app; + } + + // 2. Match on StartupWMClass from the app info + let appInfo = fav.app.get_app_info(); + if (appInfo) { + let startupClass = appInfo.get_startup_wm_class ? appInfo.get_startup_wm_class() : null; + if (!startupClass) { + try { + startupClass = appInfo.get_string("StartupWMClass"); + } catch (e) {} + } + if (startupClass) { + startupClass = startupClass.toLowerCase(); + if (startupClass === wmClass || startupClass === wmInstance) { + return fav.app; + } + } + + // 3. Match executable/command name + let exec = appInfo.get_executable ? appInfo.get_executable() : null; + if (!exec) { + try { + let cmd = appInfo.get_commandline ? appInfo.get_commandline() : ""; + if (cmd) { + exec = cmd.split(' ')[0]; + } + } catch (e) {} + } + if (exec) { + let execBase = exec.substring(exec.lastIndexOf('/') + 1).toLowerCase(); + if (execBase === wmClass || execBase === wmInstance) { + return fav.app; + } + } + } + } + return null; + } + getAppFromWindow(metaWindow) { + if (!metaWindow) { + return null; + } + + // Try matching against pinned favorites first to prevent duplicate icons + let pinnedMatch = this._matchWindowToPinnedApp(metaWindow); + if (pinnedMatch) { + return pinnedMatch; + } + const tracker = this.state.trigger('getTracker'); if (!tracker) { return null; @@ -651,6 +724,20 @@ class GroupedWindowListApplet extends Applet.Applet { if (!app) { app = tracker.get_app_from_pid(metaWindow.get_client_pid()); } + + // Fallback: If the tracker returned a transient app, check if its ID matches a pinned favorite + if (app && this.pinnedFavorites && this.pinnedFavorites._favorites) { + let appId = app.get_id().toLowerCase(); + for (let fav of this.pinnedFavorites._favorites) { + if (!fav || !fav.app) continue; + let favId = fav.id.toLowerCase(); + let baseName = favId.substring(favId.lastIndexOf('/') + 1).replace('.desktop', ''); + if (appId === baseName || appId === favId) { + return fav.app; + } + } + } + return app; } @@ -1030,6 +1117,9 @@ class GroupedWindowListApplet extends Applet.Applet { onWindowSkipTaskbarChanged(display, metaWindow) { const currentWorkspace = this.getCurrentWorkspace(); + if (!currentWorkspace) { + return; + } if (metaWindow.is_skip_taskbar()) { currentWorkspace.windowRemoved(currentWorkspace.metaWorkspace, metaWindow); From 10a6a042a06f717d952eae1cdcaa518e48b0052f Mon Sep 17 00:00:00 2001 From: Twilight0 Date: Tue, 23 Jun 2026 21:16:30 +0300 Subject: [PATCH 2/2] grouped-window-list: Refactor matching logic to reduce cyclomatic complexity --- .../applet.js | 93 ++++++++++--------- 1 file changed, 51 insertions(+), 42 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index 2ffeeeaae0..d86f95c9fd 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -639,6 +639,56 @@ class GroupedWindowListApplet extends Applet.Applet { this.state.set({lastTitleDisplay: titleDisplay}); } + _matchFavoriteToWindow(fav, wmClass, wmInstance, gtkAppId) { + if (!fav || !fav.app) { + return false; + } + + let favId = fav.id.toLowerCase(); + // Get base name without path and .desktop + let baseName = favId.substring(favId.lastIndexOf('/') + 1).replace('.desktop', ''); + + // 1. Direct match on desktop ID base name (case-insensitive) + if (baseName === wmClass || baseName === wmInstance || (gtkAppId && baseName === gtkAppId)) { + return true; + } + + // 2. Match on StartupWMClass from the app info + let appInfo = fav.app.get_app_info(); + if (appInfo) { + let startupClass = appInfo.get_startup_wm_class ? appInfo.get_startup_wm_class() : null; + if (!startupClass) { + try { + startupClass = appInfo.get_string("StartupWMClass"); + } catch (e) {} + } + if (startupClass) { + startupClass = startupClass.toLowerCase(); + if (startupClass === wmClass || startupClass === wmInstance) { + return true; + } + } + + // 3. Match executable/command name + let exec = appInfo.get_executable ? appInfo.get_executable() : null; + if (!exec) { + try { + let cmd = appInfo.get_commandline ? appInfo.get_commandline() : ""; + if (cmd) { + exec = cmd.split(' ')[0]; + } + } catch (e) {} + } + if (exec) { + let execBase = exec.substring(exec.lastIndexOf('/') + 1).toLowerCase(); + if (execBase === wmClass || execBase === wmInstance) { + return true; + } + } + } + return false; + } + _matchWindowToPinnedApp(metaWindow) { if (!this.pinnedFavorites || !this.pinnedFavorites._favorites) { return null; @@ -654,50 +704,9 @@ class GroupedWindowListApplet extends Applet.Applet { // Iterate through pinned favorites for (let fav of this.pinnedFavorites._favorites) { - if (!fav || !fav.app) continue; - - let favId = fav.id.toLowerCase(); - // Get base name without path and .desktop - let baseName = favId.substring(favId.lastIndexOf('/') + 1).replace('.desktop', ''); - - // 1. Direct match on desktop ID base name (case-insensitive) - if (baseName === wmClass || baseName === wmInstance || (gtkAppId && baseName === gtkAppId)) { + if (this._matchFavoriteToWindow(fav, wmClass, wmInstance, gtkAppId)) { return fav.app; } - - // 2. Match on StartupWMClass from the app info - let appInfo = fav.app.get_app_info(); - if (appInfo) { - let startupClass = appInfo.get_startup_wm_class ? appInfo.get_startup_wm_class() : null; - if (!startupClass) { - try { - startupClass = appInfo.get_string("StartupWMClass"); - } catch (e) {} - } - if (startupClass) { - startupClass = startupClass.toLowerCase(); - if (startupClass === wmClass || startupClass === wmInstance) { - return fav.app; - } - } - - // 3. Match executable/command name - let exec = appInfo.get_executable ? appInfo.get_executable() : null; - if (!exec) { - try { - let cmd = appInfo.get_commandline ? appInfo.get_commandline() : ""; - if (cmd) { - exec = cmd.split(' ')[0]; - } - } catch (e) {} - } - if (exec) { - let execBase = exec.substring(exec.lastIndexOf('/') + 1).toLowerCase(); - if (execBase === wmClass || execBase === wmInstance) { - return fav.app; - } - } - } } return null; }