diff --git a/maildir_gtd/app.py b/maildir_gtd/app.py index 5ee39c4..de6d730 100644 --- a/maildir_gtd/app.py +++ b/maildir_gtd/app.py @@ -57,7 +57,9 @@ class EmailViewerApp(App): folder = reactive("INBOX") header_expanded = reactive(False) reload_needed = reactive(True) - all_envelopes = reactive([]) + all_envelopes = reactive([]) # Keep as list for compatibility with ListView + envelope_map = {} # Add a dictionary to map IDs to envelope data + envelope_index_map = {} # Map indices in the list to envelope IDs oldest_id: Reactive[int] = reactive(0) newest_id: Reactive[int] = reactive(0) msg_worker: Worker | None = None @@ -107,7 +109,7 @@ class EmailViewerApp(App): Binding("1", "focus_1", "Focus Accounts Panel"), Binding("2", "focus_2", "Focus Folders Panel"), Binding("3", "focus_3", "Focus Envelopes Panel"), - Binding("f", "toggle_mode", "Toggle Content Mode"), + Binding("m", "toggle_mode", "Toggle Content Mode"), ] BINDINGS.extend( @@ -232,7 +234,12 @@ class EmailViewerApp(App): message_date = datetime.strptime(message_date, "%Y-%m-%d %H:%M").strftime( "%a %b %d %H:%M" ) - self.current_message_index = metadata["index"] + + # Only update the current_message_index if it's different from the index in the ListView + # This prevents the sidebar selection from getting out of sync with the displayed content + if self.current_message_index != metadata["index"]: + self.current_message_index = metadata["index"] + content_container.update_header( subject=metadata.get("subject", "").strip(), from_=metadata["from"].get("addr", ""), @@ -240,50 +247,29 @@ class EmailViewerApp(App): date=message_date, cc=metadata["cc"].get("addr", "") if "cc" in metadata else "", ) - self.query_one(ListView).index = metadata["index"] + + # Make sure the ListView index matches the current message index + list_view = self.query_one("#envelopes_list") + if list_view.index != metadata["index"]: + list_view.index = metadata["index"] else: logging.warning(f"Message ID {new_message_id} not found in metadata.") - - - - def on_list_view_selected(self, event: ListView.Selected) -> None: """Called when an item in the list view is selected.""" - # logging.info(f"Selected item: {self.all_envelopes[event.list_view.index]}") - if ( - self.all_envelopes[event.list_view.index] is None - or self.all_envelopes[event.list_view.index].get("type") == "header" - ): - # If the selected item is a header, do not change the current message ID + current_item = self.all_envelopes[event.list_view.index] + + # Skip if it's a header or None + if current_item is None or current_item.get("type") == "header": return - self.current_message_id = int(self.all_envelopes[event.list_view.index]["id"]) - # @work(exclusive=False) - # async def fetch_one_message(self, new_message_id: int) -> None: - # content_container = self.query_one(ContentContainer) + # Get the message ID and update current index in a consistent way + message_id = int(current_item["id"]) + self.current_message_id = message_id - # try: - # process = await asyncio.create_subprocess_shell( - # f"himalaya message read {str(new_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: - # # Render the email content as Markdown - # fixedText = stdout.decode().replace("(https://urldefense.com/v3/", "(") - # fixedText = re.sub(r"atlOrigin.+?\)", ")", fixedText) - # logging.info(f"rendering fixedText: {fixedText[0:50]}") - # self.message_body_cache[new_message_id] = fixedText - # await content_container.display_content(new_message_id) - # self.query_one("#main_content").loading = False - # logging.info(fixedText) - - # except Exception as e: - # self.show_status(f"Error fetching message content: {e}", "error") - # logging.error(f"Error fetching message content: {e}") + # Update the index directly based on the ListView selection + # This ensures the sidebar selection and displayed content stay in sync + self.current_message_index = event.list_view.index @work(exclusive=False) async def fetch_envelopes(self) -> None: @@ -313,6 +299,12 @@ class EmailViewerApp(App): ) grouped_envelopes = group_envelopes_by_date(envelopes) self.all_envelopes = grouped_envelopes + + # Update our dictionary mappings + self.envelope_map = {int(envelope["id"]): envelope for envelope in grouped_envelopes if "id" in envelope} + self.envelope_index_map = {index: int(envelope["id"]) for index, envelope in enumerate(grouped_envelopes) if "id" in envelope} + + # Store metadata with correct indices self.message_metadata = { int(envelope["id"]): { "subject": envelope.get("subject", ""), @@ -325,6 +317,8 @@ class EmailViewerApp(App): for index, envelope in enumerate(self.all_envelopes) if "id" in envelope } + + # Add items to the ListView for item in grouped_envelopes: if item.get("type") == "header": msglist.append( @@ -431,8 +425,6 @@ class EmailViewerApp(App): message, title="Status", severity=severity, timeout=2.6, markup=True ) - - async def action_toggle_sort_order(self) -> None: """Toggle the sort order of the envelope list.""" self.sort_order_ascending = not self.sort_order_ascending @@ -485,7 +477,10 @@ class EmailViewerApp(App): self.fetch_envelopes() if self.reload_needed else None async def action_delete(self) -> None: + # Remove from all data structures self.all_envelopes = [item for item in self.all_envelopes if item and item.get("id") != self.current_message_id] + self.envelope_map.pop(self.current_message_id, None) + self.envelope_index_map = {index: id for index, id in self.envelope_index_map.items() if id != self.current_message_id} self.message_metadata = { k: v for k, v in self.message_metadata.items() if k != self.current_message_id } @@ -493,16 +488,34 @@ class EmailViewerApp(App): k: v for k, v in self.message_body_cache.items() if k != self.current_message_id } self.total_messages = len(self.message_metadata) + + # Perform delete operation delete_current(self) - newmsg = self.all_envelopes[self.current_message_index] - if newmsg.get("type") == "header": - newmsg = self.all_envelopes[self.current_message_index + 1] - return - self.show_message(newmsg["id"]) + + # Get next message to display + try: + newmsg = self.all_envelopes[self.current_message_index] + # Skip headers + if newmsg.get("type") == "header": + if self.current_message_index + 1 < len(self.all_envelopes): + newmsg = self.all_envelopes[self.current_message_index + 1] + else: + # If we're at the end, go to the previous message + newmsg = self.all_envelopes[self.current_message_index - 1] + self.current_message_index -= 1 + + # Show the next message + if "id" in newmsg: + self.show_message(newmsg["id"]) + except (IndexError, KeyError): + # If no more messages, just reload envelopes + self.reload_needed = True + self.fetch_envelopes() async def action_archive(self) -> None: - # self.query_one("#envelopes_list").pop(self.current_message_index) self.all_envelopes = [item for item in self.all_envelopes if item and item.get("id") != self.current_message_id] + self.envelope_map.pop(self.current_message_id, None) + self.envelope_index_map = {index: id for index, id in self.envelope_index_map.items() if id != self.current_message_id} self.message_metadata = { k: v for k, v in self.message_metadata.items() if k != self.current_message_id } diff --git a/maildir_gtd/widgets/ContentContainer.py b/maildir_gtd/widgets/ContentContainer.py index cef49d4..a0c1d69 100644 --- a/maildir_gtd/widgets/ContentContainer.py +++ b/maildir_gtd/widgets/ContentContainer.py @@ -13,6 +13,7 @@ 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 @@ -26,7 +27,7 @@ class ContentContainer(ScrollableContainer): 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 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: