#!/usr/bin/env python3 """mnotifdctl — a small CLI for mnotifd's D-Bus surface, the "dunstctl" mnotifd never had (screenrec.sh has its own note on this gap). Talks to: org.freedesktop.Notifications (CloseNotification) eu.abdelbaki.mnotifd.History1 (List/Get/Pop/PopLatest/Remove/Clear) `close-all` is the one exception: it isn't a D-Bus call (mnotifd doesn't expose window.close_all() over the bus), it's the same SIGUSR1 signal the Super+Ctrl+C keybind already sends. """ from __future__ import annotations import argparse import json import subprocess import sys import gi gi.require_version("Gtk", "4.0") from gi.repository import Gio, GLib # noqa: E402 FDN_NAME = "org.freedesktop.Notifications" FDN_PATH = "/org/freedesktop/Notifications" FDN_IFACE = "org.freedesktop.Notifications" HISTORY_IFACE = "eu.abdelbaki.mnotifd.History1" def _proxy(iface: str) -> Gio.DBusProxy: return Gio.DBusProxy.new_for_bus_sync( Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None, FDN_NAME, FDN_PATH, iface, None) def cmd_list(args: argparse.Namespace) -> int: proxy = _proxy(HISTORY_IFACE) (entries,) = proxy.call_sync( "List", None, Gio.DBusCallFlags.NONE, -1, None).unpack() if args.json: print(json.dumps(entries, indent=2)) return 0 if not entries: print("(history is empty)") return 0 for e in entries: print(f"{e['id']:>4} {(e.get('app_name') or '?'):<20} {e.get('summary', '')}") return 0 def cmd_get(args: argparse.Namespace) -> int: proxy = _proxy(HISTORY_IFACE) (entry,) = proxy.call_sync( "Get", GLib.Variant("(u)", (args.id,)), Gio.DBusCallFlags.NONE, -1, None).unpack() print(json.dumps(entry, indent=2)) return 0 def cmd_pop(args: argparse.Namespace) -> int: proxy = _proxy(HISTORY_IFACE) if args.id is None: (new_id,) = proxy.call_sync( "PopLatest", None, Gio.DBusCallFlags.NONE, -1, None).unpack() else: (new_id,) = proxy.call_sync( "Pop", GLib.Variant("(u)", (args.id,)), Gio.DBusCallFlags.NONE, -1, None).unpack() print(new_id) return 0 def cmd_remove(args: argparse.Namespace) -> int: proxy = _proxy(HISTORY_IFACE) proxy.call_sync( "Remove", GLib.Variant("(u)", (args.id,)), Gio.DBusCallFlags.NONE, -1, None) return 0 def cmd_clear(_args: argparse.Namespace) -> int: proxy = _proxy(HISTORY_IFACE) proxy.call_sync("Clear", None, Gio.DBusCallFlags.NONE, -1, None) return 0 def cmd_close(args: argparse.Namespace) -> int: proxy = _proxy(FDN_IFACE) proxy.call_sync( "CloseNotification", GLib.Variant("(u)", (args.id,)), Gio.DBusCallFlags.NONE, -1, None) return 0 def cmd_close_all(_args: argparse.Namespace) -> int: # bracket trick so pkill's own argv doesn't self-match, same as the # Super+Ctrl+C keybind in hypr/usr/binds.lua subprocess.run(["pkill", "-USR1", "-f", "[b]eacon/main.py"]) return 0 def main() -> int: p = argparse.ArgumentParser(prog="mnotifdctl", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) sub = p.add_subparsers(dest="command", required=True) sp = sub.add_parser("list", help="list notification history, newest first") sp.add_argument("--json", action="store_true") sp.set_defaults(func=cmd_list) sp = sub.add_parser("get", help="show one history entry as JSON") sp.add_argument("id", type=int) sp.set_defaults(func=cmd_get) sp = sub.add_parser( "pop", help="re-display a history entry (most recent if id omitted)") sp.add_argument("id", type=int, nargs="?", default=None) sp.set_defaults(func=cmd_pop) sp = sub.add_parser("remove", help="remove one entry from history") sp.add_argument("id", type=int) sp.set_defaults(func=cmd_remove) sp = sub.add_parser("clear", help="clear all history") sp.set_defaults(func=cmd_clear) sp = sub.add_parser("close", help="dismiss a live notification by id") sp.add_argument("id", type=int) sp.set_defaults(func=cmd_close) sp = sub.add_parser("close-all", help="dismiss every visible notification") sp.set_defaults(func=cmd_close_all) args = p.parse_args() try: return args.func(args) except GLib.GError as e: print(f"mnotifdctl: {e.message}", file=sys.stderr) return 1 if __name__ == "__main__": raise SystemExit(main())