diff --git a/maildir_gtd/__pycache__/utils.cpython-311.pyc b/maildir_gtd/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..b06eb6a Binary files /dev/null and b/maildir_gtd/__pycache__/utils.cpython-311.pyc differ diff --git a/maildir_gtd/app.py b/maildir_gtd/app.py index ad06b0c..73590a3 100644 --- a/maildir_gtd/app.py +++ b/maildir_gtd/app.py @@ -6,6 +6,8 @@ import asyncio import logging from typing import Iterable +# Add the parent directory to the system path to resolve relative imports +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) @@ -26,6 +28,7 @@ from actions.delete import delete_current from actions.open import action_open from actions.task import action_create_task from widgets.EnvelopeHeader import EnvelopeHeader +from maildir_gtd.utils import group_envelopes_by_date logging.basicConfig( level="NOTSET", @@ -59,9 +62,12 @@ class EmailViewerApp(App): oldest_id: Reactive[int] = reactive(0) newest_id: Reactive[int] = reactive(0) msg_worker: Worker | None = None + messsage_metadata: dict[int, dict] = {} message_body_cache: dict[int, str] = {} total_messages: Reactive[int] = reactive(0) status_title = reactive("Message View") + sort_order_ascending: Reactive[bool] = reactive(True) + valid_envelopes = reactive([]) def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]: yield from super().get_system_commands(screen) @@ -92,7 +98,8 @@ class EmailViewerApp(App): BINDINGS.extend([ Binding("space", "scroll_page_down", "Scroll page down"), - Binding("b", "scroll_page_up", "Scroll page up") + Binding("b", "scroll_page_up", "Scroll page up"), + Binding("s", "toggle_sort_order", "Toggle Sort Order") ]) def compose(self) -> ComposeResult: @@ -117,7 +124,8 @@ class EmailViewerApp(App): self.theme = "monokai" self.title = "MaildirGTD" self.query_one("#main_content").border_title = self.status_title - self.query_one("#envelopes_list").border_title = "\[1] Emails" + sort_indicator = '\u2191' if self.sort_order_ascending else '\u2193' + self.query_one("#envelopes_list").border_title = f"\[1] Emails {sort_indicator}" self.query_one("#accounts_list").border_title = "\[2] Accounts" self.query_one("#folders_list").border_title = "\[3] Folders" @@ -133,33 +141,48 @@ class EmailViewerApp(App): self.action_oldest() def compute_status_title(self) -> None: - return f"Message ID: {self.current_message_id} | [b]{self.current_message_index}[/b]/{self.total_messages}" + sort_indicator = "\u2191" if self.sort_order_ascending else "\u2193" + return f"✉️ Message ID: {self.current_message_id} " + + def compute_valid_envelopes(self) -> None: + 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 + 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.""" + sort_indicator = "\u2191" if new_value else "\u2193" + self.query_one("#envelopes_list").border_title = f"\[1] Emails {sort_indicator}" + + def watch_current_message_index(self, old_index: int, new_index: int) -> None: + self.query_one("#envelopes_list").border_subtitle = f"[b]{self.current_message_index}[/b]/{self.total_messages}" + def compute_newest_id(self) -> None: if not self.all_envelopes: return 0 - return sorted((int(envelope['id']) for envelope in self.all_envelopes))[-1] + return sorted((int(envelope['id']) for envelope in self.valid_envelopes))[-1] def compute_oldest_id(self) -> None: - if not self.all_envelopes: + if not self.valid_envelopes: return 0 - return sorted((int(envelope['id']) for envelope in self.all_envelopes))[0] + sorted_msgs = sorted((int(envelope['id']) for envelope in self.valid_envelopes)) + if len(sorted_msgs) > 0: + return sorted_msgs[0] + return 0 def compute_next_id(self) -> None: - if not self.all_envelopes: + if not self.valid_envelopes: return 0 - for envelope_id in sorted(int(envelope['id']) for envelope in self.all_envelopes): + for envelope_id in sorted(int(envelope['id']) for envelope in self.valid_envelopes): if envelope_id > int(self.current_message_id): return envelope_id return self.newest_id def compute_previous_id(self) -> None: - if not self.all_envelopes: + if not self.valid_envelopes: return 0 - for envelope_id in sorted((int(envelope['id']) for envelope in self.all_envelopes), reverse=True): + for envelope_id in sorted((int(envelope['id']) for envelope in self.valid_envelopes), reverse=True): if envelope_id < int(self.current_message_id): return envelope_id return self.oldest_id @@ -178,17 +201,17 @@ class EmailViewerApp(App): self.msg_worker.cancel() if self.msg_worker else None headers = self.query_one(EnvelopeHeader) - for index, envelope in enumerate(self.all_envelopes): - if int(envelope['id']) == new_message_id: - self.current_message_index = index - headers.subject = str(envelope['subject']).strip() - headers.from_ = envelope['from']['addr'] - headers.to = envelope['to']['addr'] - headers.date = datetime.strptime(envelope['date'].replace("+00:00", ""), "%Y-%m-%d %H:%M").strftime("%a %b %d %H:%M") - headers.cc = envelope['cc']['addr'] if 'cc' in envelope else "" - # self.query_one(StatusTitle).current_message_index = index - self.query_one(ListView).index = index - break + if new_message_id in self.message_metadata: + metadata = self.message_metadata[new_message_id] + self.current_message_index = metadata['index'] + headers.subject = metadata['subject'].strip() + headers.from_ = metadata['from'].get('addr', '') + headers.to = metadata['to'].get('addr', '') + headers.date = datetime.strptime(metadata['date'].replace("+00:00", ""), "%Y-%m-%d %H:%M").strftime("%a %b %d %H:%M") + headers.cc = metadata['cc'].get('addr', '') if 'cc' in metadata else "" + self.query_one(ListView).index = metadata['index'] + else: + logging.warning(f"Message ID {new_message_id} not found in metadata.") if (self.message_body_cache.get(new_message_id)): # If the message body is already cached, use it @@ -201,7 +224,10 @@ class EmailViewerApp(App): 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]}") + # logging.info(f"Selected item: {self.all_envelopes[event.list_view.index]}") + if (self.all_envelopes[event.list_view.index].get('type') == "header"): + # If the selected item is a header, do not change the current message ID + return self.current_message_id = int(self.all_envelopes[event.list_view.index]['id']) @work(exclusive=False) @@ -250,14 +276,30 @@ class EmailViewerApp(App): self.reload_needed = False self.total_messages = len(envelopes) msglist.clear() - envelopes = sorted(envelopes, key=lambda x: int(x['id'])) - self.all_envelopes = envelopes - for envelope in envelopes: - item = ListItem(Label(str(envelope['subject']).strip(), classes="email_subject", markup=False)) - msglist.append(item) + + envelopes = sorted(envelopes, key=lambda x: int(x['id']), reverse=not self.sort_order_ascending) + grouped_envelopes = group_envelopes_by_date(envelopes) + self.all_envelopes = grouped_envelopes + self.message_metadata = { + envelope['id']: { + 'subject': envelope.get('subject', ''), + 'from': envelope.get('from', {}), + 'to': envelope.get('to', {}), + 'date': envelope.get('date', ''), + 'cc': envelope.get('cc', {}), + 'index': index # Store the position index + } + for index, envelope in enumerate(self.all_envelopes) + if 'id' in envelope + } + for item in grouped_envelopes: + if item.get("type") == "header": + msglist.append(ListItem(Label(item["label"], classes="group_header", markup=False))) + else: + msglist.append(ListItem(Label(str(item['subject']).strip(), classes="email_subject", markup=False))) msglist.index = self.current_message_index else: - self.show_status("Failed to fetch the most recent message ID.", "error") + self.show_status("Failed to fetch any envelopes.", "error") except Exception as e: self.show_status(f"Error fetching message list: {e}", "error") finally: @@ -326,13 +368,31 @@ class EmailViewerApp(App): header.styles.height = "1" if self.header_expanded else "auto" self.header_expanded = not self.header_expanded + 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 + worker = self.fetch_envelopes() + await worker.wait() + + # Call action_newest or action_oldest based on the new sort order + if self.sort_order_ascending: + self.action_oldest() + else: + self.action_newest() + def action_next(self) -> None: - self.show_message(self.next_id) + if self.sort_order_ascending: + self.show_message(self.next_id) + else: + self.show_message(self.previous_id) self.fetch_envelopes() if self.reload_needed else None def action_previous(self) -> None: + if self.sort_order_ascending: + self.show_message(self.previous_id) + else: + self.show_message(self.next_id) self.fetch_envelopes() if self.reload_needed else None - self.show_message(self.previous_id) def action_delete(self) -> None: self.all_envelopes.remove(self.all_envelopes[self.current_message_index]) diff --git a/maildir_gtd/email_viewer.tcss b/maildir_gtd/email_viewer.tcss index fe9a8f7..34d47d0 100644 --- a/maildir_gtd/email_viewer.tcss +++ b/maildir_gtd/email_viewer.tcss @@ -108,10 +108,23 @@ Markdown { } } -#envelopes-list ListItem:odd { - background: rgb(25, 24, 26); +#envelopes_list { + ListItem:odd { + background: rgb(45, 45, 46); + } + ListItem:even { + background: rgb(50, 50, 56); + } + & > ListItem { + &.-highlight { + color: $block-cursor-blurred-foreground; + background: $block-cursor-blurred-background; + text-style: $block-cursor-blurred-text-style; + } + } } + #open_message_container, #create_task_container { dock: bottom; width: 100%; @@ -126,3 +139,10 @@ Markdown { } } +Label.group_header { + color: rgb(64,64,64); + text-style: bold; + background: rgb(64, 62, 65); + width: 100%; + padding: 0 1; +} diff --git a/maildir_gtd/utils.py b/maildir_gtd/utils.py new file mode 100644 index 0000000..dd1b2d5 --- /dev/null +++ b/maildir_gtd/utils.py @@ -0,0 +1,39 @@ +from datetime import datetime, timedelta +from typing import List, Dict + +def group_envelopes_by_date(envelopes: List[Dict]) -> List[Dict]: + """Group envelopes by date and add headers for each group.""" + grouped_envelopes = [] + today = datetime.now() + yesterday = today - timedelta(days=1) + start_of_week = today - timedelta(days=today.weekday()) + start_of_last_week = start_of_week - timedelta(weeks=1) + start_of_month = today.replace(day=1) + start_of_last_month = (start_of_month - timedelta(days=1)).replace(day=1) + + def get_group_label(date: datetime) -> str: + if date.date() == today.date(): + return "Today" + elif date.date() == yesterday.date(): + return "Yesterday" + elif date >= start_of_week: + return "This Week" + elif date >= start_of_last_week: + return "Last Week" + elif date >= start_of_month: + return "This Month" + elif date >= start_of_last_month: + return "Last Month" + else: + return "Older" + + current_group = None + for envelope in envelopes: + envelope_date = datetime.strptime(envelope['date'].split('+')[0], "%Y-%m-%d %H:%M") + group_label = get_group_label(envelope_date) + if group_label != current_group: + grouped_envelopes.append({"type": "header", "label": group_label}) + current_group = group_label + grouped_envelopes.append(envelope) + + return grouped_envelopes