From 2afbe0522921a76b154c29341a347768ca7e4884 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 23 Jun 2026 14:28:47 +0545 Subject: [PATCH] feat(src/utils): add ActionMenuClient and depth limits for menu parsing Add flat GTK Action handling and guard DBus menu depth to prevent recursion and overflow. --- src/utils/dbusmenu.py | 41 ++++++++++++---------- src/utils/gtkmenu.py | 81 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 19 deletions(-) diff --git a/src/utils/dbusmenu.py b/src/utils/dbusmenu.py index 8dfdebf..9fdc394 100644 --- a/src/utils/dbusmenu.py +++ b/src/utils/dbusmenu.py @@ -171,7 +171,9 @@ class DBusMenuClient: return str(hash("".join(flat))) # DBUSMENU PARSER - def _parse_dbusmenu(self, node): + def _parse_dbusmenu(self, node, _depth: int = 0): + if _depth > 20: + return DBusMenuItem(id=0) item = DBusMenuItem(id=0) try: @@ -214,10 +216,12 @@ class DBusMenuClient: children = node.get_child_value(2) for i in range(children.n_children()): + if _depth >= 20: + break c = children.get_child_value(i) if c.is_of_type(GLib.VariantType("v")): c = c.get_variant() - item.children.append(self._parse_dbusmenu(c)) + item.children.append(self._parse_dbusmenu(c, _depth + 1)) if item.has_submenu and not item.children: try: @@ -248,25 +252,26 @@ class DBusMenuClient: except Exception: return None - def _parse_gtk_menu(self, variant): + def _parse_gtk_menu(self, variant, _depth: int = 0): + if _depth > 10: + return [] items = [] try: - if not variant: + if not variant or not variant.is_container(): return [] - if variant.is_container(): - for i in range(variant.n_children()): - c = variant.get_child_value(i) - item = DBusMenuItem(id=i) - try: - if c.n_children() > 0: - item.label = c.get_child_value(0).get_string() - except Exception: - pass - if c.n_children() > 1: - sub = c.get_child_value(c.n_children() - 1) - item.children = self._parse_gtk_menu(sub) - item.has_submenu = bool(item.children) - items.append(item) + for i in range(variant.n_children()): + c = variant.get_child_value(i) + item = DBusMenuItem(id=i) + try: + if c.n_children() > 0: + item.label = c.get_child_value(0).get_string() + except Exception: + pass + if c.n_children() > 1: + sub = c.get_child_value(c.n_children() - 1) + item.children = self._parse_gtk_menu(sub, _depth + 1) + item.has_submenu = bool(item.children) + items.append(item) except Exception: pass return items diff --git a/src/utils/gtkmenu.py b/src/utils/gtkmenu.py index 0fa574c..917fcdb 100644 --- a/src/utils/gtkmenu.py +++ b/src/utils/gtkmenu.py @@ -1,5 +1,5 @@ import re -from typing import List, Optional +from typing import Dict, List, Optional from fabric.utils import Gio, GLib, logger @@ -172,3 +172,82 @@ class GtkMenuClient: continue except Exception as e: logger.error(f"[GtkMenuClient] click failed: {e}") + + +class ActionMenuClient: + """Flat menu from org.gtk.Actions when org.gtk.Menus is unavailable.""" + + __slots__ = ("service_name", "object_path", "_cached_items", "_action_map") + + def __init__(self, service_name: str, object_path: str): + self.service_name = service_name + self.object_path = object_path + self._cached_items: List[DBusMenuItem] = [] + self._action_map: Dict[int, str] = {} + + def _call(self, interface, method, params, reply_type=None): + try: + return _get_bus().call_sync( + self.service_name, + self.object_path, + interface, + method, + params, + reply_type, + Gio.DBusCallFlags.NONE, + 3000, + None, + ) + except Exception: + return None + + def get_layout(self, parent_id: int = 0) -> List[DBusMenuItem]: + if parent_id != 0: + return [] + res = self._call( + "org.gtk.Actions", + "DescribeAll", + None, + GLib.VariantType("(a{s(bgav)})"), + ) + if not res: + return self._cached_items + + try: + data = res.get_child_value(0).unpack() + items: List[DBusMenuItem] = [] + self._action_map = {} + for i, (name, (enabled, param_type, state)) in enumerate(data.items()): + label = name.replace("-", " ").replace("_", " ").strip().title() + item = DBusMenuItem(id=hash((0, i))) + item.action_name = name + # Hide actions that need parameters — they can't work in a flat menu + item.visible = not bool(param_type) + item.enabled = enabled if not param_type else False + item.label = label + self._action_map[item.id] = name + items.append(item) + self._cached_items = items + return items + except Exception as e: + logger.error(f"[ActionMenuClient] parse error: {e}") + return self._cached_items + + def about_to_show(self, item_id: int) -> bool: + return False + + def click_item( + self, item_id: int, action_name: str = "", action_target=None + ) -> None: + if not action_name: + action_name = self._action_map.get(item_id, "") + if not action_name: + return + try: + self._call( + "org.gtk.Actions", + "Activate", + GLib.Variant("(sava{sv})", (action_name, [], {})), + ) + except Exception as e: + logger.error(f"[ActionMenuClient] click failed: {e}")