add sync notifications

This commit is contained in:
Tim Bendt
2025-08-06 15:35:40 -05:00
parent 8881b6933b
commit 5eddddc8ec
3 changed files with 133 additions and 3 deletions

View File

@@ -1,12 +1,12 @@
import click
import asyncio
import os
import sys
from rich.progress import Progress, SpinnerColumn, MofNCompleteColumn
from datetime import datetime, timedelta
from src.utils.mail_utils.helpers import ensure_directory_exists
from src.utils.calendar_utils import save_events_to_vdir, save_events_to_file
from src.utils.notifications import notify_new_emails
from src.services.microsoft_graph.calendar import (
fetch_calendar_events,
sync_local_calendar_changes,
@@ -103,7 +103,7 @@ async def fetch_calendar_async(
events, f"{ics_path}/events_latest.ics", progress, task_id, dry_run
)
progress.console.print(
f"[green]Finished saving events to ICS file[/green]"
"[green]Finished saving events to ICS file[/green]"
)
else:
# No destination specified
@@ -220,6 +220,7 @@ async def _sync_outlook_data(
continue_iteration,
download_attachments,
two_way_calendar,
notify,
):
"""Synchronize data from external sources."""
@@ -297,6 +298,20 @@ async def _sync_outlook_data(
progress.console.print(
"\n[bold cyan]Step 2: Fetching new data from server...[/bold cyan]"
)
# Track messages before sync for notifications
maildir_path = (
os.getenv("MAILDIR_PATH", os.path.expanduser("~/Mail")) + f"/{org}"
)
messages_before = 0
if notify:
new_dir = os.path.join(maildir_path, "new")
cur_dir = os.path.join(maildir_path, "cur")
if os.path.exists(new_dir):
messages_before += len([f for f in os.listdir(new_dir) if ".eml" in f])
if os.path.exists(cur_dir):
messages_before += len([f for f in os.listdir(cur_dir) if ".eml" in f])
await asyncio.gather(
fetch_mail_async(
maildir_path,
@@ -320,6 +335,19 @@ async def _sync_outlook_data(
continue_iteration,
),
)
# Send notification for new emails if enabled
if notify and not dry_run:
messages_after = 0
if os.path.exists(new_dir):
messages_after += len([f for f in os.listdir(new_dir) if ".eml" in f])
if os.path.exists(cur_dir):
messages_after += len([f for f in os.listdir(cur_dir) if ".eml" in f])
new_message_count = messages_after - messages_before
if new_message_count > 0:
notify_new_emails(new_message_count, org)
progress.console.print("[bold green]Step 2: New data fetched.[/bold green]")
click.echo("Sync complete.")
@@ -380,6 +408,12 @@ async def _sync_outlook_data(
help="Run in daemon mode.",
default=False,
)
@click.option(
"--notify",
is_flag=True,
help="Send macOS notifications for new email messages",
default=False,
)
def sync(
dry_run,
vdir,
@@ -391,6 +425,7 @@ def sync(
download_attachments,
two_way_calendar,
daemon,
notify,
):
if daemon:
asyncio.run(
@@ -404,6 +439,7 @@ def sync(
continue_iteration,
download_attachments,
two_way_calendar,
notify,
)
)
else:
@@ -418,6 +454,7 @@ def sync(
continue_iteration,
download_attachments,
two_way_calendar,
notify,
)
)
@@ -498,13 +535,13 @@ async def daemon_mode(
continue_iteration,
download_attachments,
two_way_calendar,
notify,
):
"""
Run the script in daemon mode, periodically syncing emails and calendar.
"""
from src.services.microsoft_graph.mail import get_inbox_count_async
from rich.console import Console
from rich.live import Live
from rich.panel import Panel
from rich.text import Text
from datetime import datetime
@@ -549,6 +586,7 @@ async def daemon_mode(
continue_iteration,
download_attachments,
two_way_calendar,
notify,
)
last_sync_time = time.time()
@@ -625,6 +663,7 @@ async def daemon_mode(
continue_iteration,
download_attachments,
two_way_calendar,
notify,
)
last_sync_time = time.time()
console.print(create_status_display("Sync completed ✅", "green"))

View File

@@ -0,0 +1,90 @@
"""
macOS notification utilities for GTD terminal tools.
"""
import subprocess
import platform
from typing import Optional
def send_notification(
title: str,
message: str,
subtitle: Optional[str] = None,
sound: Optional[str] = None,
) -> bool:
"""
Send a macOS notification using osascript.
Args:
title: The notification title
message: The notification message body
subtitle: Optional subtitle
sound: Optional sound name (e.g., "default", "Glass", "Ping")
Returns:
bool: True if notification was sent successfully, False otherwise
"""
if platform.system() != "Darwin":
return False
try:
# Escape quotes for AppleScript string literals
def escape_applescript_string(text: str) -> str:
return text.replace("\\", "\\\\").replace('"', '\\"')
escaped_title = escape_applescript_string(title)
escaped_message = escape_applescript_string(message)
# Build the AppleScript command
script_parts = [
f'display notification "{escaped_message}"',
f'with title "{escaped_title}"',
]
if subtitle:
escaped_subtitle = escape_applescript_string(subtitle)
script_parts.append(f'subtitle "{escaped_subtitle}"')
if sound:
escaped_sound = escape_applescript_string(sound)
script_parts.append(f'sound name "{escaped_sound}"')
script = " ".join(script_parts)
# Execute the notification by passing script through stdin
subprocess.run(
["osascript"], input=script, check=True, capture_output=True, text=True
)
return True
except subprocess.CalledProcessError:
return False
except Exception:
return False
except Exception:
return False
except Exception:
return False
def notify_new_emails(count: int, org: str = ""):
"""
Send notification about new email messages.
Args:
count: Number of new messages
org: Organization name (optional)
"""
if count <= 0:
return
if count == 1:
title = "New Email"
message = "You have 1 new message"
else:
title = "New Emails"
message = f"You have {count} new messages"
subtitle = f"from {org}" if org else None
send_notification(title=title, message=message, subtitle=subtitle, sound="default")