add sync notifications
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
[tools]
|
[tools]
|
||||||
|
node = "22.17.1"
|
||||||
uv = 'latest'
|
uv = 'latest'
|
||||||
|
|
||||||
[settings]
|
[settings]
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import click
|
import click
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
from rich.progress import Progress, SpinnerColumn, MofNCompleteColumn
|
from rich.progress import Progress, SpinnerColumn, MofNCompleteColumn
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from src.utils.mail_utils.helpers import ensure_directory_exists
|
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.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 (
|
from src.services.microsoft_graph.calendar import (
|
||||||
fetch_calendar_events,
|
fetch_calendar_events,
|
||||||
sync_local_calendar_changes,
|
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
|
events, f"{ics_path}/events_latest.ics", progress, task_id, dry_run
|
||||||
)
|
)
|
||||||
progress.console.print(
|
progress.console.print(
|
||||||
f"[green]Finished saving events to ICS file[/green]"
|
"[green]Finished saving events to ICS file[/green]"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# No destination specified
|
# No destination specified
|
||||||
@@ -220,6 +220,7 @@ async def _sync_outlook_data(
|
|||||||
continue_iteration,
|
continue_iteration,
|
||||||
download_attachments,
|
download_attachments,
|
||||||
two_way_calendar,
|
two_way_calendar,
|
||||||
|
notify,
|
||||||
):
|
):
|
||||||
"""Synchronize data from external sources."""
|
"""Synchronize data from external sources."""
|
||||||
|
|
||||||
@@ -297,6 +298,20 @@ async def _sync_outlook_data(
|
|||||||
progress.console.print(
|
progress.console.print(
|
||||||
"\n[bold cyan]Step 2: Fetching new data from server...[/bold cyan]"
|
"\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(
|
await asyncio.gather(
|
||||||
fetch_mail_async(
|
fetch_mail_async(
|
||||||
maildir_path,
|
maildir_path,
|
||||||
@@ -320,6 +335,19 @@ async def _sync_outlook_data(
|
|||||||
continue_iteration,
|
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]")
|
progress.console.print("[bold green]Step 2: New data fetched.[/bold green]")
|
||||||
click.echo("Sync complete.")
|
click.echo("Sync complete.")
|
||||||
|
|
||||||
@@ -380,6 +408,12 @@ async def _sync_outlook_data(
|
|||||||
help="Run in daemon mode.",
|
help="Run in daemon mode.",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--notify",
|
||||||
|
is_flag=True,
|
||||||
|
help="Send macOS notifications for new email messages",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
def sync(
|
def sync(
|
||||||
dry_run,
|
dry_run,
|
||||||
vdir,
|
vdir,
|
||||||
@@ -391,6 +425,7 @@ def sync(
|
|||||||
download_attachments,
|
download_attachments,
|
||||||
two_way_calendar,
|
two_way_calendar,
|
||||||
daemon,
|
daemon,
|
||||||
|
notify,
|
||||||
):
|
):
|
||||||
if daemon:
|
if daemon:
|
||||||
asyncio.run(
|
asyncio.run(
|
||||||
@@ -404,6 +439,7 @@ def sync(
|
|||||||
continue_iteration,
|
continue_iteration,
|
||||||
download_attachments,
|
download_attachments,
|
||||||
two_way_calendar,
|
two_way_calendar,
|
||||||
|
notify,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -418,6 +454,7 @@ def sync(
|
|||||||
continue_iteration,
|
continue_iteration,
|
||||||
download_attachments,
|
download_attachments,
|
||||||
two_way_calendar,
|
two_way_calendar,
|
||||||
|
notify,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -498,13 +535,13 @@ async def daemon_mode(
|
|||||||
continue_iteration,
|
continue_iteration,
|
||||||
download_attachments,
|
download_attachments,
|
||||||
two_way_calendar,
|
two_way_calendar,
|
||||||
|
notify,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Run the script in daemon mode, periodically syncing emails and calendar.
|
Run the script in daemon mode, periodically syncing emails and calendar.
|
||||||
"""
|
"""
|
||||||
from src.services.microsoft_graph.mail import get_inbox_count_async
|
from src.services.microsoft_graph.mail import get_inbox_count_async
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.live import Live
|
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -549,6 +586,7 @@ async def daemon_mode(
|
|||||||
continue_iteration,
|
continue_iteration,
|
||||||
download_attachments,
|
download_attachments,
|
||||||
two_way_calendar,
|
two_way_calendar,
|
||||||
|
notify,
|
||||||
)
|
)
|
||||||
last_sync_time = time.time()
|
last_sync_time = time.time()
|
||||||
|
|
||||||
@@ -625,6 +663,7 @@ async def daemon_mode(
|
|||||||
continue_iteration,
|
continue_iteration,
|
||||||
download_attachments,
|
download_attachments,
|
||||||
two_way_calendar,
|
two_way_calendar,
|
||||||
|
notify,
|
||||||
)
|
)
|
||||||
last_sync_time = time.time()
|
last_sync_time = time.time()
|
||||||
console.print(create_status_display("Sync completed ✅", "green"))
|
console.print(create_status_display("Sync completed ✅", "green"))
|
||||||
|
|||||||
90
src/utils/notifications.py
Normal file
90
src/utils/notifications.py
Normal 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")
|
||||||
Reference in New Issue
Block a user