|
| 1 | +#!/usr/bin/python3 |
| 2 | +""" |
| 3 | +Post-process the labwc menu generator output: |
| 4 | +- ensure Terminal/File Manager entries |
| 5 | +- add DMS Settings |
| 6 | +- regroup Distrobox entries under a submenu |
| 7 | +""" |
| 8 | + |
| 9 | +import os |
| 10 | +import sys |
| 11 | +import xml.etree.ElementTree as ET |
| 12 | +from copy import deepcopy |
| 13 | + |
| 14 | + |
| 15 | +def load_env(): |
| 16 | + return { |
| 17 | + "ghostty_bin": os.environ.get("GHOSTTY_BIN", "/usr/bin"), |
| 18 | + "distrobox_bin": os.environ.get("DISTROBOX_BIN", "/usr/bin"), |
| 19 | + "dbx_path": os.environ.get("DBX_PATH", "/usr/bin:/usr/local/bin"), |
| 20 | + "dms_bin": os.environ.get("DMS_BIN", "/usr/bin/dms"), |
| 21 | + "dms_path": os.environ.get("DMS_PATH", "/usr/bin"), |
| 22 | + } |
| 23 | + |
| 24 | + |
| 25 | +def ensure_terminal_entry(root_menu, ghostty_bin): |
| 26 | + if root_menu is None: |
| 27 | + return |
| 28 | + for child in root_menu.findall("item"): |
| 29 | + if (child.get("label") or "").strip().lower() == "terminal": |
| 30 | + return |
| 31 | + term_item = ET.Element("item", {"label": "Terminal", "icon": "utilities-terminal"}) |
| 32 | + action = ET.SubElement(term_item, "action", {"name": "Execute"}) |
| 33 | + cmd = ET.SubElement(action, "command") |
| 34 | + cmd.text = f"{ghostty_bin}/ghostty" |
| 35 | + root_menu.insert(0, ET.Element("separator")) |
| 36 | + root_menu.insert(0, term_item) |
| 37 | + |
| 38 | + |
| 39 | +def ensure_file_manager_entry(root_menu): |
| 40 | + if root_menu is None: |
| 41 | + return |
| 42 | + for child in root_menu.findall("item"): |
| 43 | + if (child.get("label") or "").strip().lower() == "file manager": |
| 44 | + return |
| 45 | + fm_item = ET.Element("item", {"label": "File Manager", "icon": "system-file-manager"}) |
| 46 | + action = ET.SubElement(fm_item, "action", {"name": "Execute"}) |
| 47 | + cmd = ET.SubElement(action, "command") |
| 48 | + cmd.text = "thunar" |
| 49 | + inserted = False |
| 50 | + for idx, child in enumerate(list(root_menu)): |
| 51 | + if child.tag == "item" and (child.get("label") or "").strip().lower() == "terminal": |
| 52 | + root_menu.insert(idx + 1, fm_item) |
| 53 | + inserted = True |
| 54 | + break |
| 55 | + if not inserted: |
| 56 | + root_menu.insert(0, fm_item) |
| 57 | + |
| 58 | + |
| 59 | +def is_distro_item(item, distrobox_bin): |
| 60 | + for action in item.findall("action"): |
| 61 | + cmd = (action.findtext("command") or "").lower() |
| 62 | + if "distrobox" in cmd or "distrobox-enter" in cmd: |
| 63 | + return True |
| 64 | + icon = (item.get("icon") or "").lower() |
| 65 | + return "distrobox" in icon |
| 66 | + |
| 67 | + |
| 68 | +def collect_and_prune(root_menu, distrobox_bin): |
| 69 | + distro_items = [] |
| 70 | + exclude_labels = {"ikhal"} |
| 71 | + |
| 72 | + def collect(elem): |
| 73 | + for child in list(elem): |
| 74 | + if child.tag == "item": |
| 75 | + label = (child.get("label") or "").strip().lower() |
| 76 | + if label in exclude_labels: |
| 77 | + elem.remove(child) |
| 78 | + continue |
| 79 | + if child.tag == "item" and is_distro_item(child, distrobox_bin): |
| 80 | + distro_items.append(deepcopy(child)) |
| 81 | + elem.remove(child) |
| 82 | + elif child.tag == "menu": |
| 83 | + collect(child) |
| 84 | + |
| 85 | + def prune_empty_menus(elem, keep_root=False): |
| 86 | + for child in list(elem): |
| 87 | + if child.tag == "menu": |
| 88 | + prune_empty_menus(child) |
| 89 | + if len([c for c in child if c.tag in ("item", "menu")]) == 0: |
| 90 | + elem.remove(child) |
| 91 | + |
| 92 | + if root_menu is not None: |
| 93 | + collect(root_menu) |
| 94 | + prune_empty_menus(root_menu, keep_root=True) |
| 95 | + return distro_items |
| 96 | + |
| 97 | + |
| 98 | +def ensure_dms_settings(root, root_menu, dms_path, dms_bin): |
| 99 | + settings = root.find(".//menu[@label='Settings']") or root_menu |
| 100 | + if settings is None: |
| 101 | + return |
| 102 | + for child in settings.findall("item"): |
| 103 | + if child.get("label") == "DMS Settings": |
| 104 | + return |
| 105 | + item = ET.SubElement(settings, "item", {"label": "DMS Settings", "icon": "preferences-desktop"}) |
| 106 | + action = ET.SubElement(item, "action", {"name": "Execute"}) |
| 107 | + cmd = ET.SubElement(action, "command") |
| 108 | + cmd.text = ( |
| 109 | + f"/bin/sh -c 'PATH={dms_path}:$PATH; export PATH; " |
| 110 | + "export XDG_CURRENT_DESKTOP=labwc; export QT_QPA_PLATFORM=wayland; " |
| 111 | + "export WAYLAND_DISPLAY=\"$${WAYLAND_DISPLAY:-wayland-0}\"; " |
| 112 | + "systemctl --user start dms.service >/dev/null || true; " |
| 113 | + f"exec {dms_bin} settings'" |
| 114 | + ) |
| 115 | + |
| 116 | + |
| 117 | +def rewrite_distro_item(item, distrobox_bin, ghostty_bin, dbx_path): |
| 118 | + label = item.get("label") or "" |
| 119 | + if not label: |
| 120 | + return None |
| 121 | + # Skip app entries launched inside containers (we only want the containers themselves). |
| 122 | + if "(on " in label: |
| 123 | + return None |
| 124 | + original_cmd = "" |
| 125 | + for action in item.findall("action"): |
| 126 | + cmd_text = action.findtext("command") |
| 127 | + if cmd_text: |
| 128 | + original_cmd = cmd_text |
| 129 | + break |
| 130 | + if "distrobox" not in original_cmd.lower(): |
| 131 | + return None |
| 132 | + # If the command chains distrobox enter with an app (contains "--"), skip it. |
| 133 | + if "--" in original_cmd: |
| 134 | + return None |
| 135 | + container = label.lower() |
| 136 | + if original_cmd: |
| 137 | + parts = original_cmd.strip().replace('"', "'").split() |
| 138 | + if parts: |
| 139 | + # Prefer explicit "-n name" if present; otherwise fall back to last arg. |
| 140 | + if "-n" in parts: |
| 141 | + try: |
| 142 | + idx = parts.index("-n") |
| 143 | + maybe = parts[idx + 1].strip("'").strip('"') |
| 144 | + if maybe: |
| 145 | + container = maybe |
| 146 | + except Exception: |
| 147 | + pass |
| 148 | + else: |
| 149 | + maybe = parts[-1].strip("'").strip('"') |
| 150 | + if maybe: |
| 151 | + container = maybe |
| 152 | + inner_cmd = original_cmd.strip() if original_cmd else f"{distrobox_bin}/distrobox enter {container}" |
| 153 | + if inner_cmd.startswith(f"{ghostty_bin}/ghostty") and "distrobox enter" in inner_cmd: |
| 154 | + tail = inner_cmd.split("distrobox enter", 1)[1].strip() |
| 155 | + tail = tail.strip("'\"") |
| 156 | + if tail: |
| 157 | + inner_cmd = f"{distrobox_bin}/distrobox enter {tail}" |
| 158 | + title_prefixed = f"Distrobox: {label}" |
| 159 | + launch = ( |
| 160 | + f"/bin/sh -c \"PATH={dbx_path}; export PATH; DISABLE_AUTO_TITLE=1 " |
| 161 | + f"{ghostty_bin}/ghostty --title='{title_prefixed}' -e {inner_cmd}\"" |
| 162 | + ) |
| 163 | + icon = item.get("icon") or "utilities-terminal" |
| 164 | + new_item = ET.Element("item", {"label": label, "icon": icon}) |
| 165 | + action = ET.SubElement(new_item, "action", {"name": "Execute"}) |
| 166 | + cmd = ET.SubElement(action, "command") |
| 167 | + cmd.text = launch |
| 168 | + return new_item |
| 169 | + |
| 170 | + |
| 171 | +def add_distro_menu(root_menu, distro_items, distrobox_bin, ghostty_bin, dbx_path): |
| 172 | + if root_menu is None or not distro_items: |
| 173 | + return |
| 174 | + root_menu.append(ET.Element("separator")) |
| 175 | + db_menu = ET.Element("menu", {"id": "distrobox-list", "label": "Distrobox", "icon": "utilities-terminal"}) |
| 176 | + for item in distro_items: |
| 177 | + new_item = rewrite_distro_item(item, distrobox_bin, ghostty_bin, dbx_path) |
| 178 | + if new_item is not None: |
| 179 | + db_menu.append(new_item) |
| 180 | + root_menu.append(db_menu) |
| 181 | + |
| 182 | + |
| 183 | +def main(): |
| 184 | + if len(sys.argv) != 3: |
| 185 | + print("Usage: labwc-menu-postprocess.py <infile> <outfile>", file=sys.stderr) |
| 186 | + sys.exit(1) |
| 187 | + tmp, dest = sys.argv[1], sys.argv[2] |
| 188 | + env = load_env() |
| 189 | + |
| 190 | + tree = ET.parse(tmp) |
| 191 | + root = tree.getroot() |
| 192 | + root_menu = root.find(".//menu[@id='root-menu']") or root.find("./menu") |
| 193 | + |
| 194 | + ensure_terminal_entry(root_menu, env["ghostty_bin"]) |
| 195 | + ensure_file_manager_entry(root_menu) |
| 196 | + distro_items = collect_and_prune(root_menu, env["distrobox_bin"]) |
| 197 | + ensure_dms_settings(root, root_menu, env["dms_path"], env["dms_bin"]) |
| 198 | + add_distro_menu(root_menu, distro_items, env["distrobox_bin"], env["ghostty_bin"], env["dbx_path"]) |
| 199 | + |
| 200 | + try: |
| 201 | + ET.indent(tree, space=" ") |
| 202 | + except Exception: |
| 203 | + pass |
| 204 | + tree.write(dest, encoding="utf-8", xml_declaration=True) |
| 205 | + |
| 206 | + |
| 207 | +if __name__ == "__main__": |
| 208 | + main() |
0 commit comments