This commit is contained in:
Tim Bendt
2025-07-08 08:47:58 -04:00
parent b46415b8d9
commit 7cc1c30356
3 changed files with 121 additions and 31 deletions

View File

@@ -216,6 +216,8 @@ async def _sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forwar
task_archive = progress.add_task("[yellow]Archiving mail...", total=0) task_archive = progress.add_task("[yellow]Archiving mail...", total=0)
task_delete = progress.add_task("[red]Deleting mail...", total=0) task_delete = progress.add_task("[red]Deleting mail...", total=0)
# Stage 1: Synchronize local changes (read, archive, delete) to the server
progress.console.print("[bold cyan]Step 1: Syncing local changes to server...[/bold cyan]")
await asyncio.gather( await asyncio.gather(
synchronize_maildir_async( synchronize_maildir_async(
maildir_path, headers, progress, task_read, dry_run maildir_path, headers, progress, task_read, dry_run
@@ -224,6 +226,12 @@ async def _sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forwar
progress, task_archive, dry_run), progress, task_archive, dry_run),
delete_mail_async(maildir_path, headers, delete_mail_async(maildir_path, headers,
progress, task_delete, dry_run), progress, task_delete, dry_run),
)
progress.console.print("[bold green]Step 1: Local changes synced.[/bold green]")
# Stage 2: Fetch new data from the server
progress.console.print("\n[bold cyan]Step 2: Fetching new data from server...[/bold cyan]")
await asyncio.gather(
fetch_mail_async( fetch_mail_async(
maildir_path, maildir_path,
attachments_dir, attachments_dir,
@@ -235,6 +243,7 @@ async def _sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forwar
), ),
fetch_calendar_async(headers, progress, task_calendar, dry_run, vdir, icsfile, org, days_back, days_forward, continue_iteration), fetch_calendar_async(headers, progress, task_calendar, dry_run, vdir, icsfile, org, days_back, days_forward, continue_iteration),
) )
progress.console.print("[bold green]Step 2: New data fetched.[/bold green]")
click.echo("Sync complete.") click.echo("Sync complete.")

View File

@@ -4,7 +4,6 @@ from .widgets.EnvelopeHeader import EnvelopeHeader
from .actions.task import action_create_task from .actions.task import action_create_task
from .actions.open import action_open from .actions.open import action_open
from .actions.delete import delete_current from .actions.delete import delete_current
from .actions.archive import archive_current
from src.services.taskwarrior import client as taskwarrior_client from src.services.taskwarrior import client as taskwarrior_client
from src.services.himalaya import client as himalaya_client from src.services.himalaya import client as himalaya_client
from textual.containers import ScrollableContainer, Vertical, Horizontal from textual.containers import ScrollableContainer, Vertical, Horizontal
@@ -69,6 +68,7 @@ class EmailViewerApp(App):
status_title = reactive("Message View") status_title = reactive("Message View")
sort_order_ascending: Reactive[bool] = reactive(True) sort_order_ascending: Reactive[bool] = reactive(True)
selected_messages: Reactive[set[int]] = reactive(set()) selected_messages: Reactive[set[int]] = reactive(set())
main_content_visible: Reactive[bool] = reactive(True)
def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]: def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
yield from super().get_system_commands(screen) yield from super().get_system_commands(screen)
@@ -110,7 +110,7 @@ class EmailViewerApp(App):
Binding("2", "focus_2", "Focus Folders Panel"), Binding("2", "focus_2", "Focus Folders Panel"),
Binding("3", "focus_3", "Focus Envelopes Panel"), Binding("3", "focus_3", "Focus Envelopes Panel"),
Binding("4", "focus_4", "Focus Main Content"), Binding("4", "focus_4", "Focus Main Content"),
Binding("m", "toggle_mode", "Toggle Content Mode"), Binding("w", "toggle_main_content", "Toggle Message View Window"),
] ]
BINDINGS.extend( BINDINGS.extend(
@@ -215,35 +215,35 @@ class EmailViewerApp(App):
) )
if new_message_id == old_message_id: if new_message_id == old_message_id:
return return
self.msg_worker.cancel() if self.msg_worker else None
logging.info(f"new_message_id: {new_message_id}, type: {
type(new_message_id)}")
# If the main content view is not visible, don't load the message
if not self.main_content_visible:
return
# Cancel any existing message loading worker
if self.msg_worker:
self.msg_worker.cancel()
# Start a new worker to load the message content
self.msg_worker = self.load_message_content(new_message_id)
@work(exclusive=True)
async def load_message_content(self, message_id: int) -> None:
"""Worker to load message content asynchronously."""
content_container = self.query_one(ContentContainer) content_container = self.query_one(ContentContainer)
content_container.display_content(new_message_id) content_container.display_content(message_id)
metadata = self.message_store.get_metadata(new_message_id) metadata = self.message_store.get_metadata(message_id)
if metadata: if metadata:
# Pass the complete date string with timezone information
message_date = metadata["date"] message_date = metadata["date"]
if self.current_message_index != metadata["index"]: if self.current_message_index != metadata["index"]:
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", ""),
# to=metadata["to"].get("addr", ""),
# date=message_date,
# cc=metadata["cc"].get("addr", "") if "cc" in metadata else "",
# )
list_view = self.query_one("#envelopes_list", ListView) list_view = self.query_one("#envelopes_list", ListView)
if list_view.index != metadata["index"]: if list_view.index != metadata["index"]:
list_view.index = metadata["index"] list_view.index = metadata["index"]
else: else:
logging.warning( logging.warning(f"Message ID {message_id} not found in metadata.")
f"Message ID {new_message_id} not found in metadata.")
def on_list_view_selected(self, event: ListView.Selected) -> None: def on_list_view_selected(self, event: ListView.Selected) -> None:
"""Called when an item in the list view is selected.""" """Called when an item in the list view is selected."""
@@ -286,9 +286,11 @@ class EmailViewerApp(App):
# Use the Himalaya client to fetch envelopes # Use the Himalaya client to fetch envelopes
envelopes, success = await himalaya_client.list_envelopes() envelopes, success = await himalaya_client.list_envelopes()
if success and envelopes: if success:
self.reload_needed = False self.reload_needed = False
self.message_store.load(envelopes, self.sort_order_ascending) # Ensure envelopes is a list, even if it's None from the client
envelopes_list = envelopes if envelopes is not None else []
self.message_store.load(envelopes_list, self.sort_order_ascending)
self.total_messages = self.message_store.total_messages self.total_messages = self.message_store.total_messages
# Use the centralized refresh method to update the ListView # Use the centralized refresh method to update the ListView
@@ -530,21 +532,73 @@ class EmailViewerApp(App):
await worker.wait() await worker.wait()
async def action_archive(self) -> None: async def action_archive(self) -> None:
"""Archive the current message and update UI consistently.""" """Archive the current or selected messages and update UI consistently."""
next_id_to_select = None
if self.selected_messages: if self.selected_messages:
message_ids = [str(msg_id) for msg_id in self.selected_messages] # --- Multi-message archive ---
_, success = await himalaya_client.archive_messages(message_ids) message_ids_to_archive = list(self.selected_messages)
if message_ids_to_archive:
highest_archived_id = max(message_ids_to_archive)
metadata = self.message_store.get_metadata(highest_archived_id)
if metadata:
next_id, _ = self.message_store.find_next_valid_id(metadata["index"])
if next_id is None:
next_id, _ = self.message_store.find_prev_valid_id(
metadata["index"]
)
next_id_to_select = next_id
message, success = await himalaya_client.archive_messages(
[str(mid) for mid in message_ids_to_archive]
)
if success: if success:
self.show_status(f"{len(message_ids)} messages archived.") self.show_status(message)
self.selected_messages.clear() self.selected_messages.clear()
self.fetch_envelopes()
else: else:
self.show_status("Failed to archive messages.", "error") self.show_status(f"Failed to archive messages: {message}", "error")
return
else: else:
# Call the archive_current function which uses our Himalaya client module # --- Single message archive ---
worker = archive_current(self) if not self.current_message_id:
self.show_status("No message selected to archive.", "error")
return
current_id = self.current_message_id
current_idx = self.current_message_index
next_id, _ = self.message_store.find_next_valid_id(current_idx)
if next_id is None:
next_id, _ = self.message_store.find_prev_valid_id(current_idx)
next_id_to_select = next_id
message, success = await himalaya_client.archive_message(current_id)
if success:
self.show_status(message)
else:
self.show_status(
f"Failed to archive message {current_id}: {message}", "error"
)
return
# Refresh the envelope list
worker = self.fetch_envelopes()
await worker.wait() await worker.wait()
# After refresh, select the next message
if next_id_to_select:
new_metadata = self.message_store.get_metadata(next_id_to_select)
if new_metadata:
self.current_message_id = next_id_to_select
else:
self.action_oldest()
else:
self.action_oldest()
def action_open(self) -> None: def action_open(self) -> None:
action_open(self) action_open(self)
@@ -567,6 +621,22 @@ class EmailViewerApp(App):
"""Scroll the main content up by a page.""" """Scroll the main content up by a page."""
self.query_one("#main_content").scroll_page_up() self.query_one("#main_content").scroll_page_up()
def action_toggle_main_content(self) -> None:
"""Toggle the visibility of the main content pane."""
self.main_content_visible = not self.main_content_visible
def watch_main_content_visible(self, visible: bool) -> None:
"""Called when main_content_visible changes."""
main_content = self.query_one("#main_content")
accounts_list = self.query_one("#accounts_list")
folders_list = self.query_one("#folders_list")
main_content.display = visible
accounts_list.display = visible
folders_list.display = visible
self.query_one("#envelopes_list").focus()
def action_quit(self) -> None: def action_quit(self) -> None:
self.exit() self.exit()

View File

@@ -1,5 +1,6 @@
from markitdown import MarkItDown from markitdown import MarkItDown
from textual import work from textual import work
from textual.binding import Binding
from textual.containers import Vertical, ScrollableContainer from textual.containers import Vertical, ScrollableContainer
from textual.widgets import Static, Markdown, Label from textual.widgets import Static, Markdown, Label
from src.services.himalaya import client as himalaya_client from src.services.himalaya import client as himalaya_client
@@ -57,6 +58,9 @@ class EnvelopeHeader(Vertical):
class ContentContainer(ScrollableContainer): class ContentContainer(ScrollableContainer):
can_focus = True can_focus = True
BINDINGS = [
Binding("m", "toggle_mode", "Toggle View Mode")
]
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
@@ -79,7 +83,7 @@ class ContentContainer(ScrollableContainer):
self.content.styles.display = "none" self.content.styles.display = "none"
self.html_content.styles.display = "block" self.html_content.styles.display = "block"
async def toggle_mode(self): async def action_toggle_mode(self):
"""Toggle between plaintext and HTML viewing modes.""" """Toggle between plaintext and HTML viewing modes."""
if self.current_mode == "html": if self.current_mode == "html":
self.current_mode = "text" self.current_mode = "text"
@@ -91,6 +95,7 @@ class ContentContainer(ScrollableContainer):
self.html_content.styles.display = "block" self.html_content.styles.display = "block"
# self.action_notify(f"switched to mode {self.current_mode}") # self.action_notify(f"switched to mode {self.current_mode}")
# Reload the content if we have a message ID # Reload the content if we have a message ID
self.border_sibtitle = self.current_mode;
if self.current_message_id: if self.current_message_id:
self.display_content(self.current_message_id) self.display_content(self.current_message_id)
@@ -119,6 +124,12 @@ class ContentContainer(ScrollableContainer):
self.current_message_id = message_id self.current_message_id = message_id
# Immediately show a loading message
if self.current_mode == "text":
self.content.update("Loading...")
else:
self.html_content.update("Loading...")
# Cancel any existing content fetch operations # Cancel any existing content fetch operations
if self.content_worker: if self.content_worker:
self.content_worker.cancel() self.content_worker.cancel()