Files
luk/maildir_gtd/widgets/ContentContainer.py
Tim Bendt 5c9ad69309 wip
2025-05-13 08:16:23 -06:00

187 lines
7.1 KiB
Python

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
self.message_cache = dict()
# LRU cache with a max size of 100 messages
def compose(self) -> ComposeResult:
"""Compose the container with a label for plaintext and markdown for rich content."""
yield EnvelopeHeader()
yield Label(id="plaintext_content", markup=False)
yield Markdown(id="markdown_content", classes="hidden")
def update_header(self, subject: str = "", date: str = "", from_: str = "", to: str = "", cc: str = "", bcc: str = "") -> None:
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
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
# Check if the message is already cached
if message_id in self.message_cache:
self.current_text = self.message_cache[message_id]
plaintext = self.query_one("#plaintext_content", Label)
plaintext.update(self.current_text)
if not self.plaintext_mode:
# We're in markdown mode, so render the markdown
self.render_markdown()
else:
# Hide markdown, show plaintext
plaintext.remove_class("hidden")
self.query_one("#markdown_content").add_class("hidden")
self.loading = False
return self.current_text
else:
# Get message body (from cache or fetch new)
self.get_message_body(message_id)
@work(exclusive=True)
async def get_message_body(self, message_id: int) -> str:
"""Fetch the message body from Himalaya CLI."""
try:
# Store the ID of the message we're currently loading
loading_id = message_id
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]}...")
# Check if we're still loading the same message or if navigation has moved on
if loading_id != self.current_id:
logging.info(f"Message ID changed during loading. Abandoning load of {loading_id}")
return ""
if process.returncode == 0:
# Process the email content
content = stdout.decode()
# Remove header lines from the beginning of the message
# Headers typically end with a blank line before the message body
lines = content.split('\n')
body_start = 0
# Find the first blank line which typically separates headers from body
for i, line in enumerate(lines):
if line.strip() == '' and i > 0:
# Check if we're past the headers section
# Headers are typically in "Key: Value" format
has_headers = any(': ' in l for l in lines[:i])
if has_headers:
body_start = i + 1
break
# Join the body lines back together
content = '\n'.join(lines[body_start:])
# Apply existing cleanup logic
fixed_text = content.replace("https://urldefense.com/v3/", "")
fixed_text = re.sub(r"atlOrigin.+?\w", "", fixed_text)
logging.info(f"rendering fixedText: {fixed_text[0:50]}")
self.current_text = fixed_text
self.message_cache[message_id] = fixed_text
# Check again if we're still on the same message before updating UI
if loading_id != self.current_id:
logging.info(f"Message ID changed after loading. Abandoning update for {loading_id}")
return fixed_text
# Update the plaintext content
plaintext = self.query_one("#plaintext_content", Label)
plaintext.update(fixed_text)
if not self.plaintext_mode:
# We're in markdown mode, so render the markdown
self.render_markdown()
else:
# Hide markdown, show plaintext
plaintext.remove_class("hidden")
self.query_one("#markdown_content").add_class("hidden")
self.loading = False
return fixed_text
else:
logging.error(f"Error fetching message: {stderr.decode()}")
self.loading = False
return f"Error fetching message content: {stderr.decode()}"
except Exception as e:
logging.error(f"Error fetching message content: {e}")
self.loading = False
return f"Error fetching message content: {e}"
finally:
# Ensure loading state is always reset if this worker completes
# This prevents the loading indicator from getting stuck
if loading_id == self.current_id:
self.loading = False
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