120 lines
4.3 KiB
Python
Executable File
120 lines
4.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# description: Show desktop notifications with optional actions. Args: <summary> [body] [--actions=label1,label2,...] [--timeout=ms] [--icon=name]
|
|
"""Send interactive notifications via org.freedesktop.Notifications D-Bus interface.
|
|
|
|
If --actions are specified, waits for user response and prints chosen action label.
|
|
Timeout defaults to 10000ms (10s) for action notifications, 5000ms otherwise.
|
|
"""
|
|
import sys
|
|
import argparse
|
|
import subprocess
|
|
import json
|
|
|
|
def parse_args():
|
|
p = argparse.ArgumentParser(description="Desktop notification with optional actions")
|
|
p.add_argument("summary", help="Notification title")
|
|
p.add_argument("body", nargs="?", default="", help="Notification body text")
|
|
p.add_argument("--actions", default="", help="Comma-separated action labels")
|
|
p.add_argument("--timeout", type=int, default=0, help="Timeout in ms (0=default)")
|
|
p.add_argument("--icon", default="ollie", help="Icon name (default: ollie)")
|
|
p.add_argument("--urgency", choices=["low", "normal", "critical"], default="normal")
|
|
return p.parse_args()
|
|
|
|
def main():
|
|
args = parse_args()
|
|
|
|
# Build actions array: ["id1", "label1", "id2", "label2", ...]
|
|
actions_list = []
|
|
if args.actions:
|
|
labels = [a.strip() for a in args.actions.split(",") if a.strip()]
|
|
for i, label in enumerate(labels):
|
|
actions_list.append(str(i))
|
|
actions_list.append(label)
|
|
|
|
timeout = args.timeout
|
|
if timeout == 0:
|
|
timeout = 10000 if actions_list else 5000
|
|
|
|
urgency_map = {"low": 0, "normal": 1, "critical": 2}
|
|
|
|
# Build gdbus call
|
|
# Notify(app_name, replaces_id, icon, summary, body, actions, hints, timeout)
|
|
actions_str = "[" + ", ".join(f"'{a}'" for a in actions_list) + "]"
|
|
hints_str = "{'urgency': <byte %d>}" % urgency_map[args.urgency]
|
|
|
|
cmd = [
|
|
"gdbus", "call", "--session",
|
|
"--dest", "org.freedesktop.Notifications",
|
|
"--object-path", "/org/freedesktop/Notifications",
|
|
"--method", "org.freedesktop.Notifications.Notify",
|
|
"ollie", # app_name
|
|
"0", # replaces_id (uint32)
|
|
args.icon, # icon
|
|
args.summary, # summary
|
|
args.body, # body
|
|
actions_str, # actions
|
|
hints_str, # hints
|
|
str(timeout), # 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)
|
|
|
|
# Parse notification ID from "(uint32 N,)"
|
|
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) # extra 2s grace
|
|
|
|
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:
|
|
# Parse: /org/freedesktop/Notifications: org.freedesktop.Notifications.ActionInvoked (uint32 N, 'action_id')
|
|
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()
|