diff --git a/.coverage b/.coverage index c1201d5..0aee14d 100644 Binary files a/.coverage and b/.coverage differ diff --git a/src/maildir_gtd/actions/delete.py b/src/maildir_gtd/actions/delete.py index a2331a6..daa0278 100644 --- a/src/maildir_gtd/actions/delete.py +++ b/src/maildir_gtd/actions/delete.py @@ -22,7 +22,7 @@ async def delete_current(app): next_id, next_idx = app.message_store.find_prev_valid_id(current_index) # Delete the message using our Himalaya client module - success = await himalaya_client.delete_message(current_message_id) + message, success = await himalaya_client.delete_message(current_message_id) if success: app.show_status(f"Message {current_message_id} deleted.", "success") @@ -38,4 +38,6 @@ async def delete_current(app): app.current_message_id = 0 app.show_status("No more messages available.", "warning") else: - app.show_status(f"Failed to delete message {current_message_id}.", "error") + app.show_status( + f"Failed to delete message {current_message_id}: {message}", "error" + ) diff --git a/src/maildir_gtd/app.py b/src/maildir_gtd/app.py index 7b051c3..abf6021 100644 --- a/src/maildir_gtd/app.py +++ b/src/maildir_gtd/app.py @@ -250,8 +250,16 @@ class EmailViewerApp(App): if event.list_view.index is None: return + # Only handle selection from the envelopes list + if event.list_view.id != "envelopes_list": + return + selected_index = event.list_view.index + # Check bounds before accessing + if selected_index < 0 or selected_index >= len(self.message_store.envelopes): + return + current_item = self.message_store.envelopes[selected_index] if current_item is None or current_item.get("type") == "header": @@ -261,13 +269,26 @@ class EmailViewerApp(App): self.current_message_id = message_id self.current_message_index = selected_index + # Focus the main content panel after selecting a message + self.action_focus_4() + def on_list_view_highlighted(self, event: ListView.Highlighted) -> None: """Called when an item in the list view is highlighted (e.g., via arrow keys).""" if event.list_view.index is None: return + # Only handle highlights from the envelopes list + if event.list_view.id != "envelopes_list": + return + highlighted_index = event.list_view.index + # Check bounds before accessing + if highlighted_index < 0 or highlighted_index >= len( + self.message_store.envelopes + ): + return + current_item = self.message_store.envelopes[highlighted_index] if current_item is None or current_item.get("type") == "header": diff --git a/src/maildir_gtd/config.py b/src/maildir_gtd/config.py index 8b67190..3d949b7 100644 --- a/src/maildir_gtd/config.py +++ b/src/maildir_gtd/config.py @@ -89,6 +89,13 @@ class LinkPanelConfig(BaseModel): close_on_open: bool = False +class MailConfig(BaseModel): + """Configuration for mail operations.""" + + # Folder to move messages to when archiving + archive_folder: str = "Archive" + + class ThemeConfig(BaseModel): """Theme/appearance settings.""" @@ -104,6 +111,7 @@ class MaildirGTDConfig(BaseModel): ) content_display: ContentDisplayConfig = Field(default_factory=ContentDisplayConfig) link_panel: LinkPanelConfig = Field(default_factory=LinkPanelConfig) + mail: MailConfig = Field(default_factory=MailConfig) keybindings: KeybindingsConfig = Field(default_factory=KeybindingsConfig) theme: ThemeConfig = Field(default_factory=ThemeConfig) diff --git a/src/maildir_gtd/email_viewer.tcss b/src/maildir_gtd/email_viewer.tcss index a3962c5..a6b58f8 100644 --- a/src/maildir_gtd/email_viewer.tcss +++ b/src/maildir_gtd/email_viewer.tcss @@ -11,17 +11,23 @@ width: 1fr } +.list_view { + height: 3; +} + #main_content { width: 2fr; } + .envelope-selected { tint: $accent 20%; } #sidebar:focus-within { background: $panel; + .list_view:blur { height: 3; } @@ -30,6 +36,11 @@ } } + +#envelopes_list { + height: 2fr; +} + #main_content:focus, .list_view:focus { border: round $secondary; background: rgb(55, 53, 57); diff --git a/src/maildir_gtd/widgets/EnvelopeListItem.py b/src/maildir_gtd/widgets/EnvelopeListItem.py index ba775ae..45118f4 100644 --- a/src/maildir_gtd/widgets/EnvelopeListItem.py +++ b/src/maildir_gtd/widgets/EnvelopeListItem.py @@ -147,6 +147,9 @@ class EnvelopeListItem(Static): date_str = date_str.replace("Z", "+00:00") dt = datetime.fromisoformat(date_str) + # Convert to local timezone + dt = dt.astimezone() + parts = [] if self.config.show_date: parts.append(dt.strftime(self.config.date_format)) diff --git a/src/services/himalaya/client.py b/src/services/himalaya/client.py index 2769eb6..8195742 100644 --- a/src/services/himalaya/client.py +++ b/src/services/himalaya/client.py @@ -4,6 +4,8 @@ import json import logging import subprocess +from src.maildir_gtd.config import get_config + async def list_envelopes(limit: int = 9999) -> Tuple[List[Dict[str, Any]], bool]: """ @@ -92,7 +94,7 @@ async def list_folders() -> Tuple[List[Dict[str, Any]], bool]: return [], False -async def delete_message(message_id: int) -> bool: +async def delete_message(message_id: int) -> Tuple[Optional[str], bool]: """ Delete a message by its ID. @@ -100,7 +102,9 @@ async def delete_message(message_id: int) -> bool: message_id: The ID of the message to delete Returns: - True if deletion was successful, False otherwise + Tuple containing: + - Result message or error + - Success status (True if deletion was successful) """ try: process = await asyncio.create_subprocess_shell( @@ -110,10 +114,15 @@ async def delete_message(message_id: int) -> bool: ) stdout, stderr = await process.communicate() - return process.returncode == 0 + if process.returncode == 0: + return stdout.decode().strip() or "Deleted successfully", True + else: + error_msg = stderr.decode().strip() + logging.error(f"Error deleting message: {error_msg}") + return error_msg or "Unknown error", False except Exception as e: logging.error(f"Exception during message deletion: {e}") - return False + return str(e), False # async def archive_message(message_id: int) -> [str, bool]: @@ -151,8 +160,10 @@ async def archive_messages(message_ids: List[str]) -> Tuple[Optional[str], bool] A tuple containing an optional output string and a boolean indicating success. """ try: + config = get_config() + archive_folder = config.mail.archive_folder ids_str = " ".join(message_ids) - cmd = f"himalaya message move Archives {ids_str}" + cmd = f"himalaya message move {archive_folder} {ids_str}" process = await asyncio.create_subprocess_shell( cmd, @@ -162,13 +173,14 @@ async def archive_messages(message_ids: List[str]) -> Tuple[Optional[str], bool] stdout, stderr = await process.communicate() if process.returncode == 0: - return stdout.decode(), True + return stdout.decode().strip() or "Archived successfully", True else: - logging.error(f"Error archiving messages: {stderr.decode()}") - return None, False + error_msg = stderr.decode().strip() + logging.error(f"Error archiving messages: {error_msg}") + return error_msg or "Unknown error", False except Exception as e: logging.error(f"Exception during message archiving: {e}") - return None, False + return str(e), False async def get_message_content(message_id: int) -> Tuple[Optional[str], bool]: