#!/usr/bin/env python3 # args_json: {"type":"object","required":["summary"],"properties":{"summary":{"type":"string","description":"Notification title"},"body":{"type":"string","description":"Notification body"},"actions":{"type":"string","description":"Comma-separated action labels"},"timeout":{"type":"integer","description":"Timeout in ms (default: 5000, 10000 with actions)"},"icon":{"type":"string","description":"Icon name (default: ollie)"},"urgency":{"type":"string","description":"low|normal|critical (default: normal)"}}} # ollie:prompt # ## gui_notify # # Show desktop notifications. # # ``` # gui_notify(summary="Build complete", body="All tests passed") # ``` # ollie:end """Send interactive notifications via org.freedesktop.Notifications D-Bus interface.""" import sys import os import subprocess sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _lib.args import parse_args def main(): args = parse_args() summary = args.require("summary") body = args.get("body", "") actions_str = args.get("actions", "") timeout = args.get_int("timeout", 0) icon = args.get("icon", "ollie") urgency = args.get("urgency", "normal") # Build actions array actions_list = [] if actions_str: labels = [a.strip() for a in actions_str.split(",") if a.strip()] for i, label in enumerate(labels): actions_list.append(str(i)) actions_list.append(label) if timeout == 0: timeout = 10000 if actions_list else 5000 urgency_map = {"low": 0, "normal": 1, "critical": 2} actions_gdbus = "[" + ", ".join(f"'{a}'" for a in actions_list) + "]" hints_gdbus = "{'urgency': }" % urgency_map.get(urgency, 1) cmd = [ "gdbus", "call", "--session", "--dest", "org.freedesktop.Notifications", "--object-path", "/org/freedesktop/Notifications", "--method", "org.freedesktop.Notifications.Notify", "ollie", "0", icon, summary, body, actions_gdbus, hints_gdbus, str(timeout), ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: print(f"ERROR: {result.stderr.strip()}", file=sys.stderr) sys.exit(1) nid = result.stdout.strip().strip("()").split(",")[0].replace("uint32 ", "") if not actions_list: print(f"notification:{nid}") return # Wait for ActionInvoked or NotificationClosed signal labels = [actions_list[i+1] for i in range(0, len(actions_list), 2)] monitor_cmd = [ "gdbus", "monitor", "--session", "--dest", "org.freedesktop.Notifications", "--object-path", "/org/freedesktop/Notifications", ] import signal class TimeoutError(Exception): pass def alarm_handler(signum, frame): raise TimeoutError() signal.signal(signal.SIGALRM, alarm_handler) signal.alarm((timeout // 1000) + 2) try: proc = subprocess.Popen(monitor_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) for line in proc.stdout: if "ActionInvoked" in line and nid in line: parts = line.split("'") if len(parts) >= 2: action_id = parts[1] try: idx = int(action_id) print(labels[idx]) except (ValueError, IndexError): print(action_id) proc.terminate() return if "NotificationClosed" in line and nid in line: print("dismissed") proc.terminate() return except TimeoutError: proc.terminate() print("timeout") except Exception as e: print(f"ERROR: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()