139 lines
5.2 KiB
Python
Executable File
139 lines
5.2 KiB
Python
Executable File
#!/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())
|