From a7da11767e4e865e3dfbb22bdf560dace5c94951 Mon Sep 17 00:00:00 2001 From: Sohail Date: Mon, 27 Apr 2026 16:38:31 +0200 Subject: [PATCH] Add manual StartupWMClass detection button for Chromium web apps --- usr/lib/webapp-manager/common.py | 30 +++++- usr/lib/webapp-manager/webapp-manager.py | 111 +++++++++++++++++++++ usr/share/webapp-manager/webapp-manager.ui | 21 ++++ 3 files changed, 159 insertions(+), 3 deletions(-) diff --git a/usr/lib/webapp-manager/common.py b/usr/lib/webapp-manager/common.py index 58a6b80..db7cab3 100644 --- a/usr/lib/webapp-manager/common.py +++ b/usr/lib/webapp-manager/common.py @@ -92,14 +92,18 @@ def __init__(self, path, codename): self.navbar = False self.privatewindow = False - is_webapp = False + has_startup_wmclass = False + has_webapp_metadata = False + has_legacy_webapp_wmclass = False with open(path) as desktop_file: for line in desktop_file: line = line.strip() # Identify if the app is a webapp - if "StartupWMClass=WebApp" in line or "StartupWMClass=Chromium" in line or "StartupWMClass=ICE-SSB" in line: - is_webapp = True + if line.startswith("StartupWMClass="): + has_startup_wmclass = True + if "StartupWMClass=WebApp" in line or "StartupWMClass=Chromium" in line or "StartupWMClass=ICE-SSB" in line: + has_legacy_webapp_wmclass = True continue if "Name=" in line: @@ -125,29 +129,36 @@ def __init__(self, path, codename): continue if "X-WebApp-Browser=" in line: + has_webapp_metadata = True self.web_browser = line.replace("X-WebApp-Browser=", "") continue if "X-WebApp-URL=" in line: + has_webapp_metadata = True self.url = line.replace("X-WebApp-URL=", "") continue if "X-WebApp-CustomParameters" in line: + has_webapp_metadata = True self.custom_parameters = line.replace("X-WebApp-CustomParameters=", "") continue if "X-WebApp-Isolated" in line: + has_webapp_metadata = True self.isolate_profile = line.replace("X-WebApp-Isolated=", "").lower() == "true" continue if "X-WebApp-Navbar" in line: + has_webapp_metadata = True self.navbar = line.replace("X-WebApp-Navbar=", "").lower() == "true" continue if "X-WebApp-PrivateWindow" in line: + has_webapp_metadata = True self.privatewindow = line.replace("X-WebApp-PrivateWindow=", "").lower() == "true" continue + is_webapp = has_legacy_webapp_wmclass or (has_startup_wmclass and has_webapp_metadata) if is_webapp and self.name is not None and self.icon is not None: self.is_valid = True @@ -469,6 +480,19 @@ def edit_webapp(self, path, name, desc, browser, url, icon, category, custom_par with open(path, 'w') as configfile: config.write(configfile, space_around_delimiters=False) + def update_startup_wmclass(self, path, startup_wmclass): + config = configparser.RawConfigParser() + config.optionxform = str + config.read(path) + + if not config.has_section("Desktop Entry"): + return + + config.set("Desktop Entry", "StartupWMClass", startup_wmclass) + + with open(path, 'w') as configfile: + config.write(configfile, space_around_delimiters=False) + def bool_to_string(boolean): if boolean: return "true" diff --git a/usr/lib/webapp-manager/webapp-manager.py b/usr/lib/webapp-manager/webapp-manager.py index d6aeb3e..d46dd68 100755 --- a/usr/lib/webapp-manager/webapp-manager.py +++ b/usr/lib/webapp-manager/webapp-manager.py @@ -4,6 +4,7 @@ import gettext import locale import os +import re import shutil import subprocess import warnings @@ -64,6 +65,7 @@ def __init__(self, application): self.settings = Gio.Settings(schema_id="org.x.webapp-manager") self.manager = WebAppManager() self.selected_webapp = None + self.detecting_dialog = None self.icon_theme = Gtk.IconTheme.get_default() # Set the Glade file @@ -87,6 +89,7 @@ def __init__(self, application): self.remove_button = self.builder.get_object("remove_button") self.edit_button = self.builder.get_object("edit_button") self.run_button = self.builder.get_object("run_button") + self.run_detect_button = self.builder.get_object("run_detect_button") self.ok_button = self.builder.get_object("ok_button") self.name_entry = self.builder.get_object("name_entry") self.desc_entry = self.builder.get_object("desc_entry") @@ -114,6 +117,7 @@ def __init__(self, application): self.remove_button.connect("clicked", self.on_remove_button) self.edit_button.connect("clicked", self.on_edit_button) self.run_button.connect("clicked", self.on_run_button) + self.run_detect_button.connect("clicked", self.on_run_detect_button) self.ok_button.connect("clicked", self.on_ok_button) self.favicon_button.connect("clicked", self.on_favicon_button) self.name_entry.connect("changed", self.on_name_entry) @@ -267,6 +271,7 @@ def on_webapp_selected(self, selection): self.remove_button.set_sensitive(True) self.edit_button.set_sensitive(True) self.run_button.set_sensitive(True) + self.run_detect_button.set_sensitive(True) def on_webapp_activated(self, treeview, path, column): self.run_webapp(self.selected_webapp) @@ -306,6 +311,111 @@ def run_webapp(self, webapp): def on_run_button(self, widget): self.run_webapp(self.selected_webapp) + def on_run_detect_button(self, widget): + if self.selected_webapp is None: + return + + self.run_webapp(self.selected_webapp) + self.detect_startup_wmclass(self.selected_webapp) + + @_async + def detect_startup_wmclass(self, webapp): + if webapp is None: + return + + self.show_detecting_popup() + + if shutil.which("xprop") is None: + self.close_detecting_popup() + self.show_info_message( + _("xprop Not Found"), + _("The 'xprop' command is not installed."), + _("Install xprop (x11-utils) to use WM_CLASS detection.")) + return + + try: + result = subprocess.run(["xprop", "WM_CLASS"], capture_output=True, text=True) + except Exception as e: + self.close_detecting_popup() + self.show_info_message( + _("WM_CLASS Detection Failed"), + str(e), + None) + return + + if result.returncode != 0: + message = result.stderr.strip() if result.stderr else _("xprop returned an error.") + self.close_detecting_popup() + self.show_info_message( + _("WM_CLASS Detection Failed"), + message, + None) + return + + wmclass_match = re.search(r'WM_CLASS\(STRING\)\s*=\s*"([^"]+)"', result.stdout) + if wmclass_match is None: + self.close_detecting_popup() + self.show_info_message( + _("WM_CLASS Detection Failed"), + _("Could not parse WM_CLASS output."), + result.stdout.strip()) + return + + startup_wmclass = wmclass_match.group(1) + self.manager.update_startup_wmclass(webapp.path, startup_wmclass) + self.on_startup_wmclass_updated(startup_wmclass) + + @idle + def show_detecting_popup(self): + if self.detecting_dialog is not None: + return + + dialog = Gtk.MessageDialog( + transient_for=self.window, + flags=0, + message_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.NONE, + text=_("Detecting StartupWMClass")) + dialog.format_secondary_text( + _("Click the launched web app window to capture WM_CLASS.\n\nIf you click another window, the wrong class may be saved.")) + dialog.set_modal(False) + dialog.show_all() + self.detecting_dialog = dialog + + @idle + def close_detecting_popup(self): + if self.detecting_dialog is not None: + self.detecting_dialog.destroy() + self.detecting_dialog = None + + @idle + def show_info_message(self, title, text, secondary_text=None): + dialog = Gtk.MessageDialog( + transient_for=self.window, + flags=0, + message_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.OK, + text=title) + dialog.format_secondary_text(text) + if secondary_text: + dialog.format_secondary_text("%s\n\n%s" % (text, secondary_text)) + dialog.run() + dialog.destroy() + + @idle + def on_startup_wmclass_updated(self, startup_wmclass): + self.load_webapps() + dialog = Gtk.MessageDialog( + transient_for=self.window, + flags=0, + message_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.OK, + text=_("StartupWMClass Updated")) + dialog.format_secondary_text(_("StartupWMClass was set to '%s'.") % startup_wmclass) + dialog.run() + dialog.destroy() + self.close_detecting_popup() + def on_ok_button(self, widget): category = self.category_combo.get_model()[self.category_combo.get_active()][CATEGORY_ID] browser = self.browser_combo.get_model()[self.browser_combo.get_active()][BROWSER_OBJ] @@ -514,6 +624,7 @@ def load_webapps(self): self.remove_button.set_sensitive(False) self.edit_button.set_sensitive(False) self.run_button.set_sensitive(False) + self.run_detect_button.set_sensitive(False) webapps = self.manager.get_webapps() for webapp in webapps: diff --git a/usr/share/webapp-manager/webapp-manager.ui b/usr/share/webapp-manager/webapp-manager.ui index f08bd9f..ae75f03 100644 --- a/usr/share/webapp-manager/webapp-manager.ui +++ b/usr/share/webapp-manager/webapp-manager.ui @@ -150,6 +150,27 @@ 3 + + + True + False + False + False + Run and detect StartupWMClass (click the launched app window) + + + True + False + applications-engineering-symbolic + + + + + False + True + 4 + +