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");