import re import asyncio import logging from functools import lru_cache from textual import work from textual.app import ComposeResult from textual.widgets import Label, Markdown from textual.containers import ScrollableContainer from widgets.EnvelopeHeader import EnvelopeHeader class ContentContainer(ScrollableContainer): """A custom container that can switch between plaintext and markdown rendering.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.plaintext_mode = True self.markup_worker = None self.current_text = "" self.current_id = None # LRU cache with a max size of 100 messages self.get_message_body = lru_cache(maxsize=100)(self._get_message_body) def compose(self) -> ComposeResult: """Compose the container with a label for plaintext and markdown for rich content.""" yield EnvelopeHeader() yield Label(id="plaintext_content") yield Markdown(id="markdown_content", classes="hidden") def update_header(self, subject: str = "", date: str = "", from_: str = "", to: str = "", cc: str = "", bcc: str = "") -> None: """Update the header with the given email details.""" header = self.query_one(EnvelopeHeader) header.subject = subject header.date = date header.from_ = from_ header.to = to header.cc = cc header.bcc = bcc def action_toggle_header(self) -> None: """Toggle the visibility of the EnvelopeHeader panel.""" header = self.query_one(EnvelopeHeader) header.styles.height = "1" if self.header_expanded else "auto" self.header_expanded = not self.header_expanded async def display_content(self, message_id: int) -> None: """Display content for the given message ID.""" self.current_id = message_id # Show loading state self.loading = True # Get message body (from cache or fetch new) message_text = await self.get_message_body(message_id) self.current_text = message_text # Update the plaintext content plaintext = self.query_one("#plaintext_content", Label) await plaintext.update(message_text) if not self.plaintext_mode: # We're in markdown mode, so render the markdown await self.render_markdown() else: # Hide markdown, show plaintext plaintext.remove_class("hidden") self.query_one("#markdown_content").add_class("hidden") self.loading = False @work(exclusive=True) async def _get_message_body(self, message_id: int) -> str: """Fetch the message body from Himalaya CLI.""" try: process = await asyncio.create_subprocess_shell( f"himalaya message read {str(message_id)} -p", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() logging.info(f"stdout: {stdout.decode()[0:50]}...") if process.returncode == 0: # Process the email content fixedText = stdout.decode().replace("https://urldefense.com/v3/", "") fixedText = re.sub(r"atlOrigin.+?\w", "", fixedText) logging.info(f"rendering fixedText: {fixedText[0:50]}") return fixedText else: logging.error(f"Error fetching message: {stderr.decode()}") return f"Error fetching message content: {stderr.decode()}" except Exception as e: logging.error(f"Error fetching message content: {e}") return f"Error fetching message content: {e}" async def render_markdown(self) -> None: """Render the markdown content asynchronously.""" if self.markup_worker: self.markup_worker.cancel() markdown = self.query_one("#markdown_content", Markdown) plaintext = self.query_one("#plaintext_content", Label) await markdown.update(self.current_text) # Show markdown, hide plaintext markdown.remove_class("hidden") plaintext.add_class("hidden") async def toggle_mode(self) -> None: """Toggle between plaintext and markdown mode.""" self.plaintext_mode = not self.plaintext_mode if self.plaintext_mode: # Switch to plaintext self.query_one("#plaintext_content").remove_class("hidden") self.query_one("#markdown_content").add_class("hidden") else: # Switch to markdown await self.render_markdown() return self.plaintext_mode def clear_cache(self) -> None: """Clear the message body cache.""" self.get_message_body.cache_clear()