#!/usr/bin/env python3 """beacon — the Cosmonaut Shell notification daemon for hyprdrive. Replaces dunst: owns org.freedesktop.Notifications and renders each notification as a holographic card (scanline/sweep/noise overlay, emitted-magenta text, violet holo-glass, radio-squiggle divider) in a top-centre layer-shell stack, so notifications read as one more orbit of the astro-menu / orbit-menu / station-bar look instead of a plain popup. main.py run the resident daemon (owns the notification bus name, stays hidden until a notification arrives) """ from __future__ import annotations import os import signal import sys from pathlib import Path # beacon-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 (same rationale as the rest of the suite's main.py). 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 theme # noqa: E402 from paths import APP_ID, FDN_NAME # noqa: E402 from server import NotificationServer # noqa: E402 from window import BeaconWindow # noqa: E402 class BeaconApp(Gtk.Application): def __init__(self) -> None: super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.DEFAULT_FLAGS) self.window: BeaconWindow | None = None self._server: NotificationServer | None = None self._name_id = 0 def do_startup(self) -> None: Gtk.Application.do_startup(self) theme.load_css() self.window = BeaconWindow(self, on_closed=self._on_closed) # Own the freedesktop notification name; the server is created once the bus # connection is in hand. self._name_id = Gio.bus_own_name( Gio.BusType.SESSION, FDN_NAME, Gio.BusNameOwnerFlags.NONE, self._on_bus_acquired, None, self._on_name_lost) # SIGUSR1 = close every visible card (the `Super+Ctrl+C` keybind, which # used to run `dunstctl close-all`). GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGUSR1, self._on_sigusr1) self.hold() # stay alive with no visible window def _on_sigusr1(self) -> bool: if self.window is not None: self.window.close_all() return GLib.SOURCE_CONTINUE def _on_bus_acquired(self, connection: Gio.DBusConnection, _name: str) -> None: assert self.window is not None self._server = NotificationServer(connection, self.window) def _on_closed(self, nid: int, reason: int) -> None: if self._server is not None: self._server.emit_closed(nid, reason) def _on_name_lost(self, _connection, _name: str) -> None: # Another notification daemon (e.g. a still-running dunst) already owns it. sys.stderr.write( "beacon: could not acquire org.freedesktop.Notifications " "(another notification daemon is running); exiting.\n") self.quit() def do_activate(self) -> None: pass # resident daemon: nothing to do on activate def main() -> int: GLib.set_prgname("beacon") return BeaconApp().run(sys.argv) if __name__ == "__main__": raise SystemExit(main())