Add escape_markup_text() utility function to safely escape special characters
This commit is contained in:
1 parent
eb243ab8e8
commit
d53341a0c0
5 files changed
+95
-46
No files matched your search
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
verify_cache_persistence()
|
||||
|
||||
+7
-7
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user