diff --git a/maildir_gtd/actions/__pycache__/archive.cpython-311.pyc b/maildir_gtd/actions/__pycache__/archive.cpython-311.pyc index 1ad8669..56252b1 100644 Binary files a/maildir_gtd/actions/__pycache__/archive.cpython-311.pyc and b/maildir_gtd/actions/__pycache__/archive.cpython-311.pyc differ diff --git a/maildir_gtd/actions/__pycache__/delete.cpython-311.pyc b/maildir_gtd/actions/__pycache__/delete.cpython-311.pyc index 5e8a068..bc41998 100644 Binary files a/maildir_gtd/actions/__pycache__/delete.cpython-311.pyc and b/maildir_gtd/actions/__pycache__/delete.cpython-311.pyc differ diff --git a/maildir_gtd/actions/__pycache__/newest.cpython-311.pyc b/maildir_gtd/actions/__pycache__/newest.cpython-311.pyc new file mode 100644 index 0000000..a186f51 Binary files /dev/null and b/maildir_gtd/actions/__pycache__/newest.cpython-311.pyc differ diff --git a/maildir_gtd/actions/__pycache__/next.cpython-311.pyc b/maildir_gtd/actions/__pycache__/next.cpython-311.pyc index e633e96..5d71eed 100644 Binary files a/maildir_gtd/actions/__pycache__/next.cpython-311.pyc and b/maildir_gtd/actions/__pycache__/next.cpython-311.pyc differ diff --git a/maildir_gtd/actions/__pycache__/oldest.cpython-311.pyc b/maildir_gtd/actions/__pycache__/oldest.cpython-311.pyc new file mode 100644 index 0000000..319cbc0 Binary files /dev/null and b/maildir_gtd/actions/__pycache__/oldest.cpython-311.pyc differ diff --git a/maildir_gtd/actions/__pycache__/previous.cpython-311.pyc b/maildir_gtd/actions/__pycache__/previous.cpython-311.pyc index 9bd94cd..7b174ac 100644 Binary files a/maildir_gtd/actions/__pycache__/previous.cpython-311.pyc and b/maildir_gtd/actions/__pycache__/previous.cpython-311.pyc differ diff --git a/maildir_gtd/actions/__pycache__/show_message.cpython-311.pyc b/maildir_gtd/actions/__pycache__/show_message.cpython-311.pyc index 2f335f9..845fe33 100644 Binary files a/maildir_gtd/actions/__pycache__/show_message.cpython-311.pyc and b/maildir_gtd/actions/__pycache__/show_message.cpython-311.pyc differ diff --git a/maildir_gtd/actions/archive.py b/maildir_gtd/actions/archive.py index a72c49e..23ab991 100644 --- a/maildir_gtd/actions/archive.py +++ b/maildir_gtd/actions/archive.py @@ -12,9 +12,8 @@ def action_archive(app) -> None: text=True ) if result.returncode == 0: - app.query_one("#main_content", Static).update(f"Message {app.current_message_id} archived.") action_next(app) # Automatically show the next message else: - app.query_one("#main_content", Static).update(f"Failed to archive message {app.current_message_id}.") + app.show_status(f"Error archiving message: {result.stderr}", "error") except Exception as e: - app.query_one("#main_content", Static).update(f"Error: {e}") + app.show_status(f"Error: {e}", "error") diff --git a/maildir_gtd/actions/delete.py b/maildir_gtd/actions/delete.py index fd8aa80..7991bb4 100644 --- a/maildir_gtd/actions/delete.py +++ b/maildir_gtd/actions/delete.py @@ -14,10 +14,9 @@ def action_delete(app) -> None: text=True ) if result.returncode == 0: - app.query_one("#main_content", Static).loading = False - app.query_one("#main_content", Static).update(f"Message {app.current_message_id} deleted.") + app.query_one("#main_content").loading = False action_next(app) # Automatically show the next message else: - app.query_one("#main_content", Static).update(f"Failed to delete message {app.current_message_id}.") + app.show_status(f"Failed to delete message {app.current_message_id}.", "error") except Exception as e: - app.query_one("#main_content", Static).update(f"Error: {e}") + app.show_status(f"Error: {e}", "error") diff --git a/maildir_gtd/actions/newest.py b/maildir_gtd/actions/newest.py new file mode 100644 index 0000000..d6da131 --- /dev/null +++ b/maildir_gtd/actions/newest.py @@ -0,0 +1,23 @@ + +import subprocess + +def action_newest(app) -> None: + """Show the previous email message by finding the next lower ID from the list of envelope IDs.""" + try: + result = subprocess.run( + ["himalaya", "envelope", "list", "-o", "json", "-s", "9999"], + capture_output=True, + text=True + ) + if result.returncode == 0: + import json + envelopes = json.loads(result.stdout) + ids = sorted((int(envelope['id']) for envelope in envelopes), reverse=True) + app.current_message_id = ids[0] + app.show_message(app.current_message_id) + return + + else: + app.show_status("Failed to fetch envelope list.", severity="error") + except Exception as e: + app.show_status(f"Error: {e}", severity="error") diff --git a/maildir_gtd/actions/next.py b/maildir_gtd/actions/next.py index 05ec6e2..4cef2d8 100644 --- a/maildir_gtd/actions/next.py +++ b/maildir_gtd/actions/next.py @@ -15,7 +15,7 @@ def action_next(app) -> None: """Show the next email message by finding the next higher ID from the list of envelope IDs.""" try: result = subprocess.run( - ["himalaya", "envelope", "list", "-o", "json"], + ["himalaya", "envelope", "list", "-o", "json", "-s", "9999"], capture_output=True, text=True ) @@ -23,13 +23,13 @@ def action_next(app) -> None: import json envelopes = json.loads(result.stdout) ids = sorted(int(envelope['id']) for envelope in envelopes) - for index, envelope_id in enumerate(ids): + for envelope_id in ids: if envelope_id > int(app.current_message_id): app.show_message(envelope_id) return app.show_status("No newer messages found.", severity="warning") - app.show_message(envelopes[-1]['id']) # Automatically show the previous message + app.action_newest() else: app.show_status("Failed to fetch envelope list.", severity="error") except Exception as e: diff --git a/maildir_gtd/actions/oldest.py b/maildir_gtd/actions/oldest.py new file mode 100644 index 0000000..419aef5 --- /dev/null +++ b/maildir_gtd/actions/oldest.py @@ -0,0 +1,23 @@ + +import subprocess + +def action_oldest(app) -> None: + """Show the previous email message by finding the next lower ID from the list of envelope IDs.""" + try: + result = subprocess.run( + ["himalaya", "envelope", "list", "-o", "json", "-s", "9999"], + capture_output=True, + text=True + ) + if result.returncode == 0: + import json + envelopes = json.loads(result.stdout) + ids = sorted((int(envelope['id']) for envelope in envelopes)) + app.current_message_id = ids[0] + app.show_message(app.current_message_id) + return + + else: + app.show_status("Failed to fetch envelope list.", severity="error") + except Exception as e: + app.show_status(f"Error: {e}", severity="error") diff --git a/maildir_gtd/actions/previous.py b/maildir_gtd/actions/previous.py index 4e7ed49..f475d2c 100644 --- a/maildir_gtd/actions/previous.py +++ b/maildir_gtd/actions/previous.py @@ -1,11 +1,11 @@ -from textual.widgets import Static + import subprocess def action_previous(app) -> None: """Show the previous email message by finding the next lower ID from the list of envelope IDs.""" try: result = subprocess.run( - ["himalaya", "envelope", "list", "-o", "json"], + ["himalaya", "envelope", "list", "-o", "json", "-s", "9999"], capture_output=True, text=True ) @@ -19,6 +19,7 @@ def action_previous(app) -> None: app.show_message(app.current_message_id) return app.show_status("No older messages found.", severity="warning") + app.action_oldest() else: app.show_status("Failed to fetch envelope list.", severity="error") except Exception as e: diff --git a/maildir_gtd/actions/show_message.py b/maildir_gtd/actions/show_message.py index 9a01eca..10b7e72 100644 --- a/maildir_gtd/actions/show_message.py +++ b/maildir_gtd/actions/show_message.py @@ -1,23 +1,14 @@ -from textual.widgets import Static -from rich.markdown import Markdown +import logging import subprocess +from textual.logging import TextualHandler + + +logging.basicConfig( + level="NOTSET", + handlers=[TextualHandler()], +) def show_message(app, message_id: int) -> None: """Fetch and display the email message by ID.""" app.current_message_id = message_id - app.query_one("#main_content", Static).loading = True - try: - result = subprocess.run( - ["himalaya", "message", "read", str(message_id)], - capture_output=True, - text=True - ) - if result.returncode == 0: - # Render the email content as Markdown - markdown_content = Markdown(result.stdout, justify=True) - app.query_one("#main_content", Static).loading = False - app.query_one("#main_content", Static).update(markdown_content) - else: - app.query_one("#main_content", Static).update(f"Failed to fetch message {message_id}.") - except Exception as e: - app.query_one("#main_content", Static).update(f"Error: {e}") + logging.info("Showing message ID: " + str(message_id)) diff --git a/maildir_gtd/app.py b/maildir_gtd/app.py index 5e46e62..927bb0c 100644 --- a/maildir_gtd/app.py +++ b/maildir_gtd/app.py @@ -1,5 +1,10 @@ +import re import sys import os +from datetime import datetime # Add this import at the top of the file + +from actions.newest import action_newest +from actions.oldest import action_oldest sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) import logging @@ -9,8 +14,8 @@ from textual.widget import Widget from textual.app import App, ComposeResult, SystemCommand, RenderResult from textual.logging import TextualHandler from textual.screen import Screen -from textual.widgets import Header, Footer, Static, Label, Input, Button -from textual.reactive import Reactive +from textual.widgets import Header, Footer, Static, Label, Input, Button, Markdown +from textual.reactive import reactive, Reactive from textual.binding import Binding from textual.timer import Timer from textual.containers import ScrollableContainer, Horizontal, Vertical, Grid @@ -22,25 +27,24 @@ from maildir_gtd.actions.show_message import show_message from maildir_gtd.actions.next import action_next from maildir_gtd.actions.previous import action_previous from maildir_gtd.actions.task import action_create_task +from maildir_gtd.widgets.EnvelopeHeader import EnvelopeHeader logging.basicConfig( level="NOTSET", handlers=[TextualHandler()], ) -class Hello(Widget): - """Display a greeting.""" - def render(self) -> RenderResult: - return "Hello, [b]World[/b]!" class StatusTitle(Static): - total_messages: Reactive[int] = Reactive(0) - current_message_index: Reactive[int] = Reactive(0) - current_message_id: Reactive[int] = Reactive(1) + total_messages: Reactive[int] = reactive(0) + current_message_index: Reactive[int] = reactive(0) + current_message_id: Reactive[int] = reactive(1) + folder: Reactive[str] = reactive("INBOX") def render(self) -> RenderResult: - return f"Inbox Message ID: {self.current_message_id} | [b]{self.current_message_index}[/b]/{self.total_messages}" + return f"{self.folder} | ID: {self.current_message_id} | [b]{self.current_message_index}[/b]/{self.total_messages}" + @@ -48,9 +52,10 @@ class StatusTitle(Static): class EmailViewerApp(App): """A simple email viewer app using the Himalaya CLI.""" title = "Maildir GTD Reader" - current_message_id: Reactive[int] = Reactive(1) + current_message_id: Reactive[int] = reactive(1) CSS_PATH = "email_viewer.tcss" - + folder = reactive("INBOX") + markdown: Reactive[str] = reactive("Loading...") def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]: yield from super().get_system_commands(screen) @@ -60,6 +65,8 @@ class EmailViewerApp(App): yield SystemCommand("Archive Message", "Archive the current message", self.action_archive) yield SystemCommand("Open Message", "Open a specific message by ID", self.action_open) yield SystemCommand("Create Task", "Create a task using the task CLI", self.action_create_task) + yield SystemCommand("Oldest Message", "Show the oldest message", self.action_oldest) + yield SystemCommand("Newest Message", "Show the newest message", self.action_newest) BINDINGS = [ Binding("j", "next", "Next message"), @@ -80,20 +87,36 @@ class EmailViewerApp(App): def compose(self) -> ComposeResult: """Create child widgets for the app.""" - yield Header(show_clock=True) + # yield Header(show_clock=True) yield StatusTitle().data_bind(EmailViewerApp.current_message_id) - yield ScrollableContainer(Static(Label("Loading Email Viewer App"), id="main_content")) + yield EnvelopeHeader() + yield Markdown(id="main_content", markdown=self.markdown) yield Footer() - def watch_current_message_id(self, message_id: int, old_message_id: int) -> None: + def watch_current_message_id(self, old_message_id: int, new_message_id: int) -> None: """Called when the current message ID changes.""" - if (message_id == old_message_id): + logging.info(f"Current message ID changed from {old_message_id} to {new_message_id}") + self.query_one("#main_content").loading = True + self.markdown = "" + if (new_message_id == old_message_id): return - # self.notify("Current message ID changed", title="Status", severity="information") - logging.info(f"Current message ID changed to {message_id}") + try: + rawText = subprocess.run( + ["himalaya", "message", "read", str(new_message_id)], + capture_output=True, + text=True + ) + if rawText.returncode == 0: + # Render the email content as Markdown + fixedText = rawText.stdout.replace("(https://urldefense.com/v3/", "(") + fixedText = re.sub(r"atlOrigin.+?\)", ")", fixedText) + self.query_one("#main_content").loading = False + self.query_one("#main_content").update(markdown = str(fixedText)) + logging.info(fixedText) + result = subprocess.run( - ["himalaya", "envelope", "list", "-o", "json"], + ["himalaya", "envelope", "list", "-o", "json", "-s", "9999"], capture_output=True, text=True ) @@ -102,40 +125,35 @@ class EmailViewerApp(App): envelopes = json.loads(result.stdout) if envelopes: status = self.query_one(StatusTitle) - status.total_messages = len(envelopes) # Get the first envelope's ID - status.current_message_index = next( - (index + 1 for index, envelope in enumerate(envelopes) if int(envelope['id']) == message_id), - 0 # Default to 0 if no match is found - ) + status.total_messages = len(envelopes) + + headers = self.query_one(EnvelopeHeader) + # Find the index of the envelope that matches the current_message_id + for index, envelope in enumerate(sorted(envelopes, key=lambda x: int(x['id']))): + if int(envelope['id']) == new_message_id: + headers.subject = envelope['subject'] + 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") + status.current_message_index = index + 1 # 1-based index + break + status.update() + headers.update() + else: - self.query_one("#main_content", Static).update("Failed to fetch the most recent message ID.") + self.query_one("#main_content").update("Failed to fetch the most recent message ID.") except Exception as e: - self.query_one("#main_content", Static).update(f"Error: {e}") + self.query_one("#main_content").update(f"Error: {e}") def on_mount(self) -> None: self.alert_timer: Timer | None = None # Timer to throttle alerts self.theme = "monokai" + self.title = "MaildirGTD" # self.watch(self.query_one(StatusTitle), "current_message_id", update_progress) # Fetch the ID of the most recent message using the Himalaya CLI - try: - result = subprocess.run( - ["himalaya", "envelope", "list", "-o", "json"], - capture_output=True, - text=True - ) - if result.returncode == 0: - import json - envelopes = json.loads(result.stdout) - if envelopes: - self.current_message_id = int(envelopes[0]['id']) - else: - self.query_one("#main_content", Static).update("Failed to fetch the most recent message ID.") - except Exception as e: - self.query_one("#main_content", Static).update(f"Error: {e}") - - self.show_message(self.current_message_id) + self.action_oldest() def show_message(self, message_id: int) -> None: show_message(self, message_id) @@ -165,24 +183,30 @@ class EmailViewerApp(App): def action_scroll_down(self) -> None: """Scroll the main content down.""" - self.query_one("#main_content", Static).scroll_down() + self.query_one("#main_content").scroll_down() def action_scroll_up(self) -> None: """Scroll the main content up.""" - self.query_one("#main_content", Static).scroll_up() + self.query_one("#main_content").scroll_up() def action_scroll_page_down(self) -> None: """Scroll the main content down by a page.""" - self.query_one("#main_content", Static).scroll_page_down() + self.query_one("#main_content").scroll_page_down() def action_scroll_page_up(self) -> None: """Scroll the main content up by a page.""" - self.query_one("#main_content", Static).scroll_page_up() + self.query_one("#main_content").scroll_page_up() def action_quit(self) -> None: """Quit the application.""" self.exit() + def action_oldest(self) -> None: + action_oldest(self) + + def action_newest(self) -> None: + action_newest(self) + if __name__ == "__main__": app = EmailViewerApp() app.run() diff --git a/maildir_gtd/email_viewer.tcss b/maildir_gtd/email_viewer.tcss index 51138c8..93fffe5 100644 --- a/maildir_gtd/email_viewer.tcss +++ b/maildir_gtd/email_viewer.tcss @@ -18,6 +18,16 @@ StatusTitle { width: 100%; height: 1; color: $text; - background: $surface; + background: rgb(64, 62, 65); content-align: center middle; } + +EnvelopeHeader { + width: 100%; + height: auto; + background: $panel; +} + +#main_content { + padding: 1 2; +} diff --git a/maildir_gtd/widgets/EnvelopeHeader.py b/maildir_gtd/widgets/EnvelopeHeader.py new file mode 100644 index 0000000..99300a5 --- /dev/null +++ b/maildir_gtd/widgets/EnvelopeHeader.py @@ -0,0 +1,24 @@ +from textual.reactive import Reactive +from textual.app import RenderResult +from textual.widgets import Static, Label + +class EnvelopeHeader(Static): + + subject = Reactive("") + from_ = Reactive("") + to = Reactive("") + date = Reactive("") + + """Header for the email viewer.""" + def on_mount(self) -> None: + """Mount the header.""" + + def render(self) -> RenderResult: + return f"[b][dim]Subject:[/dim] {self.subject}[/] \r\n" \ + f"[dim]From:[/dim] {self.from_} \r\n" \ + f"[dim]To:[/dim] {self.to} \r\n" \ + f"[dim]Date:[/dim] {self.date}" + + + + diff --git a/maildir_gtd/widgets/__init__.py b/maildir_gtd/widgets/__init__.py new file mode 100644 index 0000000..e6ab631 --- /dev/null +++ b/maildir_gtd/widgets/__init__.py @@ -0,0 +1 @@ +# Initialize the screens subpackage diff --git a/maildir_gtd/widgets/__pycache__/EnvelopeHeader.cpython-311.pyc b/maildir_gtd/widgets/__pycache__/EnvelopeHeader.cpython-311.pyc new file mode 100644 index 0000000..b44a98a Binary files /dev/null and b/maildir_gtd/widgets/__pycache__/EnvelopeHeader.cpython-311.pyc differ diff --git a/maildir_gtd/widgets/__pycache__/__init__.cpython-311.pyc b/maildir_gtd/widgets/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..814758d Binary files /dev/null and b/maildir_gtd/widgets/__pycache__/__init__.cpython-311.pyc differ