From d53341a0c0134477249603bc46fec56e4ab2af95 Mon Sep 17 00:00:00 2001 From: tr1x_em Date: Thu, 14 Aug 2025 22:28:34 +0530 Subject: [PATCH] Add escape_markup_text() utility function to safely escape special characters --- modules/notification/notification.py | 14 ++-- modules/notification/notification_center.py | 73 ++++++++++++++------- modules/notification/unified_cache.py | 21 ++++-- styles/widgets.css | 14 ++-- utils/functions.py | 19 ++++++ 5 files changed, 95 insertions(+), 46 deletions(-) diff --git a/modules/notification/notification.py b/modules/notification/notification.py index fde83ca..77be746 100644 --- a/modules/notification/notification.py +++ b/modules/notification/notification.py @@ -28,6 +28,7 @@ from fabric.widgets.eventbox import EventBox from fabric.widgets.image import Image from fabric.widgets.label import Label from utils.roam import modus_service +from utils.functions import escape_markup_text from widgets.custom_image import CustomImage from widgets.customrevealer import SlideRevealer from widgets.wayland import WaylandWindow as Window @@ -136,6 +137,7 @@ def get_cached_pixbuf(cache_key, fallback_size=(48, 48), cache_dir=None): try: cache_path = os.path.join(cache_dir, f"{cache_key}.png") if os.path.exists(cache_path): + logger.debug(f"Using cached notification icon: {cache_key}") logger.debug(f"Using cached notification icon: {cache_key}") return GdkPixbuf.Pixbuf.new_from_file_at_scale( cache_path, fallback_size[0], fallback_size[1], True @@ -159,7 +161,6 @@ def cache_notification_icon(source, size=(48, 48), app_name=None): cache_key, fallback_size=size, cache_dir=NOTIFICATION_ICON_CACHE_DIR ) if cached_pixbuf: - logger.debug(f"Cache hit for icon: {cache_key}") return cached_pixbuf # Load, cache, and return icon in one optimized flow @@ -185,7 +186,6 @@ def cache_notification_icon(source, size=(48, 48), app_name=None): cache_key, fallback_size=size, cache_dir=NOTIFICATION_ICON_CACHE_DIR ) if cached_pixbuf: - logger.debug(f"Cache hit for pixbuf: {cache_key}") return cached_pixbuf # Scale once and cache immediately @@ -193,7 +193,6 @@ def cache_notification_icon(source, size=(48, 48), app_name=None): size[0], size[1], GdkPixbuf.InterpType.BILINEAR ) save_pixbuf_to_cache(scaled_pixbuf, cache_key, NOTIFICATION_ICON_CACHE_DIR) - logger.debug(f"Cached new pixbuf icon: {cache_key}") return scaled_pixbuf except Exception as e: @@ -305,7 +304,6 @@ def get_cached_notification_image(cache_key): try: cache_path = os.path.join(NOTIFICATION_IMAGE_CACHE_DIR, f"{cache_key}.png") if os.path.exists(cache_path): - logger.debug(f"Using cached notification image: {cache_key}") return GdkPixbuf.Pixbuf.new_from_file(cache_path) except Exception as e: logger.warning(f"Failed to load cached notification image: {e}") @@ -495,7 +493,6 @@ def preload_notification_assets(notification): try: # Only cache at 35x35 to reduce disk usage - headers will scale this down cache_notification_icon(notification.app_icon, (35, 35)) - logger.debug(f"Preloaded app icon (35x35) for: {notification.app_name}") except Exception as icon_error: logger.debug( f"Failed to preload app icon for {notification.app_name}: { @@ -663,7 +660,7 @@ class NotificationWidget(Box): children=[ Label( name="notification-summary", - markup=notification.summary.replace("\n", " "), + markup=escape_markup_text(notification.summary.replace("\n", " ")), h_align="start", max_chars_width=40, ellipsization="end", @@ -678,7 +675,7 @@ class NotificationWidget(Box): ), ( Label( - markup=notification.body.replace("\n", " "), + markup=escape_markup_text(notification.body.replace("\n", " ")), h_align="start", max_chars_width=45, ellipsization="end", @@ -761,7 +758,6 @@ class NotificationWidget(Box): cache_key = get_notification_image_cache_key(notification_id, notification.image_pixbuf) cached_image = get_cached_notification_image(cache_key) if cached_image: - logger.debug(f"Using cached notification image: {cache_key[:8]}") return cached_image except Exception as image_error: logger.debug(f"Notification image processing failed: {image_error}") @@ -776,13 +772,11 @@ class NotificationWidget(Box): if app_icon_source: cached_app_icon = cache_notification_icon(app_icon_source, (35, 35)) if cached_app_icon: - logger.debug(f"Using cached app icon for: {app_icon_source}") return cached_app_icon except Exception as e: logger.debug(f"Failed to get cached app icon: {e}") # Ultimate fallback - logger.debug("Using fallback notification icon") return get_fallback_notification_icon((35, 35)) def create_action_buttons(self, notification): diff --git a/modules/notification/notification_center.py b/modules/notification/notification_center.py index 0e0e346..3aa77b0 100644 --- a/modules/notification/notification_center.py +++ b/modules/notification/notification_center.py @@ -23,6 +23,7 @@ from modules.notification.notification import ( get_fallback_notification_icon, ) from services.modus import notification_service +from utils.functions import escape_markup_text from widgets.custom_image import CustomImage from widgets.wayland import WaylandWindow as Window from config import data @@ -122,9 +123,9 @@ class ExpandableNotificationGroup(Box): ), Label( name="notification-body", - markup=latest_notification._notification.summary.replace( + markup=escape_markup_text(latest_notification._notification.summary.replace( "\n", " " - ), + )), max_chars_width=25, h_align="start", ellipsization="end", @@ -168,26 +169,41 @@ class ExpandableNotificationGroup(Box): notification_id = getattr(notification, "id", None) # First try to get cached notification image using stored cache key - if hasattr(cached_notification, 'cache_metadata') and cached_notification.cache_metadata: - notification_image_cache_key = cached_notification.cache_metadata.get('notification_image_cache_key') + if ( + hasattr(cached_notification, "cache_metadata") + and cached_notification.cache_metadata + ): + notification_image_cache_key = cached_notification.cache_metadata.get( + "notification_image_cache_key" + ) if notification_image_cache_key: try: - cached_image = get_cached_notification_image(notification_image_cache_key) + cached_image = get_cached_notification_image( + notification_image_cache_key + ) if cached_image: - logger.debug(f"Using cached notification image: {notification_image_cache_key}") + logger.debug( + f"Using cached notification image: {notification_image_cache_key}" + ) return cached_image except Exception as e: logger.debug(f"Failed to load cached notification image: {e}") # Fallback to app icon using cached key - if hasattr(cached_notification, 'cache_metadata') and cached_notification.cache_metadata: - app_icon_cache_key = cached_notification.cache_metadata.get('app_icon_cache_key') + if ( + hasattr(cached_notification, "cache_metadata") + and cached_notification.cache_metadata + ): + app_icon_cache_key = cached_notification.cache_metadata.get( + "app_icon_cache_key" + ) if app_icon_cache_key: try: from modules.notification.unified_cache import get_from_cache + cached_app_icon = get_from_cache(app_icon_cache_key, (35, 35)) if cached_app_icon: - logger.debug(f"Using cached app icon: {app_icon_cache_key}") + # logger.debug(f"Using cached app icon: {app_icon_cache_key}") return cached_app_icon except Exception as e: logger.debug(f"Failed to load cached app icon: {e}") @@ -198,7 +214,9 @@ class ExpandableNotificationGroup(Box): if app_icon_source: cached_app_icon = cache_notification_icon(app_icon_source, (35, 35)) if cached_app_icon: - logger.debug(f"Using directly cached app icon for: {app_icon_source}") + logger.debug( + f"Using directly cached app icon for: {app_icon_source}" + ) return cached_app_icon except Exception as e: logger.debug(f"Failed to get directly cached app icon: {e}") @@ -433,7 +451,7 @@ class ExpandableNotificationGroup(Box): class NotificationCenterWidget(NotificationWidget): def __init__(self, notification, **kwargs): self.notification_id = notification.cache_id - self.cache_metadata = getattr(notification, 'cache_metadata', {}) + self.cache_metadata = getattr(notification, "cache_metadata", {}) super().__init__( notification._notification, @@ -449,25 +467,32 @@ class NotificationCenterWidget(NotificationWidget): # First try to get cached notification image using stored cache key if self.cache_metadata: - notification_image_cache_key = self.cache_metadata.get('notification_image_cache_key') + notification_image_cache_key = self.cache_metadata.get( + "notification_image_cache_key" + ) if notification_image_cache_key: try: - cached_image = get_cached_notification_image(notification_image_cache_key) + cached_image = get_cached_notification_image( + notification_image_cache_key + ) if cached_image: - logger.debug(f"Using cached notification image: {notification_image_cache_key}") + logger.debug( + f"Using cached notification image: {notification_image_cache_key}" + ) return cached_image except Exception as e: logger.debug(f"Failed to load cached notification image: {e}") # Fallback to app icon using cached key if self.cache_metadata: - app_icon_cache_key = self.cache_metadata.get('app_icon_cache_key') + app_icon_cache_key = self.cache_metadata.get("app_icon_cache_key") if app_icon_cache_key: try: from modules.notification.unified_cache import get_from_cache + cached_app_icon = get_from_cache(app_icon_cache_key, (35, 35)) if cached_app_icon: - logger.debug(f"Using cached app icon: {app_icon_cache_key}") + # logger.debug(f"Using cached app icon: {app_icon_cache_key}") return cached_app_icon except Exception as e: logger.debug(f"Failed to load cached app icon: {e}") @@ -478,7 +503,9 @@ class NotificationCenterWidget(NotificationWidget): if app_icon_source: cached_app_icon = cache_notification_icon(app_icon_source, (35, 35)) if cached_app_icon: - logger.debug(f"Using directly cached app icon for: {app_icon_source}") + logger.debug( + f"Using directly cached app icon for: {app_icon_source}" + ) return cached_app_icon except Exception as e: logger.debug(f"Failed to get directly cached app icon: {e}") @@ -527,7 +554,7 @@ class NotificationCenterWidget(NotificationWidget): children=[ Label( name="notification-summary", - markup=notification.summary.replace("\n", " "), + markup=escape_markup_text(notification.summary.replace("\n", " ")), h_align="start", max_chars_width=25, ellipsization="end", @@ -536,7 +563,7 @@ class NotificationCenterWidget(NotificationWidget): ), ( Label( - markup=notification.body.replace("\n", " "), + markup=escape_markup_text(notification.body.replace("\n", " ")), h_align="start", max_chars_width=35, ellipsization="end", @@ -711,7 +738,9 @@ class NotificationCenter(Window): for app_name, notifications in self.notification_groups.items(): # Sort notifications by ID (highest ID first - newest notifications) # This ensures the latest notifications appear at the top of each group - notifications.sort(key=lambda n: getattr(n._notification, "id", 0), reverse=True) + notifications.sort( + key=lambda n: getattr(n._notification, "id", 0), reverse=True + ) # Handle limited apps history - only keep 5 notifications during rebuild if app_name in data.NOTIFICATION_LIMITED_APPS_HISTORY: @@ -739,7 +768,7 @@ class NotificationCenter(Window): # Add to groups in sorted order (highest ID first - maintains newest-first ordering) notifications_list = self.notification_groups[app_name] new_notification_id = getattr(cached_notification._notification, "id", 0) - + # Find the correct position to insert based on ID (highest first) insert_position = 0 for i, existing_notification in enumerate(notifications_list): @@ -748,7 +777,7 @@ class NotificationCenter(Window): insert_position = i break insert_position = i + 1 - + notifications_list.insert(insert_position, cached_notification) # Handle limited apps history - only keep 5 notifications diff --git a/modules/notification/unified_cache.py b/modules/notification/unified_cache.py index 656ae82..0bd673b 100644 --- a/modules/notification/unified_cache.py +++ b/modules/notification/unified_cache.py @@ -34,12 +34,12 @@ def get_unified_cache_key(source_data, size=None, app_name=None): # For file paths - create hash-based name if source_data.startswith("file://"): source_data = source_data[7:] - + # Create hash from file path and size hash_input = source_data if size: hash_input += f"_{size[0]}x{size[1]}" - + return hashlib.md5(hash_input.encode()).hexdigest()[:8] else: # Fallback to random UUID @@ -62,7 +62,9 @@ def save_to_cache(pixbuf, cache_key, size=None): # Scale if size is specified if size and (pixbuf.get_width() != size[0] or pixbuf.get_height() != size[1]): - pixbuf = pixbuf.scale_simple(size[0], size[1], GdkPixbuf.InterpType.BILINEAR) + pixbuf = pixbuf.scale_simple( + size[0], size[1], GdkPixbuf.InterpType.BILINEAR + ) pixbuf.savev(cache_path, "png", [], []) logger.debug(f"Cached notification asset: {cache_key}") @@ -77,7 +79,7 @@ def get_from_cache(cache_key, size=None): try: cache_path = os.path.join(UNIFIED_NOTIFICATION_CACHE_DIR, f"{cache_key}.png") if os.path.exists(cache_path): - logger.debug(f"Using cached asset: {cache_key}") + # logger.debug(f"Using cached asset: {cache_key}") if size: return GdkPixbuf.Pixbuf.new_from_file_at_scale( cache_path, size[0], size[1], True @@ -96,7 +98,9 @@ def cleanup_cache(cache_key=None): if cache_key: # Remove specific cached asset - cache_path = os.path.join(UNIFIED_NOTIFICATION_CACHE_DIR, f"{cache_key}.png") + cache_path = os.path.join( + UNIFIED_NOTIFICATION_CACHE_DIR, f"{cache_key}.png" + ) if os.path.exists(cache_path): os.unlink(cache_path) logger.debug(f"Cleaned up cached asset: {cache_key}") @@ -144,7 +148,9 @@ def verify_cache_persistence(): if os.path.exists(UNIFIED_NOTIFICATION_CACHE_DIR): cache_files = [ - f for f in os.listdir(UNIFIED_NOTIFICATION_CACHE_DIR) if f.endswith(".png") + f + for f in os.listdir(UNIFIED_NOTIFICATION_CACHE_DIR) + if f.endswith(".png") ] logger.info(f"Cache persistence check: {len(cache_files)} assets cached") @@ -187,4 +193,5 @@ def get_fallback_icon(size=(48, 48)): # Initialize cache on module load ensure_cache_dir() cleanup_old_cache_files() -verify_cache_persistence() \ No newline at end of file +verify_cache_persistence() + diff --git a/styles/widgets.css b/styles/widgets.css index 5f6682b..8656200 100644 --- a/styles/widgets.css +++ b/styles/widgets.css @@ -14,7 +14,7 @@ #city { margin-top: 10px; - color: #1a87e6; + color: var(--blue-dim); font-weight: 600; font-size: 20px; } @@ -54,7 +54,7 @@ #calendar-month { font-size: 16px; font-weight: 600; - color: #1a87e6; + color: var(--blue-dim); margin-bottom: 6px; } @@ -99,7 +99,7 @@ font-size: 12px; font-weight: 700; color: #191919; - background-color: #1a87e6; + background-color: var(--blue-dim); border-radius: 50%; min-width: 18px; min-height: 16px; @@ -137,7 +137,7 @@ font-weight: 600; } #day label { - color: alpha(#1a87e6, 0.9); + color: alpha(var(--blue-dim), 0.9); } #date-container { @@ -165,13 +165,13 @@ font-size: 13px; } #progress { - border: solid 8px #1a87e6; + border: solid 8px var(--blue-dim); color: alpha(#aaaaaa, 0.7); } /* Color indicators for memory usage */ #used-color-indicator { - color: #1a87e6; + color: var(--blue-dim); font-size: 10px; font-weight: 900; } @@ -183,7 +183,7 @@ } #temp-color-indicator { - color: #1a87e6; + color: var(--blue-dim); font-size: 10px; font-weight: 900; } diff --git a/utils/functions.py b/utils/functions.py index fdae428..edbf259 100644 --- a/utils/functions.py +++ b/utils/functions.py @@ -1,3 +1,4 @@ +import html import json import os import threading @@ -151,5 +152,23 @@ def is_special_workspace(client: dict) -> bool: return False +def escape_markup_text(text: str) -> str: + """ + Escape special characters in text to make it safe for Pango markup. + + Args: + text: Raw text that may contain special characters + + Returns: + Escaped text safe for use in Pango markup + """ + if not text or not isinstance(text, str): + return "" + + # Use html.escape to escape XML/HTML special characters + # This handles &, <, >, and quotes + return html.escape(text) + +