OpenAdapt Tray uses desktop-notifier for modern, native notifications across all platforms. This provides a rich notification experience with support for callbacks, action buttons, and platform-specific features.
The notification system is implemented in src/openadapt_tray/notifications.py and provides:
- Cross-platform support: Works on macOS, Windows, and Linux
- Native notifications: Uses platform-native notification centers
- Click callbacks: Respond to user interactions
- Action buttons: Add interactive buttons (platform-dependent)
- Reply fields: Support text replies on macOS
- Urgency levels: Normal, low, and critical priorities
- Graceful fallback: Falls back to AppleScript/PowerShell if desktop-notifier unavailable
from openadapt_tray.notifications import NotificationManager
# Initialize
notifications = NotificationManager()
# Show a simple notification
notifications.show(
title="OpenAdapt",
body="Recording started successfully!"
)
# Cleanup when done
notifications.cleanup()Respond when users click on notifications:
def on_notification_clicked():
print("User clicked the notification!")
# Open dashboard, show details, etc.
notifications.show(
title="Recording Complete",
body="Click to view details",
on_clicked=on_notification_clicked
)Control notification priority:
# Low priority
notifications.show(
title="Info",
body="Background task complete",
urgency="low"
)
# Critical priority (stays visible longer, may bypass Do Not Disturb)
notifications.show(
title="Error",
body="Recording failed!",
urgency="critical"
)Add interactive buttons to notifications:
def on_click():
print("User interacted with notification")
notifications.show(
title="Training Complete",
body="Choose what to do next",
buttons=["View Results", "Start New Training", "Dismiss"],
on_clicked=on_click
)Note: Action button support varies by platform:
- macOS: Full support via Notification Center
- Windows: Limited support
- Linux: Depends on desktop environment
Add text input to notifications on macOS:
notifications.show(
title="Name this recording",
body="Enter a name for your capture",
reply_field="Recording name",
on_clicked=lambda: print("User submitted reply")
)Display custom icons with notifications:
notifications.show(
title="OpenAdapt",
body="Custom icon notification",
icon_path="/path/to/icon.png"
)For async contexts, use the async interface:
import asyncio
async def send_notification():
await notifications.show_async(
title="Async Notification",
body="Sent from async context"
)
asyncio.run(send_notification())The NotificationManager automatically selects the best backend:
-
desktop-notifier (preferred): If available, uses native notification APIs
- macOS: Notification Center framework
- Windows: WinRT Python bridge
- Linux: DBus (org.freedesktop.Notifications)
-
Fallback implementations: If desktop-notifier unavailable
- macOS: AppleScript
display notification - Windows: PowerShell toast notifications or pystray
- Linux:
notify-sendcommand
- macOS: AppleScript
desktop-notifier requires asyncio. The NotificationManager:
- Creates/reuses an event loop automatically
- Handles both sync and async contexts
- Cleans up resources on shutdown
# The event loop is managed internally
notifications = NotificationManager()
# Works in sync contexts
notifications.show("Title", "Body")
# Also works in async contexts
await notifications.show_async("Title", "Body")
# Clean up when done
notifications.cleanup()The notification system integrates seamlessly with the tray app:
# In app.py
class TrayApplication:
def __init__(self):
self.notifications = NotificationManager()
# ... other setup
def _on_state_change(self, state: AppState):
if self.config.show_notifications:
self.notifications.show(
title="State Changed",
body=f"Now {state.state}"
)
def quit(self):
self.notifications.cleanup()
# ... other cleanupNotifications can be configured via tray.json:
{
"show_notifications": true,
"notification_duration_ms": 5000
}show_notifications: Enable/disable notifications (default: true)notification_duration_ms: Advisory duration in milliseconds (default: 5000)
Note: The actual display duration is controlled by the OS and may differ.
-
Signing requirement: Python must be properly signed for notifications on macOS 10.14+
- Official python.org installer: Works out of the box
- Homebrew Python: May not show notifications (unsigned)
-
Notification Center: Notifications appear in Notification Center
-
Do Not Disturb: Respects system DND settings (except critical urgency)
-
Grouping: Notifications group by app name ("OpenAdapt")
- Windows 10+: Full support via WinRT
- Older Windows: Falls back to PowerShell or pystray
- Action Center: Notifications appear in Action Center
- Focus Assist: Respects Focus Assist settings
- Requirements: Requires
notify-sendor compatible notification daemon - Desktop environments: Works with GNOME, KDE, XFCE, etc.
- Fallback: Uses
notify-sendcommand if desktop-notifier unavailable
Test notifications manually:
# Run the test script
cd /Users/abrichr/oa/src/openadapt-tray
python test_notification_simple.pyTest script shows:
- Basic notification
- Critical notification
- Notification with callback
Check your system's Notification Center to verify notifications appear correctly.
-
Check Python signing:
codesign -dv /path/to/python
-
Use official python.org installer instead of Homebrew
-
Check Notification Center settings:
- System Preferences → Notifications
- Ensure Python/Terminal is allowed
- Check Focus Assist settings
- Ensure Windows 10+ or fallback dependencies available
- Check Action Center settings
-
Install notification daemon:
# Ubuntu/Debian sudo apt install libnotify-bin # Fedora sudo dnf install libnotify
-
Check desktop environment supports notifications
Initialize the notification manager. Automatically detects the best backend.
show(title, body, icon_path=None, duration_ms=5000, on_clicked=None, urgency="normal", buttons=None, reply_field=None) -> bool
Show a notification.
Parameters:
title(str): Notification titlebody(str): Notification body texticon_path(str, optional): Path to icon imageduration_ms(int): Advisory duration in millisecondson_clicked(callable, optional): Callback when notification is clickedurgency(str): "low", "normal", or "critical"buttons(list, optional): List of button labelsreply_field(str, optional): Reply field placeholder (macOS only)
Returns: bool - True if successful
Async version of show(). Use in async contexts.
Set pystray icon for Windows fallback notifications.
Clean up resources. Call when shutting down the application.
If migrating from the old AppleScript/pystray implementation:
# Basic usage still works
notifications.show("Title", "Body")
notifications.show("Title", "Body", duration_ms=3000)# Now you can also use:
notifications.show(
"Title",
"Body",
on_clicked=callback,
urgency="critical",
buttons=["Action 1", "Action 2"]
)The API is backward compatible - existing code continues to work without changes.
Potential improvements:
- Notification history: Track and replay recent notifications
- Custom sounds: Per-notification sound support
- Rich media: Images and videos in notification body
- Progress notifications: Show progress bars in notifications
- Scheduled notifications: Send notifications at specific times