-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathnotifier.py
More file actions
146 lines (124 loc) · 4.76 KB
/
notifier.py
File metadata and controls
146 lines (124 loc) · 4.76 KB
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""Windows toast notifications and reminder/appointment scheduler thread."""
import subprocess
import threading
import time
from datetime import datetime
from logger import log
import config
import database as db
import locales
from brand import get_notification_icon_path
# Try multiple notification backends in order of preference
_backend = None
try:
from winotify import Notification as _WinotifyNotification
_backend = "winotify"
except ImportError:
pass
if _backend is None:
try:
from plyer import notification as _plyer
_backend = "plyer"
except ImportError:
pass
def _send_toast(title: str, message: str):
"""Show a Windows toast notification using the best available backend."""
ico = get_notification_icon_path()
if _backend == "winotify":
try:
toast = _WinotifyNotification(
app_id="Writher",
title=title,
msg=message,
duration="long",
icon=ico,
)
toast.show()
log.info("Toast sent via winotify.")
return
except Exception as exc:
log.warning("winotify failed: %s", exc)
if _backend == "plyer":
try:
_plyer.notify(
title=title,
message=message,
app_name="Writher",
timeout=10,
)
log.info("Toast sent via plyer.")
return
except Exception as exc:
log.warning("plyer failed: %s", exc)
# Fallback: simple PowerShell BurntToast or basic balloon tip
try:
ps = (
f'Add-Type -AssemblyName System.Windows.Forms; '
f'$n = New-Object System.Windows.Forms.NotifyIcon; '
f'$n.Icon = [System.Drawing.SystemIcons]::Information; '
f'$n.Visible = $true; '
f'$n.ShowBalloonTip(10000, "{_ps_escape(title)}", '
f'"{_ps_escape(message)}", '
f'[System.Windows.Forms.ToolTipIcon]::Info); '
f'Start-Sleep -Seconds 12; '
f'$n.Dispose()'
)
subprocess.Popen(
["powershell", "-WindowStyle", "Hidden", "-Command", ps],
creationflags=0x08000000, # CREATE_NO_WINDOW
)
log.info("Toast sent via PowerShell balloon.")
except Exception as exc:
log.warning("All notification methods failed: %s", exc)
def _ps_escape(s: str) -> str:
"""Escape string for safe embedding in PowerShell double-quoted string."""
return s.replace('"', '`"').replace("'", "`'").replace("\n", " ")
class ReminderScheduler:
"""Background thread that checks for due reminders and upcoming
appointments every 30 seconds."""
def __init__(self):
self._stop = threading.Event()
self._thread = None
def start(self):
self._thread = threading.Thread(target=self._loop, daemon=True)
self._thread.start()
def stop(self):
self._stop.set()
def _loop(self):
while not self._stop.is_set():
self._check_reminders()
self._check_appointments()
self._stop.wait(30)
def _check_reminders(self):
"""Fire toast for reminders that are due."""
try:
pending = db.get_pending_reminders()
for rem in pending:
_send_toast(locales.get("reminder_toast_title"), rem["message"])
db.mark_reminder_notified(rem["id"])
log.info("Reminder notified: %s", rem["message"])
except Exception as exc:
log.error("Reminder scheduler error: %s", exc)
def _check_appointments(self):
"""Fire toast for appointments within the configured lead time."""
try:
lead = getattr(config, "APPOINTMENT_REMIND_MINUTES", 15)
upcoming = db.get_upcoming_appointments(within_minutes=lead)
now = datetime.now()
for appt in upcoming:
try:
appt_dt = datetime.fromisoformat(appt["dt"])
delta_min = max(0, int((appt_dt - now).total_seconds() / 60))
except (ValueError, TypeError):
delta_min = 0
title = appt.get("title", "")
if delta_min <= 0:
body = locales.get("appointment_toast_now", title=title)
else:
body = locales.get("appointment_toast_body",
title=title, minutes=delta_min)
_send_toast(locales.get("appointment_toast_title"), body)
db.mark_appointment_notified(appt["id"])
log.info("Appointment notified: %s (in %d min)", title, delta_min)
except Exception as exc:
log.error("Appointment scheduler error: %s", exc)