"""Builds the 5-category MenuItem tree: Power, Tools, Scripts, Management, Windows. Power/Tools/Management are static lists. Scripts and Windows are `dynamic=True` — orbit_menu.py never caches a dynamic node's page, so both are rebuilt fresh (current ~/Documents/Scripts listing, current `hyprctl clients -j`) every time they're entered; that dynamism cascades to their children too (see OrbitMenu._cache_key), so a window's own action ring and its "send to workspace" submenu are always live. """ from __future__ import annotations import json import os import subprocess import actions from orbit_menu import MenuItem from paths import SCRIPTS_DIR def _hyprctl_json(*args: str): try: out = subprocess.run(["hyprctl", *args, "-j"], capture_output=True, text=True, timeout=1.5, check=True).stdout return json.loads(out) except (subprocess.SubprocessError, json.JSONDecodeError, OSError): return None # -- Power ------------------------------------------------------------------ def power_root() -> MenuItem: return MenuItem(label="Power", icon="⏻", children=[ MenuItem(label="Sleep", icon="☾", danger=True, action=lambda: actions.hyprshutdown("systemctl suspend")), MenuItem(label="Reboot", icon="⟳", danger=True, action=lambda: actions.hyprshutdown("systemctl reboot")), MenuItem(label="Power Off", icon="⏻", danger=True, action=lambda: actions.hyprshutdown("systemctl poweroff")), MenuItem(label="Soft Reboot", icon="⟲", danger=True, action=lambda: actions.hyprshutdown("systemctl soft-reboot")), ]) # -- Tools -------------------------------------------------------------------- # The little utilities also reachable via their own keybinds (see binds.lua) — # gathered here as one launcher so they don't all need to be memorised. def tools_root() -> MenuItem: def kitty(tag: str, cmd: str) -> str: return f"[tag {tag}] kitty {cmd}" return MenuItem(label="Tools", icon="⚙", children=[ MenuItem(label="Timer", action=lambda: actions.launch( kitty("+centered-S", "bash ~/.config/scripts/timer-pick"))), MenuItem(label="SSH Manager", action=lambda: actions.launch( kitty("+centered-L", "-e ~/.config/scripts/amssh"))), MenuItem(label="System Monitor", action=lambda: actions.launch(kitty("+centered-L", "btop"))), MenuItem(label="Screen Recorder", action=lambda: actions.launch("~/.config/scripts/screenrec.sh")), MenuItem(label="Color Picker", action=lambda: actions.launch("hyprpicker | wl-copy")), MenuItem(label="Caffeine", action=lambda: actions.launch("~/.config/scripts/caffeine.sh")), MenuItem(label="On-Screen Kbd", action=lambda: actions.launch("~/.config/scripts/onscreenkb.sh")), MenuItem(label="Calculator", action=lambda: actions.launch("wofi-calc")), MenuItem(label="Keybind Help", action=lambda: actions.launch( kitty("+centered", "~/.config/scripts/helpmenu.sh"))), ]) # -- Scripts (dynamic) --------------------------------------------------------- def _script_children() -> list[MenuItem]: if not SCRIPTS_DIR.is_dir(): return [MenuItem(label=f"(create {SCRIPTS_DIR})")] entries = sorted(p for p in SCRIPTS_DIR.iterdir() if p.is_file()) if not entries: return [MenuItem(label="(no scripts found)")] items = [] for p in entries: if os.access(p, os.X_OK): cmd = f"[tag +centered-L] kitty {p}" elif p.suffix == ".py": cmd = f"[tag +centered-L] kitty python3 {p}" elif p.suffix in (".sh", ".bash"): cmd = f"[tag +centered-L] kitty bash {p}" else: continue items.append(MenuItem(label=p.stem, action=lambda c=cmd: actions.launch(c))) return items or [MenuItem(label="(no runnable scripts)")] def scripts_root() -> MenuItem: return MenuItem(label="Scripts", icon="❯", dynamic=True, children=_script_children) # -- Management ----------------------------------------------------------------- def management_root() -> MenuItem: return MenuItem(label="Management", icon="⛶", children=[ MenuItem(label="Wallpaper Picker", action=lambda: actions.launch( "[tag +centered-L] kitty -e ~/.config/scripts/wallpaper-picker ~/Pictures")), MenuItem(label="Monitor Manager", action=lambda: actions.launch( "[tag +centered-L] kitty -e ~/.config/scripts/monitor-manager")), ]) # -- Windows (dynamic) ----------------------------------------------------------- def _window_label(w: dict) -> str: return (w.get("title") or w.get("class") or "?")[:28] def _workspace_children(address: str) -> list[MenuItem]: workspaces = _hyprctl_json("workspaces") or [] if not workspaces: return [MenuItem(label="(no workspaces)")] items = [] for ws in sorted(workspaces, key=lambda w: w.get("id", 0)): wsid = ws.get("id") name = ws.get("name") or str(wsid) items.append(MenuItem(label=name, icon="⇒", action=lambda w=wsid: actions.send_to_workspace(address, w))) return items def _window_actions(w: dict) -> list[MenuItem]: address = w["address"] pid = w.get("pid", -1) active = _hyprctl_json("activeworkspace") or {} active_ws = active.get("id", w.get("workspace", {}).get("id", 0)) title = _window_label(w) return [ MenuItem(label="Go To Window", icon="➤", action=lambda: actions.focus_window(address)), MenuItem(label="Bring Here", icon="⇤", action=lambda: actions.bring_here(address, active_ws)), MenuItem(label="Close Window", icon="✕", action=lambda: actions.close_window(address)), MenuItem(label="Force Kill", icon="⊘", danger=True, action=lambda: actions.force_kill(pid)), MenuItem(label="Get PID", icon="#", action=lambda: actions.report_pid(pid, title)), MenuItem(label="Send To Workspace", icon="⇒", dynamic=True, children=lambda: _workspace_children(address)), ] def _window_children() -> list[MenuItem]: clients = _hyprctl_json("clients") or [] wins = [w for w in clients if w.get("mapped", True) and w.get("class")] if not wins: return [MenuItem(label="(no open windows)")] wins.sort(key=lambda w: (w.get("workspace", {}).get("id", 0), _window_label(w).lower())) return [MenuItem(label=_window_label(w), dynamic=True, children=lambda w=w: _window_actions(w)) for w in wins] def windows_root() -> MenuItem: return MenuItem(label="Windows", icon="▣", dynamic=True, children=_window_children) # -- Roots ------------------------------------------------------------------ def full_menu_root() -> MenuItem: return MenuItem(label="Menu", children=[ power_root(), tools_root(), scripts_root(), management_root(), windows_root(), ]) def power_only_root() -> MenuItem: return power_root()