dot

vizie's NixOS desktop: Hyprland, CachyOS kernel and gaming tweaks

88 lines2768 bytesPythonexecutable
1#!/usr/bin/env python3
2"""Click on an entry in the Eww notifications panel: go to the app that sent it.
3
4Usage: open-notification.py <dunst notification id>
5
6Focuses the sending app's window (matched by app name against Hyprland's window
7classes and titles), removes the entry from dunst's history and closes the panel.
8If no window of that app is open, nothing happens and the entry stays.
9
10Dunst can't re-run the app's own action here: once a notification has left the
11screen it has been reported to the app as closed, and dunst no longer signals
12actions for it. That only works by clicking the pop-up while it is showing.
13
14Only the id comes from the panel; the app name is looked up in dunst's history, so
15text chosen by whoever sent the notification never reaches a shell.
16"""
17import json
18import os
19import subprocess
20import sys
21
22DIR = os.path.dirname(os.path.abspath(__file__))
23
24
25def run(*args):
26 try:
27 return subprocess.run(args, capture_output=True, text=True, timeout=3).stdout
28 except Exception:
29 return ""
30
31
32def app_of(notification_id):
33 try:
34 history = json.loads(run("dunstctl", "history"))["data"][0]
35 except Exception:
36 return ""
37 for n in history:
38 if (n.get("id") or {}).get("data") == notification_id:
39 return ((n.get("appname") or {}).get("data") or "").strip()
40 return ""
41
42
43def find_window(app):
44 """Best matching window for an app name, preferring the most recently focused."""
45 app = app.lower()
46 if not app:
47 return None
48 try:
49 clients = json.loads(run("hyprctl", "clients", "-j"))
50 except Exception:
51 return None
52
53 def score(client):
54 names = [(client.get("class") or "").lower(), (client.get("initialClass") or "").lower()]
55 if app in names:
56 return 3
57 if any(name and (app in name or name in app) for name in names):
58 return 2
59 if app in (client.get("title") or "").lower():
60 return 1
61 return 0
62
63 matches = [c for c in clients if c.get("mapped", True) and score(c) > 0]
64 if not matches:
65 return None
66 matches.sort(key=lambda c: (-score(c), c.get("focusHistoryID", 1 << 30)))
67 return matches[0]["address"]
68
69
70def main():
71 try:
72 notification_id = int(sys.argv[1])
73 except (IndexError, ValueError):
74 return 2
75
76 address = find_window(app_of(notification_id))
77 if not address:
78 return 1
79
80 subprocess.run(["bash", os.path.join(DIR, "toggle.sh"), "close"])
81 subprocess.run(["hyprctl", "dispatch", f'hl.dsp.focus({{ window = "address:{address}" }})'],
82 capture_output=True)
83 subprocess.run(["dunstctl", "history-rm", str(notification_id)])
84 return 0
85
86
87if __name__ == "__main__":
88 sys.exit(main())