feat(globalmenu): got a prototype of working globalmenu for some apps
This commit is contained in:
10 files changed
+930
-123
No files matched your search
@@ -73,8 +73,8 @@ class AppName:
|
||||
name = name.split(".")[-1]
|
||||
|
||||
if update:
|
||||
modus_service.current_active_app_name = name
|
||||
modus_service.current_active_wm_class = wmclass
|
||||
modus_service.current_active_app_name = name
|
||||
return name
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
"""Pure Python DBusMenu extractor using Gio.DBus."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
from gi.repository import GLib, Gio
|
||||
from fabric.utils import logger
|
||||
|
||||
|
||||
@dataclass
|
||||
class DBusMenuItem:
|
||||
id: int
|
||||
label: str = ""
|
||||
enabled: bool = True
|
||||
visible: bool = True
|
||||
type: str = "standard" # standard, separator
|
||||
shortcut: str = ""
|
||||
icon_name: str = ""
|
||||
has_submenu: bool = False
|
||||
children: List["DBusMenuItem"] = field(default_factory=list)
|
||||
|
||||
|
||||
class DBusMenuClient:
|
||||
def __init__(self, service_name: str, object_path: str):
|
||||
self.service_name = service_name
|
||||
self.object_path = object_path
|
||||
self.bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
|
||||
def _call_method(
|
||||
self,
|
||||
method: str,
|
||||
params: Optional[GLib.Variant],
|
||||
reply_type: Optional[GLib.VariantType],
|
||||
) -> Optional[GLib.Variant]:
|
||||
try:
|
||||
return self.bus.call_sync(
|
||||
self.service_name,
|
||||
self.object_path,
|
||||
"com.canonical.dbusmenu",
|
||||
method,
|
||||
params,
|
||||
reply_type,
|
||||
Gio.DBusCallFlags.NONE,
|
||||
1000,
|
||||
None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"[DBusMenuClient] Call to {method} failed: {e}")
|
||||
return None
|
||||
|
||||
def get_layout(
|
||||
self, parent_id: int = 0, recursion_depth: int = -1
|
||||
) -> List[DBusMenuItem]:
|
||||
"""Fetches and parses the full menu layout."""
|
||||
# Signature: iias -> u(ia{sv}av)
|
||||
params = GLib.Variant("(iias)", (parent_id, recursion_depth, []))
|
||||
res = self._call_method("GetLayout", params, None)
|
||||
if not res:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Result is usually a tuple: (u, (i, a{sv}, av)) or (u, v) depending on the DBus implementation
|
||||
# We bypass the strict type by unpacking the child
|
||||
if res.n_children() < 2:
|
||||
return []
|
||||
|
||||
layout_var = res.get_child_value(1)
|
||||
if layout_var.is_of_type(GLib.VariantType("v")):
|
||||
layout_var = layout_var.get_variant()
|
||||
|
||||
return self._parse_node(layout_var).children
|
||||
except Exception as e:
|
||||
logger.error(f"[DBusMenuClient] Error parsing GetLayout: {e}")
|
||||
return []
|
||||
|
||||
def _parse_node(self, node: GLib.Variant) -> DBusMenuItem:
|
||||
"""Parses a layout node (ia{sv}av) into a DBusMenuItem."""
|
||||
item = DBusMenuItem(id=0)
|
||||
|
||||
try:
|
||||
# node is a struct with 3 elements: id (i), properties (a{sv}), children (av)
|
||||
if node.n_children() != 3:
|
||||
return item
|
||||
|
||||
item.id = node.get_child_value(0).get_int32()
|
||||
|
||||
props_dict = node.get_child_value(1).unpack()
|
||||
|
||||
# Map properties
|
||||
item.label = props_dict.get("label", "").replace("_", "")
|
||||
item.enabled = props_dict.get("enabled", True)
|
||||
item.visible = props_dict.get("visible", True)
|
||||
item.type = props_dict.get("type", "standard")
|
||||
item.icon_name = props_dict.get("icon-name", "")
|
||||
|
||||
# Submenus
|
||||
if props_dict.get("children-display") == "submenu":
|
||||
item.has_submenu = True
|
||||
|
||||
# Shortcut parsing (usually a nested list of strings)
|
||||
shortcut_data = props_dict.get("shortcut")
|
||||
if (
|
||||
shortcut_data
|
||||
and isinstance(shortcut_data, list)
|
||||
and len(shortcut_data) > 0
|
||||
):
|
||||
parts = []
|
||||
for s in shortcut_data[0]:
|
||||
if s == "Control":
|
||||
parts.append("Ctrl")
|
||||
elif s == "Shift":
|
||||
parts.append("Shift")
|
||||
elif s == "Alt":
|
||||
parts.append("Alt")
|
||||
else:
|
||||
parts.append(s.upper())
|
||||
item.shortcut = "+".join(parts)
|
||||
|
||||
# Parse children
|
||||
children_array = node.get_child_value(2)
|
||||
for i in range(children_array.n_children()):
|
||||
child_var = children_array.get_child_value(i)
|
||||
if child_var.is_of_type(GLib.VariantType("v")):
|
||||
child_var = child_var.get_variant()
|
||||
child_item = self._parse_node(child_var)
|
||||
if child_item.visible:
|
||||
item.children.append(child_item)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"[DBusMenuClient] Error parsing node: {e}")
|
||||
|
||||
return item
|
||||
|
||||
def about_to_show(self, item_id: int) -> bool:
|
||||
"""Tells the app to populate a dynamic submenu."""
|
||||
# Signature: i -> b
|
||||
res = self._call_method(
|
||||
"AboutToShow", GLib.Variant("(i)", (item_id,)), GLib.VariantType("(b)")
|
||||
)
|
||||
if res:
|
||||
return res.get_child_value(0).get_boolean()
|
||||
return False
|
||||
|
||||
def click_item(self, item_id: int) -> None:
|
||||
"""Triggers the activation event on a menu item."""
|
||||
# Signature: isvu -> nothing
|
||||
# Event(id, eventId, data, timestamp)
|
||||
params = GLib.Variant("(isvu)", (item_id, "clicked", GLib.Variant("s", ""), 0))
|
||||
self._call_method("Event", params, None)
|
||||
Reference in new issue
Block a user