From 64b15b95623511dd7978326c6bcc24856edf814b Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Fri, 4 Sep 2026 14:16:27 +0200 Subject: [PATCH] fix: improve DBusMenu compatibility on XFCE --- Makefile | 2 +- fluxtuner/gui/tray/linux_sni.py | 106 +++++++++++++++++++++++++++++--- pyproject.toml | 1 + requirements-dev.txt | 4 +- tests/test_gtk_tray_sni.py | 7 +++ tests/test_web_js_modules.py | 1 + 6 files changed, 107 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index bdeabba..f01ec66 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,6 @@ gate: python -m compileall fluxtuner tests python -m pytest mypy --follow-imports=skip fluxtuner/ - node --check fluxtuner/web/static/app.js + node --input-type=module --check < fluxtuner/web/static/app.js pip-audit --local bandit -r fluxtuner -c pyproject.toml diff --git a/fluxtuner/gui/tray/linux_sni.py b/fluxtuner/gui/tray/linux_sni.py index a7989ee..6db9d88 100644 --- a/fluxtuner/gui/tray/linux_sni.py +++ b/fluxtuner/gui/tray/linux_sni.py @@ -57,16 +57,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -251,13 +282,41 @@ def _menu_item_properties(self, item_id: int) -> dict[str, Any]: } return {} + def _menu_properties_for_item( + self, + item_id: int, + property_names: list[str] | tuple[str, ...] | None = None, + ) -> dict[str, Any]: + _gio, GLib = self._gio_glib() + if item_id == 0: + properties = {"children-display": GLib.Variant("s", "submenu")} + else: + properties = self._menu_item_properties(item_id) + + if not property_names: + return properties + return {name: value for name, value in properties.items() if name in property_names} + def _menu_layout(self): _gio, GLib = self._gio_glib() children = [ - GLib.Variant("(ia{sv}av)", (item_id, self._menu_item_properties(item_id), [])) + GLib.Variant( + "(ia{sv}av)", + (item_id, self._menu_properties_for_item(item_id), []), + ) for item_id in (1, 2, 3, 4, 5, 6) ] - return (0, {"children-display": GLib.Variant("s", "submenu")}, children) + return (0, self._menu_properties_for_item(0), children) + + def _dispatch_menu_event(self, item_id: int, event_id: str) -> None: + if event_id != "clicked": + return + if item_id == 3 and self._on_show is not None: + self._on_show() + elif item_id == 4 and self._on_stop is not None: + self._on_stop() + elif item_id == 6 and self._on_quit is not None: + self._on_quit() def _on_method_call( self, @@ -295,21 +354,48 @@ def _on_menu_method_call( ) -> None: _gio, GLib = self._gio_glib() if method_name == "GetLayout": + _parent_id, _recursion_depth, _property_names = parameters.unpack() invocation.return_value(GLib.Variant("(u(ia{sv}av))", (1, self._menu_layout()))) return + if method_name == "GetGroupProperties": + item_ids, property_names = parameters.unpack() + properties = [ + ( + item_id, + self._menu_properties_for_item(item_id, property_names), + ) + for item_id in item_ids + if item_id == 0 or 1 <= item_id <= 6 + ] + invocation.return_value(GLib.Variant("(a(ia{sv}))", (properties,))) + return + if method_name == "GetProperty": + item_id, property_name = parameters.unpack() + value = self._menu_properties_for_item(item_id).get(property_name) + if value is None: + invocation.return_dbus_error( + "com.canonical.dbusmenu.Error.UnknownProperty", + f"Unknown menu property {property_name!r} for item {item_id}", + ) + return + invocation.return_value(GLib.Variant("(v)", (value,))) + return if method_name == "Event": item_id, event_id, _data, _timestamp = parameters.unpack() - if event_id == "clicked": - if item_id == 3 and self._on_show is not None: - self._on_show() - elif item_id == 4 and self._on_stop is not None: - self._on_stop() - elif item_id == 6 and self._on_quit is not None: - self._on_quit() + self._dispatch_menu_event(item_id, event_id) invocation.return_value(None) return + if method_name == "EventGroup": + events = parameters.unpack()[0] + for item_id, event_id, _data, _timestamp in events: + self._dispatch_menu_event(item_id, event_id) + invocation.return_value(GLib.Variant("(ai)", ([],))) + return if method_name == "AboutToShow": - invocation.return_value(GLib.Variant("(b)", (True,))) + invocation.return_value(GLib.Variant("(b)", (False,))) + return + if method_name == "AboutToShowGroup": + invocation.return_value(GLib.Variant("(aiai)", ([], []))) return invocation.return_value(None) diff --git a/pyproject.toml b/pyproject.toml index 7e3f204..a38449f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ web = [ ] dev = [ "argon2-cffi>=25.1.0,<26", + "bandit>=1.8,<2", "build>=1.2.0,<2", "fastapi>=0.115,<1", "jinja2>=3.1,<4", diff --git a/requirements-dev.txt b/requirements-dev.txt index 113b1af..aefbcb6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1 @@ --e . -ruff>=0.6.0 -build>=1.2.0 +-e .[dev] diff --git a/tests/test_gtk_tray_sni.py b/tests/test_gtk_tray_sni.py index acb8de7..e844abd 100644 --- a/tests/test_gtk_tray_sni.py +++ b/tests/test_gtk_tray_sni.py @@ -39,8 +39,15 @@ def test_sni_menu_contract_is_exposed() -> None: assert MENU_OBJECT_PATH == "/MenuBar" assert DBUSMENU_INTERFACE == "com.canonical.dbusmenu" assert 'method name="GetLayout"' in MENU_INTROSPECTION_XML + assert 'method name="GetGroupProperties"' in MENU_INTROSPECTION_XML + assert 'method name="GetProperty"' in MENU_INTROSPECTION_XML assert 'method name="Event"' in MENU_INTROSPECTION_XML + assert 'method name="EventGroup"' in MENU_INTROSPECTION_XML assert 'method name="AboutToShow"' in MENU_INTROSPECTION_XML + assert 'method name="AboutToShowGroup"' in MENU_INTROSPECTION_XML + assert 'signal name="ItemsPropertiesUpdated"' in MENU_INTROSPECTION_XML + assert 'signal name="LayoutUpdated"' in MENU_INTROSPECTION_XML + assert 'signal name="ItemActivationRequested"' in MENU_INTROSPECTION_XML def test_sni_accepts_show_and_quit_callbacks() -> None: diff --git a/tests/test_web_js_modules.py b/tests/test_web_js_modules.py index 4c24625..85cee62 100644 --- a/tests/test_web_js_modules.py +++ b/tests/test_web_js_modules.py @@ -1545,6 +1545,7 @@ class FakeDocument { }, windowRef, documentRef, + navigatorRef: {}, }); controller.setStation("https://example.com/live", "Flux FM");