From 2fcad5700d5c88338dc27afafe1c81e9004f315f Mon Sep 17 00:00:00 2001 From: Tim Bendt Date: Mon, 12 May 2025 10:40:01 -0600 Subject: [PATCH] fixes to email display --- Congruence | 1 + drive_view_tui.py | 43 ++++++++++++++-- maildir_gtd/app.py | 4 +- maildir_gtd/widgets/ContentContainer.py | 65 ++++++++++++++++--------- 4 files changed, 82 insertions(+), 31 deletions(-) create mode 160000 Congruence diff --git a/Congruence b/Congruence new file mode 160000 index 0000000..2860e18 --- /dev/null +++ b/Congruence @@ -0,0 +1 @@ +Subproject commit 2860e1884dbbebb1ce0ee55946610764a9c7003d diff --git a/drive_view_tui.py b/drive_view_tui.py index 466c641..d2b2369 100644 --- a/drive_view_tui.py +++ b/drive_view_tui.py @@ -3,6 +3,7 @@ import sys import json import asyncio from datetime import datetime +from pathlib import Path import msal import aiohttp @@ -34,6 +35,20 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "maildir_gtd")) from maildir_gtd.screens.DocumentViewer import DocumentViewerScreen +class FolderHistoryEntry: + """Represents an entry in the folder navigation history.""" + + def __init__(self, folder_id: str, folder_name: str, parent_id: str = None): + self.folder_id = folder_id + self.folder_name = folder_name + self.parent_id = parent_id + + def __eq__(self, other): + if not isinstance(other, FolderHistoryEntry): + return False + return self.folder_id == other.folder_id + + class OneDriveTUI(App): """A Textual app for OneDrive integration with MSAL authentication.""" @@ -44,6 +59,8 @@ class OneDriveTUI(App): selected_drive_id = reactive("") drive_name = reactive("") current_view = reactive("Following") # Track current view: "Following" or "Root" + current_folder_id = reactive("root") # Track current folder ID + current_folder_name = reactive("Root") # Track current folder name # App bindings BINDINGS = [ @@ -54,6 +71,8 @@ class OneDriveTUI(App): Binding("enter", "open_url", "Open URL"), Binding("v", "view_document", "View Document"), Binding("tab", "next_view", "Switch View"), + Binding("backspace", "navigate_back", "Back"), + Binding("b", "navigate_back", "Back"), ] def __init__(self): @@ -61,8 +80,8 @@ class OneDriveTUI(App): self.access_token = None self.drives = [] self.followed_items = [] - - self.current_items = {} # Store currently displayed items + self.current_items = {} # Store currently displayed items + self.folder_history = [] # History stack for folder navigation self.msal_app = None self.cache = msal.SerializableTokenCache() # Read Azure app credentials from environment variables @@ -287,18 +306,22 @@ class OneDriveTUI(App): self.action_view_document() @work - async def load_root_items(self, folder_id: str = "", drive_id: str = ""): + async def load_root_items(self, folder_id: str = "", drive_id: str = "", track_history: bool = True): """Load root items from the selected drive.""" if not self.access_token or not self.selected_drive_id: return - self.query_one("#status_label").update("Loading root items...") + self.query_one("#status_label").update("Loading drive folder items...") headers = {"Authorization": f"Bearer {self.access_token}"} url = f"https://graph.microsoft.com/v1.0/me/drive/root/children" if folder_id and drive_id: url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{folder_id}/children" + if track_history: + self.folder_history.append(FolderHistoryEntry(folder_id, self.current_folder_name, drive_id)) self.selected_drive_id = drive_id + self.current_folder_id = folder_id + self.current_folder_name = self.current_items[folder_id].get("name", "Unknown") try: async with aiohttp.ClientSession() as session: @@ -311,7 +334,7 @@ class OneDriveTUI(App): # Update the table with the root items - self.update_items_table(items_data.get("value", []), is_root_view=True) + self.update_items_table(items_data.get("value", [])) except Exception as e: self.notify(f"Error loading root items: {str(e)}", severity="error") @@ -456,6 +479,16 @@ class OneDriveTUI(App): """Quit the application.""" self.exit() + async def action_navigate_back(self) -> None: + """Navigate back to the previous folder.""" + if self.folder_history: + previous_entry = self.folder_history.pop() + self.current_folder_id = previous_entry.folder_id + self.current_folder_name = previous_entry.folder_name + self.load_root_items(folder_id=previous_entry.folder_id, drive_id=previous_entry.parent_id, track_history=False) + else: + self.notify("No previous folder to navigate back to") + if __name__ == "__main__": app = OneDriveTUI() diff --git a/maildir_gtd/app.py b/maildir_gtd/app.py index c27e87e..5ee39c4 100644 --- a/maildir_gtd/app.py +++ b/maildir_gtd/app.py @@ -164,7 +164,7 @@ class EmailViewerApp(App): return (envelope for envelope in self.all_envelopes if envelope.get("id")) def watch_status_title(self, old_status_title: str, new_status_title: str) -> None: - self.query_one("#main_content").border_title = new_status_title + self.query_one(ContentContainer).border_title = new_status_title def watch_sort_order_ascending(self, old_value: bool, new_value: bool) -> None: """Update the border title of the envelopes list when the sort order changes.""" @@ -223,7 +223,7 @@ class EmailViewerApp(App): logging.info(f"new_message_id: {new_message_id}, type: {type(new_message_id)}") logging.info(f"message_metadata keys: {list(self.message_metadata.keys())}") - content_container = self.query_one("#main_content") + content_container = self.query_one(ContentContainer) content_container.display_content(new_message_id) if new_message_id in self.message_metadata: diff --git a/maildir_gtd/widgets/ContentContainer.py b/maildir_gtd/widgets/ContentContainer.py index f1b58f0..cef49d4 100644 --- a/maildir_gtd/widgets/ContentContainer.py +++ b/maildir_gtd/widgets/ContentContainer.py @@ -19,8 +19,9 @@ class ContentContainer(ScrollableContainer): self.markup_worker = None self.current_text = "" self.current_id = None + self.message_cache = dict() # 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.""" @@ -29,7 +30,6 @@ class ContentContainer(ScrollableContainer): 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 @@ -44,34 +44,37 @@ class ContentContainer(ScrollableContainer): 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: + 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: + # 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 - await self.render_markdown() - else: + self.render_markdown() + else: # Hide markdown, show plaintext plaintext.remove_class("hidden") self.query_one("#markdown_content").add_class("hidden") - self.loading = False + 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: + 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", @@ -83,10 +86,26 @@ class ContentContainer(ScrollableContainer): 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 + fixed_text = stdout.decode().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 + + # 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 else: logging.error(f"Error fetching message: {stderr.decode()}") return f"Error fetching message content: {stderr.decode()}" @@ -122,6 +141,4 @@ class ContentContainer(ScrollableContainer): return self.plaintext_mode - def clear_cache(self) -> None: - """Clear the message body cache.""" - self.get_message_body.cache_clear() +