From 7176d0baf7daf92052cc7f552c7feb347b1d3989 Mon Sep 17 00:00:00 2001 From: Tim Bendt Date: Mon, 5 May 2025 09:00:51 -0600 Subject: [PATCH] refactor metadata --- maildir_gtd/__pycache__/utils.cpython-311.pyc | Bin 0 -> 2541 bytes maildir_gtd/app.py | 120 +++++++++++++----- maildir_gtd/email_viewer.tcss | 24 +++- maildir_gtd/utils.py | 39 ++++++ 4 files changed, 151 insertions(+), 32 deletions(-) create mode 100644 maildir_gtd/__pycache__/utils.cpython-311.pyc create mode 100644 maildir_gtd/utils.py diff --git a/maildir_gtd/__pycache__/utils.cpython-311.pyc b/maildir_gtd/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b06eb6a4f51db1663e32ff9cdcd2318242dbe7c2 GIT binary patch literal 2541 zcmbVNO>7fa5Poa#uGc>spoxRyB<%_?$r>jqDlGyNpi-cuBJQD7RT8C^lebAs)<5iS zK(QPt2c+0hB%(?c6cyn^MMD9VLk}D|_PBN}WwjC#Qcv6><$^eM-ZM6F5+vHu?!1|K z^X9#o`S$H^qUb?D!6Sj$zd3~dAxfP4K$)sb?JIaBNy z3%XNrDs0EwMF?TGS)XPxcat6CF#j!*U9X6r?63u#Fcc%1QfxQjSW%4xnAyuo-LToA z?gnu z?5^-&fb+4<{wwaHdq>1QGWg(VF3cI*HpaX`VNx?qvjtZ{D7Xurg16wC=CSLhZ_$B; zMQ0ujp^58@Y*WqodZ0W2;2k_lq;I20z(-}=t5>vC%3|Y|siU3SzY!)XLbQrlCiZVw8Cm4v;P&9J> zCcu-_TH)P#4eTfWHp-(CNV};Sh()fn+XXDmUURg(ngc^(9~hVaPjMY2Zjqg%pBwDs zq`IS_vs8CBbdKswLxv8MF+c8Hxil=&rHWv2oO(^RrW{CM$#p_rD3c2hz*@i zb4KlO3-!b41n|-lr8i#=e5V5*2H?({RMOZO7*c9?oHj6H;66wvl}snpJ|mM+^?_6( zsp6y-hpB;iBdO~B*#&#wjAFzojNqh`ikkbhrZhkppInF&1ofX$GgApwKij`!1xZ_9 z1@?LgS>AoOPOY3OMpvVELifXW!#_uUiB!BtP47{wHMn-t5)a%OT^TKoua4gtz8||A zD|bbzU6G0yF~x}0`te#6Jk};E;vrK!RM$;^Petr8#h$uh?T3nZ*c1<2t)Hx&fd!_& zw<7kMVlOqkBpYqPc~I;Pb4(*h%(a9@`$DO{5SBuhPKQQpzV;=NUxcxcRcwytF1t1I zRoKpS=7vVDOxs18K+xgRl&qSVQf#MgXtn?>OMf?h~E%_p*1Xn-tgq&@ZlT0;fL z8&%YT#4b+FYnqaNTX0*bPupT$qNTRI+Zv*gk5p?T%uiT5VFw8d0MkWaQjN}2(#Tb3 zAAC<2wFu0T)bs_oG+t9$LN5bBll8ywwXI$*##UoX7p(pMd+yRTGa#1(auxV~d1=^c z>$o?({`nt+zYjh<_xPOI6)kr~tH9f$OCwgt(I*|x4w!PZEJv%rJEA7rZux_^Cw`nL zU0a(d`{k-%UW!?r@{{3b$IZxKIWkxU-Z{7tyjTfd{OhpnRKuq?!sjdD^Uqt$;S1I9h3AQC z_>%ecSUE6W4U88=t39-qDW%HoC#vly3SP_Gey6YE?J>PQmalumcckJwvOfE~(+rKk zQ})HGzL@EYSzSGa5liUU5TuGAl>%iUTouA5eO`LIOtwqwgcihPp5m5Z-g_c44zk&v)Ta6lS^Mu0^4*l3}c~QbFarj-R54;CVQCaE*a}X Vzvea(h)?me6MtT%;(HlA%D;EwYMuZ9 literal 0 HcmV?d00001 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