#!/usr/bin/env python3 """astro-menu — a touch-friendly GTK4 popup control centre (Location, Weather, Bluetooth, Network) plus an application drawer, replacing nwg-dock/nwg-drawer. Single-instance: the first launch builds the (hidden) window and holds. Later invocations forward their arguments to it over D-Bus, so `main.py --toggle` toggles the running instance without spawning a new process. main.py run the resident instance (stays hidden until toggled) main.py --toggle toggle visibility main.py --show show | --hide hide | --appdrawer show with drawer open """ from __future__ import annotations import os import sys from pathlib import Path # astro-menu-start.sh LD_PRELOADs libgtk4-layer-shell so it loads before # libwayland-client (a load-ordering requirement of the layer-shell library). That # only matters at *this* process's exec: the library is already resident now, so the # variable is never read again. Drop it here so the GUI apps we launch via # AstalApps.launch() (and the backend subprocesses) don't inherit it — Firefox, for # one, aborts at startup with libgtk4-layer-shell preloaded. os.environ.pop("LD_PRELOAD", None) # Make sibling modules importable no matter the CWD. 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 theme # noqa: E402 from appservices import Services # noqa: E402 from paths import APP_ID, ensure_dirs # noqa: E402 from settings import Settings # noqa: E402 from window import MenuWindow # noqa: E402 class AstalMenuApp(Gtk.Application): def __init__(self) -> None: super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE) self.window: MenuWindow | None = None def do_startup(self) -> None: Gtk.Application.do_startup(self) ensure_dirs() theme.load_css() settings = Settings() services = Services() self.window = MenuWindow(self, settings, services) self._register_actions() self.hold() # stay alive with no visible window def _register_actions(self) -> None: # Mirrors the --show/--hide/--toggle/--appdrawer verbs from do_command_line, but # reachable via org.gtk.Actions.Activate. menu-toggle.sh calls these directly with # `gdbus call` for already-running instances, which skips spawning a whole second # python3 + GTK process (~0.5s) just to forward one verb over D-Bus. def add(name: str, callback, has_side: bool) -> None: action = Gio.SimpleAction.new(name, GLib.VariantType.new("s") if has_side else None) action.connect("activate", callback) self.add_action(action) def side_of(param: GLib.Variant | None) -> str: return param.get_string() if param is not None else "" def on_show(_action, param) -> None: assert self.window is not None if side_of(param): self.window.set_side(side_of(param)) self.window.show_menu() def on_hide(_action, _param) -> None: assert self.window is not None self.window.hide_menu() def on_toggle(_action, param) -> None: assert self.window is not None if side_of(param): self.window.set_side(side_of(param)) self.window.toggle() def on_appdrawer(_action, param) -> None: assert self.window is not None if side_of(param): self.window.set_side(side_of(param)) self.window.show_menu(focus_appdrawer=True) add("show", on_show, has_side=True) add("hide", on_hide, has_side=False) add("toggle", on_toggle, has_side=True) add("appdrawer", on_appdrawer, has_side=True) def do_command_line(self, cmdline: Gio.ApplicationCommandLine) -> int: args = list(cmdline.get_arguments()[1:]) # Optional "--side top|bottom|left|right": the menu slides in from and pins to # that monitor edge. Applied before the verb so the surface maps animated. side = None if "--side" in args: i = args.index("--side") if i + 1 < len(args): side = args[i + 1] del args[i:i + 2] # No args = start (or keep) the resident instance hidden. Only explicit # verbs change visibility, so the autostart launch never pops the menu. action = args[0] if args else "--daemon" if self.window is None: return 0 if side: self.window.set_side(side) if action == "--show": self.window.show_menu() elif action == "--hide": self.window.hide_menu() elif action == "--appdrawer": self.window.show_menu(focus_appdrawer=True) elif action == "--toggle": self.window.toggle() # --daemon and anything else: no-op (stay as-is) return 0 def do_activate(self) -> None: # Resident instance: nothing to do on plain activate. pass def main() -> int: GLib.set_prgname("astro-menu") return AstalMenuApp().run(sys.argv) if __name__ == "__main__": raise SystemExit(main())