Dotfiles/desktopenvs/hyprdrive/orbit-menu/menu_tree.py

183 lines
7.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""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("qalculate-gtk")),
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()