aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/dot_config/waybar/executable_notification-picker.py
blob: e6d6aed68fbbd3822900d8f54cdedb7691bbdbfc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env python3
"""Notification picker.

Lists currently-visible mako notifications. Selecting one copies its
"summary\\nbody" to the clipboard and dismisses it via `makoctl dismiss
-n <id>`. The picker re-opens after each selection so multiple
notifications can be processed in one go; Esc closes it.
"""

from __future__ import annotations

import re
import subprocess
from pathlib import Path
from typing import Any

RECORD_RE = re.compile(r"^Notification (\d+):\s?(.*)$")
FIELD_RE = re.compile(r"^  ([A-Za-z][A-Za-z ]*?):\s*(.*)$")


def list_notifications() -> list[dict[str, Any]]:
    try:
        out = subprocess.run(
            ["makoctl", "list"], capture_output=True, text=True, check=True
        ).stdout
    except (subprocess.CalledProcessError, FileNotFoundError):
        return []
    notifs: list[dict[str, Any]] = []
    cur: dict[str, Any] | None = None
    last_field: str | None = None
    for line in out.splitlines():
        m = RECORD_RE.match(line)
        if m:
            if cur is not None:
                notifs.append(cur)
            cur = {"id": int(m.group(1)), "summary": m.group(2).strip()}
            last_field = "summary"
            continue
        if cur is None:
            continue
        m = FIELD_RE.match(line)
        if m:
            key = m.group(1).strip().lower().replace(" ", "_")
            cur[key] = m.group(2)
            last_field = key
            continue
        if last_field == "body" and line.startswith("        "):
            cur["body"] = (str(cur.get("body", "")) + " " + line.strip()).strip()
    if cur is not None:
        notifs.append(cur)
    return notifs


def fmt_line(n: dict[str, Any]) -> str:
    app = (str(n.get("app_name") or "?")).strip() or "?"
    summary = str(n.get("summary") or "").strip()
    body = str(n.get("body") or "").strip()
    text = summary if not body else f"{summary}{body}"
    text = text.replace("\t", " ").replace("\r", " ")
    return f"[{app}] {text}\t#{n['id']}"


def parse_selection(line: str) -> int | None:
    m = re.search(r"\t#(\d+)\s*$", line)
    return int(m.group(1)) if m else None


def run_wofi(input_text: str, lines: int) -> str:
    style = Path.home() / ".config/wofi/style.css"
    cmd = [
        "wofi",
        "--dmenu",
        "--hide-search",
        "--prompt",
        "Notifications",
        "--lines",
        str(lines),
    ]
    if style.exists():
        cmd += ["--style", str(style)]
    proc = subprocess.run(
        cmd, input=input_text, text=True, capture_output=True, check=False
    )
    return proc.stdout.strip()


def main() -> None:
    while True:
        notifs = list_notifications()
        if not notifs:
            _ = run_wofi("(no notifications)\n", 1)
            return

        lines_text = "\n".join(fmt_line(n) for n in notifs) + "\n"
        sel = run_wofi(lines_text, min(len(notifs), 15))
        if not sel or sel.startswith("(no "):
            return

        nid = parse_selection(sel)
        if nid is None:
            return
        notif = next((n for n in notifs if n["id"] == nid), None)
        if notif is None:
            return

        summary = str(notif.get("summary") or "").strip()
        body = str(notif.get("body") or "").strip()
        clip_text = f"{summary}\n{body}".strip()
        _ = subprocess.run(["wl-copy"], input=clip_text, text=True, check=False)
        _ = subprocess.run(["makoctl", "dismiss", "-n", str(nid)], check=False)


if __name__ == "__main__":
    main()