From 82338296214fc1124b3085b47400957135041cf4 Mon Sep 17 00:00:00 2001 From: Bendt Date: Fri, 19 Dec 2025 16:15:08 -0500 Subject: [PATCH] Add URL compression for mail content viewer --- src/mail/config.py | 4 ++ src/mail/widgets/ContentContainer.py | 101 ++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/mail/config.py b/src/mail/config.py index d5bcd82..da15e9c 100644 --- a/src/mail/config.py +++ b/src/mail/config.py @@ -82,6 +82,10 @@ class ContentDisplayConfig(BaseModel): # View mode: "markdown" for pretty rendering, "html" for raw/plain display default_view_mode: Literal["markdown", "html"] = "markdown" + # URL compression: shorten long URLs for better readability + compress_urls: bool = True + max_url_length: int = 50 # Maximum length before URL is compressed + class LinkPanelConfig(BaseModel): """Configuration for the link panel.""" diff --git a/src/mail/widgets/ContentContainer.py b/src/mail/widgets/ContentContainer.py index acb1ff6..7552f4a 100644 --- a/src/mail/widgets/ContentContainer.py +++ b/src/mail/widgets/ContentContainer.py @@ -6,10 +6,15 @@ from textual.widgets import Static, Markdown, Label from textual.reactive import reactive from src.services.himalaya import client as himalaya_client from src.mail.config import get_config -from src.mail.screens.LinkPanel import extract_links_from_content, LinkItem +from src.mail.screens.LinkPanel import ( + extract_links_from_content, + LinkItem, + LinkItem as LinkItemClass, +) import logging from datetime import datetime -from typing import Literal, List +from typing import Literal, List, Dict +from urllib.parse import urlparse import re import os import sys @@ -18,6 +23,88 @@ import sys sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +def compress_urls_in_content(content: str, max_url_len: int = 50) -> str: + """Compress long URLs in markdown/text content for better readability. + + Replaces long URLs with shortened versions using the same algorithm + as LinkPanel._shorten_url. Preserves markdown link syntax. + + Args: + content: The markdown/text content to process + max_url_len: Maximum length for displayed URLs (default 50) + + Returns: + Content with compressed URLs + """ + + # Pattern for markdown links: [text](url) + def replace_md_link(match): + anchor_text = match.group(1) + url = match.group(2) + + # Don't compress if URL is already short + if len(url) <= max_url_len: + return match.group(0) + + # Use LinkItem's shortening algorithm + short_url = LinkItemClass._shorten_url( + url, + urlparse(url).netloc.replace("www.", ""), + urlparse(url).path, + max_url_len, + ) + + # Keep original anchor text, but if it's the same as URL, use short version + if anchor_text == url or anchor_text.startswith("http"): + return f"[🔗 {short_url}]({url})" + else: + return match.group(0) # Keep original if anchor text is meaningful + + # Pattern for bare URLs (not inside markdown links) + def replace_bare_url(match): + url = match.group(0) + + # Don't compress if URL is already short + if len(url) <= max_url_len: + return url + + parsed = urlparse(url) + short_url = LinkItemClass._shorten_url( + url, parsed.netloc.replace("www.", ""), parsed.path, max_url_len + ) + + # Return as markdown link with icon + return f"[🔗 {short_url}]({url})" + + # First, process markdown links + md_link_pattern = r"\[([^\]]+)\]\((https?://[^)]+)\)" + content = re.sub(md_link_pattern, replace_md_link, content) + + # Then process bare URLs that aren't already in markdown links + # This regex matches URLs not preceded by ]( which would indicate markdown link + bare_url_pattern = r'(?"\'\)]+[^\s<>"\'\.\,\)\]]' + + # Use a more careful approach to avoid double-processing + # Split content, process bare URLs, rejoin + result = [] + last_end = 0 + + for match in re.finditer(bare_url_pattern, content): + # Check if this URL is inside a markdown link (preceded by "](") + prefix_start = max(0, match.start() - 2) + prefix = content[prefix_start : match.start()] + if prefix.endswith("]("): + continue # Skip URLs that are already markdown link targets + + result.append(content[last_end : match.start()]) + result.append(replace_bare_url(match)) + last_end = match.end() + + result.append(content[last_end:]) + + return "".join(result) + + class EnvelopeHeader(Vertical): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -192,10 +279,18 @@ class ContentContainer(ScrollableContainer): # Store the raw content for link extraction self.current_content = content + # Get URL compression settings from config + config = get_config() + compress_urls = config.content_display.compress_urls + max_url_len = config.content_display.max_url_length + try: if self.current_mode == "markdown": # For markdown mode, use the Markdown widget - self.content.update(content) + display_content = content + if compress_urls: + display_content = compress_urls_in_content(content, max_url_len) + self.content.update(display_content) else: # For HTML mode, use the Static widget with markup # First, try to extract the body content if it's HTML