From 0195971eba64b106501fb674c516d07963ce7a7a Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Wed, 29 Jul 2026 10:08:20 +0545 Subject: [PATCH] fix(globalmenu): fetch Firefox/Zen bookmarks from SQLite when DBus empty Zen Browser doesn't expose bookmark submenu children through DBusMenu. Fall back to reading places.sqlite for Bookmarks Toolbar and Other Bookmarks folders. Truncate to 25 items to avoid menu overflow. --- src/window/globalmenu/dbusmenu.py | 8 +- src/window/panel/components/globalmenu.py | 169 +++++++++++++++++++--- 2 files changed, 148 insertions(+), 29 deletions(-) diff --git a/src/window/globalmenu/dbusmenu.py b/src/window/globalmenu/dbusmenu.py index 0f24a3c..f661b4b 100644 --- a/src/window/globalmenu/dbusmenu.py +++ b/src/window/globalmenu/dbusmenu.py @@ -233,9 +233,7 @@ class DBusMenuClient: if item.has_submenu and not item.children: try: self.about_to_show(item.id) - child_params = GLib.Variant( - "(iias)", (item.id, self._revision, []) - ) + child_params = GLib.Variant("(iias)", (item.id, self._revision, [])) child_res = self._call("GetLayout", child_params) if child_res: child_layout = child_res.get_child_value(1) @@ -247,9 +245,7 @@ class DBusMenuClient: if c.is_of_type(GLib.VariantType("v")): c = c.get_variant() item.children.append( - self._parse_dbusmenu( - c, _depth + 1, item.id - ) + self._parse_dbusmenu(c, _depth + 1, item.id) ) except Exception as e: logger.warning( diff --git a/src/window/panel/components/globalmenu.py b/src/window/panel/components/globalmenu.py index 8b4005b..e95cf9f 100644 --- a/src/window/panel/components/globalmenu.py +++ b/src/window/panel/components/globalmenu.py @@ -1,3 +1,8 @@ +import os +import shutil +import sqlite3 +import tempfile + from fabric.hyprland.widgets import HyprlandActiveWindow as ActiveWindow from fabric.utils import FormattedString, GLib, exec_shell_command_async, logger from fabric.widgets.box import Box @@ -95,6 +100,121 @@ def _clean_label(label): return label.replace("_", "") +def _open_url(url): + exec_shell_command_async(f"xdg-open {url}", lambda *_: None) + + +def _get_firefox_bookmarks(pid, folder_label): + """Read Firefox/ZeN bookmarks from local SQLite database.""" + try: + exe = os.path.realpath(f"/proc/{pid}/exe") + is_zen = "zen" in exe.lower() + is_ff = "firefox" in exe.lower() or "firefox" in exe + if not (is_zen or is_ff): + return None + config_dir = os.path.expanduser( + "~/.config/zen" if is_zen else "~/.mozilla/firefox" + ) + profiles_ini = os.path.join(config_dir, "profiles.ini") + if not os.path.exists(profiles_ini): + return None + profile_path = None + with open(profiles_ini) as f: + for line in f: + line = line.strip() + if line.startswith("Default=") and "1" in line: + profile_path = None + if line.startswith("Path="): + profile_path = line.split("=", 1)[1].strip() + if not profile_path: + return None + if not os.path.isabs(profile_path): + profile_path = os.path.join(config_dir, profile_path) + places = os.path.join(profile_path, "places.sqlite") + if not os.path.exists(places): + return None + folder_map = {"Bookmarks Toolbar": "toolbar", "Other Bookmarks": "unfiled"} + sqlite_name = folder_map.get(folder_label) + if not sqlite_name: + return None + tmp = os.path.join(tempfile.gettempdir(), f"places_{pid}.sqlite") + try: + shutil.copy2(places, tmp) + except Exception: + return None + try: + conn = sqlite3.connect(tmp) + cur = conn.execute( + "SELECT id FROM moz_bookmarks WHERE title=? AND type=2", + (sqlite_name,), + ) + row = cur.fetchone() + if not row: + return None + parent_id = row[0] + cur = conn.execute( + "SELECT b.title, p.url FROM moz_bookmarks b " + "JOIN moz_places p ON b.fk = p.id " + "WHERE b.parent=? AND b.type=1", + (parent_id,), + ) + items = [] + for title, url in cur: + label = title if title else url + items.append( + type( + "", + (), + { + "_url": url, + "label": label, + "visible": True, + "enabled": True, + "type": "standard", + "has_submenu": False, + "children": [], + "id": 0, + }, + )() + ) + conn.close() + return items if items else None + finally: + try: + os.unlink(tmp) + except Exception: + pass + except Exception as e: + logger.warning(f"[GlobalMenu] bookmarks fallback failed: {e}") + return None + + +TRUNCATE_MENU_AT = 25 + + +def _truncate_children(children): + if len(children) <= TRUNCATE_MENU_AT: + return children + truncated = list(children[:TRUNCATE_MENU_AT]) + remaining = len(children) - TRUNCATE_MENU_AT + more = type( + "", + (), + { + "_url": None, + "label": f"\u2192 {remaining} more bookmarks\u2026", + "visible": True, + "enabled": False, + "type": "standard", + "has_submenu": False, + "children": [], + "id": 0, + }, + )() + truncated.append(more) + return truncated + + def _on_item_select(item, svc, child, click_handler): if getattr(item, "_lazy_loaded", False): return @@ -105,25 +225,28 @@ def _on_item_select(item, svc, child, click_handler): ) svc.about_to_show(child.id) entry, _ = svc._resolve_current_entry() + children = None if entry and entry.importer and hasattr(entry.importer, "update_layout"): - new_children = entry.importer.update_layout( + children = entry.importer.update_layout( child.id, getattr(entry, "revision", 0) ) - if new_children: - logger.info( - f"[GlobalMenu] got {len(new_children)} children for id={child.id}: " - f"{[getattr(c, 'label', '?') for c in new_children[:5]]}" - ) - new_menu = _build_gtk_menu_from_children( - new_children, click_handler, svc - ) - new_menu.show_all() - item.set_submenu(new_menu) - else: - logger.info( - f"[GlobalMenu] no children for id={child.id} " - f"(update_layout returned {new_children})" - ) + if not children and entry: + logger.info( + "[GlobalMenu] DBus empty, trying Firefox bookmark SQLite fallback" + ) + children = _get_firefox_bookmarks(entry.pid, child.label) + if children: + children = _truncate_children(children) + if children: + logger.info( + f"[GlobalMenu] got {len(children)} children for id={child.id}: " + f"{[getattr(c, 'label', '?') for c in children[:5]]}" + ) + new_menu = _build_gtk_menu_from_children(children, click_handler, svc) + new_menu.show_all() + item.set_submenu(new_menu) + else: + logger.info(f"[GlobalMenu] no children for id={child.id}") except Exception as e: logger.warning(f"[GlobalMenu] submenu load failed: {e}") @@ -151,21 +274,21 @@ def _build_gtk_menu_from_children(children, click_handler, svc=None): if has_sub: item = Gtk.MenuItem.new_with_label(f"{cleaned} \u25b8") - sub_menu = _build_gtk_menu_from_children( - sub_children, click_handler, svc - ) + sub_menu = _build_gtk_menu_from_children(sub_children, click_handler, svc) sub_menu.show_all() item.set_submenu(sub_menu) if svc and getattr(child, "id", None) is not None: - item.connect( - "select", _on_item_select, svc, child, click_handler - ) + item.connect("select", _on_item_select, svc, child, click_handler) else: item = Gtk.MenuItem.new_with_label(cleaned) if not enabled: item.set_sensitive(False) - item.connect("activate", click_handler, child.id) + url = getattr(child, "_url", None) + if url: + item.connect("activate", lambda *_: _open_url(url)) + else: + item.connect("activate", click_handler, child.id) menu.append(item)