Source code for qtile_expanded.widgets.notification_bell

"""
Notification Bell Widget for Qtile.

A widget that displays a bell icon which changes color based on notification status:
- Gray when there are no notifications
- Red with a count badge when there are notifications

This widget listens to DBus notifications to track notification count.
Optionally persists notification count across config reloads using StateStorage.
Clicking the bell opens a full-screen popup showing all notifications with a trash bin to clear them.

The widget can use a shared NotificationCenter instance or manage its own notifications.
"""

import time
from datetime import datetime

from libqtile.widget import base
from libqtile.log_utils import logger


[docs] class NotificationBell(base._TextBox): """ A bell icon widget that indicates notification status. The icon is gray (no notifications) or red (with count) when notifications exist. Usage in config: from qtile_expanded.widgets import NotificationBell widget_list = [ NotificationBell(), # ... other widgets ] Attributes: notifications_count: Current count of notifications (default: 0) notifications: List of notification dicts (default: []) icon_no_notifications: Icon to display when no notifications (default: bell outline) icon_with_notifications: Icon to display when notifications exist (default: bell outline) color_no_notifications: Color when no notifications (default: "#888888") color_with_notifications: Color when notifications exist (default: "#ff0000") show_count: Whether to show notification count badge (default: True) count_format: Format string for count display (default: "{count}") popup_enabled: Whether clicking opens a popup (default: True) use_notification_center: Use shared NotificationCenter (default: False) """ defaults = [ ("notifications_count", 0, "Current count of notifications."), ("notifications", [], "List of notification dicts."), ("icon_no_notifications", "\u23fa", "Icon when no notifications (FA bell outline)."), ("icon_with_notifications", "\u23fa", "Icon when notifications exist (FA bell outline)."), ("color_no_notifications", "#888888", "Color when no notifications."), ("color_with_notifications", "#ff0000", "Color when notifications exist."), ("show_count", True, "Whether to show notification count badge."), ("count_format", "{count}", "Format string for count display."), ("font", "FontAwesome", "Font to use for icons."), ("fontsize", 14, "Font size."), ("padding", 3, "Padding around the icon."), ("persist_state", True, "Whether to persist notifications across reloads."), ("storage_namespace", "notification_bell", "Namespace for state storage."), ("popup_enabled", True, "Whether clicking opens a popup."), ("popup_background", "#1a1a2e", "Popup background color."), ("popup_foreground", "#eeeeee", "Popup text color."), ("popup_max_notifications", 10, "Max notifications to show in popup."), ("click_behavior", "toggle", "Click behavior: 'toggle' or 'open'. 'toggle' opens/closes popup, 'open' always opens."), ("use_notification_center", False, "Use shared NotificationCenter instance instead of own state."), ]
[docs] def __init__(self, **config): """Initialize the NotificationBell widget.""" super().__init__("", **config) self.add_defaults(NotificationBell.defaults) self.notifications_count = 0 self.notifications = [] self.storage = None self.popup = None self.qtile = None self._notification_center = None # Use NotificationCenter or own state if self.use_notification_center: self._setup_notification_center() else: # Set up own state persistence if enabled if self.persist_state: self._setup_storage() # Try to connect to notification daemon via DBus self._setup_dbus_listener()
def _setup_notification_center(self): """Set up connection to NotificationCenter.""" try: from qtile_expanded.extensions.notification_center import NotificationCenter self._notification_center = NotificationCenter.get_instance() # Sync initial state self._sync_from_center() except ImportError: logger.warning( "NotificationBell: qtile_expanded.extensions.notification_center not available, " "falling back to own state" ) self._notification_center = None # Fall back to own state if self.persist_state: self._setup_storage() self._setup_dbus_listener() def _sync_from_center(self): """Sync notifications from NotificationCenter.""" if self._notification_center: self.notifications = list(self._notification_center.notifications) self.notifications_count = len(self.notifications) self.update_text() def _setup_storage(self): """Set up persistent storage for notifications.""" try: from qtile_expanded.storage import StateStorage self.storage = StateStorage(self.storage_namespace) # Load persisted notifications self.notifications = self.storage.get("notifications", []) self.notifications_count = len(self.notifications) self.update_text() logger.info( f"NotificationBell: Loaded {self.notifications_count} notifications from storage" ) except ImportError: logger.warning( "NotificationBell: qtile_expanded.storage not available, " "state persistence disabled" ) self.storage = None except Exception as e: logger.warning(f"NotificationBell: Could not set up storage: {e}") self.storage = None def _save_state(self): """Save notifications to persistent storage.""" if self.storage is not None: self.storage.set("notifications", self.notifications) def _setup_dbus_listener(self): """Set up DBus listener for notifications.""" # If using NotificationCenter, it already handles DBus if self.use_notification_center and self._notification_center: return try: import dbus from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) bus = dbus.SessionBus() # Listen to org.freedesktop.Notifications interface bus.add_signal_receiver( self._on_notification_received, signal_name="Notify", dbus_interface="org.freedesktop.Notifications", ) logger.info("NotificationBell: DBus notification listener started") except ImportError: logger.warning( "NotificationBell: dbus-python not installed. " "Notification count will not update automatically. " "You can still set notifications manually." ) except Exception as e: logger.warning(f"NotificationBell: Could not set up DBus listener: {e}") def _on_notification_received(self, *args, **kwargs): """Handle incoming notification signal from DBus.""" # If using NotificationCenter, it already handles notifications if self.use_notification_center and self._notification_center: self._sync_from_center() return # args typically: app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout # Extract notification data from args app_name = args[0] if len(args) > 0 else "Unknown" replaces_id = args[1] if len(args) > 1 else 0 app_icon = args[2] if len(args) > 2 else None summary = args[3] if len(args) > 3 else "" body = args[4] if len(args) > 4 else "" hints = args[6] if len(args) > 6 else {} # Create notification dict notification = { "app_name": app_name, "summary": summary, "body": body, "icon": app_icon, "urgency": 1, # Default urgency "timestamp": datetime.now().isoformat(), "id": replaces_id or int(time.time() * 1000), } # Check for urgency in hints if hints and "urgency" in hints: try: notification["urgency"] = int(hints["urgency"]) except (ValueError, TypeError): pass # Add to notifications list self.notifications.append(notification) self.notifications_count = len(self.notifications) self.update_text() self._save_state() logger.debug(f"NotificationBell: Received notification from {app_name}, count={self.notifications_count}")
[docs] def add_notification(self, summary="", body="", app_name="", **kwargs): """ Manually add a notification. Useful when DBus is not available. Args: summary: Notification summary/title body: Notification body text app_name: Application name **kwargs: Additional notification data (icon, urgency, etc.) """ # If using NotificationCenter, delegate to it if self.use_notification_center and self._notification_center: self._notification_center.add_notification( summary=summary, body=body, app_name=app_name, **kwargs ) self._sync_from_center() return notification = { "app_name": app_name, "summary": summary, "body": body, "timestamp": datetime.now().isoformat(), "id": int(time.time() * 1000) + len(self.notifications), } notification.update(kwargs) self.notifications.append(notification) self.notifications_count = len(self.notifications) self.update_text() self._save_state()
[docs] def clear_notifications(self): """Clear all notifications.""" if self.use_notification_center and self._notification_center: self._notification_center.clear() self._sync_from_center() else: self.notifications = [] self.notifications_count = 0 self.update_text() self._save_state() # Close popup if open if self.popup and hasattr(self.popup, 'win') and self.popup.win.visible: self.popup.hide()
[docs] def set_notifications(self, notifications): """ Set the notifications list directly. Args: notifications: List of notification dicts """ self.notifications = list(notifications) if notifications else [] self.notifications_count = len(self.notifications) self.update_text() self._save_state()
[docs] def dismiss_notification(self, index): """ Dismiss a specific notification by index. Args: index: Index of notification to dismiss """ if self.use_notification_center and self._notification_center: return self._notification_center.dismiss_notification(index) if 0 <= index < len(self.notifications): del self.notifications[index] self.notifications_count = len(self.notifications) self.update_text() self._save_state() return True return False
[docs] def update_text(self): """Update the displayed text based on notification count.""" if self.notifications_count > 0: icon = self.icon_with_notifications color = self.color_with_notifications if self.show_count: count_str = self.count_format.format(count=self.notifications_count) text = f"{icon} {count_str}" else: text = icon else: icon = self.icon_no_notifications color = self.color_no_notifications text = icon self.text = text self.color = color self.draw()
[docs] def button_press(self, x, y, button): """Handle button press events.""" if button == 1: # Left click if self.popup_enabled and self.notifications_count > 0: self._toggle_popup() elif self.click_behavior == "open": self._show_popup() else: # Clear notifications on click (if no popup or count is 0) self.clear_notifications() return True elif button == 3: # Right click # Clear all notifications self.clear_notifications() return True return False
def _show_popup(self): """Show the notification popup.""" if not self.popup_enabled: return # Get qtile instance if not already set if self.qtile is None: from libqtile import qtile as q self.qtile = q # Use NotificationCenter popup if available if self.use_notification_center and self._notification_center: self._notification_center.show_popup( self.qtile, background=self.popup_background, foreground=self.popup_foreground, max_notifications=self.popup_max_notifications, ) return # Create popup if doesn't exist if self.popup is None: from qtile_expanded.extensions.popup import NotificationPopup self.popup = NotificationPopup( self.qtile, notifications=self.notifications, on_close=self._on_popup_close, on_clear=self.clear_notifications, on_dismiss=self.dismiss_notification, background=self.popup_background, foreground=self.popup_foreground, max_notifications=self.popup_max_notifications, ) # Update notifications and show self.popup.update_notifications(self.notifications) self.popup.show() def _hide_popup(self): """Hide the notification popup.""" if self.popup and hasattr(self.popup, 'win') and self.popup.win.visible: self.popup.hide() elif self.use_notification_center and self._notification_center: self._notification_center.hide_popup() def _toggle_popup(self): """Toggle the notification popup.""" if self.popup and hasattr(self.popup, 'win') and self.popup.win.visible: self._hide_popup() else: self._show_popup() def _on_popup_close(self): """Handle popup close event.""" pass
[docs] def calculate_length(self): """Calculate the length of the widget text.""" return len(self.text) * self.fontsize + self.padding * 2
[docs] def info(self): """Return info about the widget for debugging.""" return dict( name=self.__class__.__name__, notifications_count=self.notifications_count, notifications=self.notifications, )