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

164 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""orbit-menu — a radial power/utility menu for hyprdrive.
Two entry points into the same tree:
Super+Shift+O -> --power opens straight into the Power ring (sleep / reboot /
power off / soft reboot, via hyprshutdown)
Super+Ctrl+O -> --menu opens the 5-category root (Power, Tools, Scripts,
Management, Windows)
Single-instance, same pattern as astal-menu/main.py: the first launch builds the
(hidden) window and holds; later invocations forward their verb over D-Bus via
scripts/orbit-menu.sh instead of spawning a second python3+GTK4 process.
main.py run the resident instance (stays hidden until toggled)
main.py --power show, rooted at Power
main.py --menu show, rooted at the full category menu
main.py --hide hide
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
# orbit-menu-start.sh LD_PRELOADs libgtk4-layer-shell (load-ordering requirement
# ahead of libwayland-client). Drop it once resident so it isn't inherited by
# anything this process launches (see astal-menu/main.py for the same rationale —
# some GTK4 apps abort at startup with the layer-shell lib preloaded).
os.environ.pop("LD_PRELOAD", None)
sys.path.insert(0, str(Path(__file__).resolve().parent))
import gi # noqa: E402
gi.require_version("Gtk", "4.0")
from gi.repository import Gio, GLib, Gtk # noqa: E402
import config # noqa: E402
import menu_tree # noqa: E402
import theme # noqa: E402
from orbit_menu import MenuItem, OrbitMenu # noqa: E402
from paths import APP_ID # noqa: E402
class OrbitMenuApp(Gtk.Application):
def __init__(self) -> None:
super().__init__(application_id=APP_ID,
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
self.window: OrbitMenu | None = None
self._current_mode: str | None = None # "power" | "menu"
def do_startup(self) -> None:
Gtk.Application.do_startup(self)
theme.load_css()
self.window = OrbitMenu(menu_tree.full_menu_root(), on_close=self._on_closed,
satellites=config.satellites_enabled(),
hologram=config.hologram_enabled())
self._current_mode = "menu"
self._register_actions()
self.hold() # stay alive with no visible window
def _on_closed(self) -> None:
pass # nothing to do; the window just hides itself
def _open(self, mode: str) -> None:
assert self.window is not None
if self.window.get_visible() and self._current_mode == mode:
self.window.set_visible(False)
return
if self._current_mode != mode:
root = menu_tree.power_only_root() if mode == "power" else menu_tree.full_menu_root()
self.window.set_root(root)
self._current_mode = mode
self.window.open_at_root()
def _register_actions(self) -> None:
def add(name: str, callback) -> None:
action = Gio.SimpleAction.new(name, None)
action.connect("activate", callback)
self.add_action(action)
def on_hide(*_a) -> None:
assert self.window is not None
self.window.set_visible(False)
add("power", lambda *_a: self._open("power"))
add("menu", lambda *_a: self._open("menu"))
add("hide", on_hide)
def do_command_line(self, cmdline: Gio.ApplicationCommandLine) -> int:
args = list(cmdline.get_arguments()[1:])
verb = args[0] if args else "--daemon"
if self.window is None:
return 0
if verb == "--power":
self._open("power")
elif verb == "--menu":
self._open("menu")
elif verb == "--hide":
self.window.set_visible(False)
# --daemon and anything else: no-op (stay resident, hidden)
return 0
def do_activate(self) -> None:
pass # resident instance: nothing to do on plain activate
class DmenuApp(Gtk.Application):
"""Standalone 'classic dmenu' mode: read newline-delimited items from stdin,
show them as ring nodes, print the chosen one to stdout and exit 0 (exit 1 on
cancel, printing nothing). NON_UNIQUE so it never routes to the resident daemon
(which couldn't write to this caller's stdout) — it's its own throwaway process."""
def __init__(self, items: list[str]) -> None:
super().__init__(application_id=APP_ID + ".dmenu",
flags=Gio.ApplicationFlags.NON_UNIQUE)
self._items = items
self.result: str | None = None
self.window: OrbitMenu | None = None
def do_activate(self) -> None:
if self.window is not None: # do_activate can fire more than once
return
theme.load_css()
children = [MenuItem(label=line, action=(lambda ln=line: self._pick(ln)))
for line in self._items]
root = MenuItem(label="", children=children)
self.window = OrbitMenu(root, on_close=self._on_closed,
satellites=config.satellites_enabled(),
hologram=config.hologram_enabled())
self.window.open_at_root()
self.hold() # a hidden layer-shell window wouldn't keep the app alive alone
def _pick(self, line: str) -> None:
self.result = line # the leaf's action; _close() then runs the outro + on_close
def _on_closed(self) -> None:
if self.result is not None:
sys.stdout.write(self.result + "\n")
sys.stdout.flush()
self.quit()
def _run_dmenu() -> int:
items = [ln.rstrip("\n") for ln in sys.stdin.readlines()]
items = [ln for ln in items if ln != ""]
if not items:
return 1
app = DmenuApp(items)
app.run([sys.argv[0]]) # don't forward --dmenu into GApplication arg handling
return 0 if app.result is not None else 1
def main() -> int:
GLib.set_prgname("orbit-menu")
if "--dmenu" in sys.argv[1:]:
return _run_dmenu()
return OrbitMenuApp().run(sys.argv)
if __name__ == "__main__":
raise SystemExit(main())