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_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(
synchronize_maildir_async(
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),
delete_mail_async(maildir_path, headers,
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(
maildir_path,
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),
)
progress.console.print("[bold green]Step 2: New data fetched.[/bold green]")
click.echo("Sync complete.")

View File

@@ -4,7 +4,6 @@ from .widgets.EnvelopeHeader import EnvelopeHeader
from .actions.task import action_create_task
from .actions.open import action_open
from .actions.delete import delete_current
from .actions.archive import archive_current
from src.services.taskwarrior import client as taskwarrior_client
from src.services.himalaya import client as himalaya_client
from textual.containers import ScrollableContainer, Vertical, Horizontal
@@ -69,6 +68,7 @@ class EmailViewerApp(App):
status_title = reactive("Message View")
sort_order_ascending: Reactive[bool] = reactive(True)
selected_messages: Reactive[set[int]] = reactive(set())
main_content_visible: Reactive[bool] = reactive(True)
def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
yield from super().get_system_commands(screen)
@@ -110,7 +110,7 @@ class EmailViewerApp(App):
Binding("2", "focus_2", "Focus Folders Panel"),
Binding("3", "focus_3", "Focus Envelopes Panel"),
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(
@@ -215,35 +215,35 @@ class EmailViewerApp(App):
)
if new_message_id == old_message_id:
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.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:
# Pass the complete date string with timezone information
message_date = metadata["date"]
if 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)
if list_view.index != metadata["index"]:
list_view.index = metadata["index"]
else:
logging.warning(
f"Message ID {new_message_id} not found in metadata.")
logging.warning(f"Message ID {message_id} not found in metadata.")
def on_list_view_selected(self, event: ListView.Selected) -> None:
"""Called when an item in the list view is selected."""
@@ -286,9 +286,11 @@ class EmailViewerApp(App):
# Use the Himalaya client to fetch envelopes
envelopes, success = await himalaya_client.list_envelopes()
if success and envelopes:
if success:
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
# Use the centralized refresh method to update the ListView
@@ -530,20 +532,72 @@ class EmailViewerApp(App):
await worker.wait()
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:
message_ids = [str(msg_id) for msg_id in self.selected_messages]
_, success = await himalaya_client.archive_messages(message_ids)
# --- Multi-message archive ---
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:
self.show_status(f"{len(message_ids)} messages archived.")
self.show_status(message)
self.selected_messages.clear()
self.fetch_envelopes()
else:
self.show_status("Failed to archive messages.", "error")
self.show_status(f"Failed to archive messages: {message}", "error")
return
else:
# Call the archive_current function which uses our Himalaya client module
worker = archive_current(self)
await worker.wait()
# --- Single message archive ---
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()
# 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:
action_open(self)
@@ -567,6 +621,22 @@ class EmailViewerApp(App):
"""Scroll the main content up by a page."""
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:
self.exit()

View File

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