wip
This commit is contained in:
@@ -1,6 +1,3 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from textual import work
|
||||
from src.services.himalaya import client as himalaya_client
|
||||
|
||||
@@ -23,7 +20,7 @@ async def archive_current(app):
|
||||
next_id, next_idx = app.message_store.find_prev_valid_id(current_index)
|
||||
|
||||
# Archive the message using our Himalaya client module
|
||||
success = await himalaya_client.archive_message(current_message_id)
|
||||
success = await himalaya_client.archive_messages([str(current_message_id)])
|
||||
|
||||
if success:
|
||||
app.show_status(f"Message {current_message_id} archived.", "success")
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from .message_store import MessageStore
|
||||
from .widgets.ContentContainer import ContentContainer
|
||||
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 src.services.taskwarrior import client as taskwarrior_client
|
||||
from src.services.himalaya import client as himalaya_client
|
||||
from textual.containers import ScrollableContainer, Vertical, Horizontal
|
||||
from textual.containers import Container, ScrollableContainer, Vertical, Horizontal
|
||||
from textual.timer import Timer
|
||||
from textual.binding import Binding
|
||||
from textual.reactive import reactive, Reactive
|
||||
@@ -16,14 +15,11 @@ from textual.logging import TextualHandler
|
||||
from textual.app import App, ComposeResult, SystemCommand, RenderResult
|
||||
from textual.worker import Worker
|
||||
from textual import work
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
from datetime import UTC, datetime
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Iterable, Optional, List, Dict, Any, Generator, Tuple
|
||||
from collections import defaultdict
|
||||
|
||||
# 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__))))
|
||||
@@ -57,6 +53,7 @@ class EmailViewerApp(App):
|
||||
title = "Maildir GTD Reader"
|
||||
current_message_id: Reactive[int] = reactive(0)
|
||||
current_message_index: Reactive[int] = reactive(0)
|
||||
highlighted_message_index: Reactive[int] = reactive(0)
|
||||
folder = reactive("INBOX")
|
||||
header_expanded = reactive(False)
|
||||
reload_needed = reactive(True)
|
||||
@@ -147,8 +144,7 @@ class EmailViewerApp(App):
|
||||
self.title = "MaildirGTD"
|
||||
self.query_one("#main_content").border_title = self.status_title
|
||||
sort_indicator = "↑" if self.sort_order_ascending else "↓"
|
||||
self.query_one("#envelopes_list").border_title = f"1️⃣ Emails {
|
||||
sort_indicator}"
|
||||
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"
|
||||
@@ -171,8 +167,7 @@ class EmailViewerApp(App):
|
||||
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 = "↑" if new_value else "↓"
|
||||
self.query_one("#envelopes_list").border_title = f"1️⃣ Emails {
|
||||
sort_indicator}"
|
||||
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:
|
||||
if new_index < 0:
|
||||
@@ -183,9 +178,11 @@ class EmailViewerApp(App):
|
||||
self.current_message_index = new_index
|
||||
|
||||
self._update_list_view_subtitle()
|
||||
self.query_one("#envelopes_list").index = new_index
|
||||
self.query_one("#envelopes_list", ListView).index = new_index
|
||||
|
||||
def watch_selected_messages(self, old_messages: set[int], new_messages: set[int]) -> None:
|
||||
def watch_selected_messages(
|
||||
self, old_messages: set[int], new_messages: set[int]
|
||||
) -> None:
|
||||
self._update_list_view_subtitle()
|
||||
|
||||
def _update_list_view_subtitle(self) -> None:
|
||||
@@ -210,8 +207,7 @@ class EmailViewerApp(App):
|
||||
) -> None:
|
||||
"""Called when the current message ID changes."""
|
||||
logging.info(
|
||||
f"Current message ID changed from {
|
||||
old_message_id} to {new_message_id}"
|
||||
f"Current message ID changed from {old_message_id} to {new_message_id}"
|
||||
)
|
||||
if new_message_id == old_message_id:
|
||||
return
|
||||
@@ -273,9 +269,9 @@ class EmailViewerApp(App):
|
||||
if current_item is None or current_item.get("type") == "header":
|
||||
return
|
||||
|
||||
message_id = int(current_item["id"])
|
||||
self.current_message_id = message_id
|
||||
self.current_message_index = highlighted_index
|
||||
# message_id = int(current_item["id"])
|
||||
# self.current_message_id = message_id
|
||||
self.highlighted_message_index = highlighted_index
|
||||
|
||||
@work(exclusive=False)
|
||||
async def fetch_envelopes(self) -> None:
|
||||
@@ -307,7 +303,7 @@ class EmailViewerApp(App):
|
||||
|
||||
@work(exclusive=False)
|
||||
async def fetch_accounts(self) -> None:
|
||||
accounts_list = self.query_one("#accounts_list")
|
||||
accounts_list = self.query_one("#accounts_list", ListView)
|
||||
try:
|
||||
accounts_list.loading = True
|
||||
|
||||
@@ -333,7 +329,7 @@ class EmailViewerApp(App):
|
||||
|
||||
@work(exclusive=False)
|
||||
async def fetch_folders(self) -> None:
|
||||
folders_list = self.query_one("#folders_list")
|
||||
folders_list = self.query_one("#folders_list", ListView)
|
||||
folders_list.clear()
|
||||
folders_list.append(
|
||||
ListItem(Label("INBOX", classes="folder_name", markup=False))
|
||||
@@ -370,27 +366,29 @@ class EmailViewerApp(App):
|
||||
if item and item.get("type") == "header":
|
||||
envelopes_list.append(
|
||||
ListItem(
|
||||
Horizontal(
|
||||
Label("", classes="checkbox"), # Hidden checkbox for header
|
||||
Container(
|
||||
Label("", classes="checkbox"), # Hidden checkbox for header
|
||||
Label(
|
||||
item["label"],
|
||||
classes="group_header",
|
||||
markup=False,
|
||||
),
|
||||
classes="envelope_item_row"
|
||||
classes="envelope_item_row",
|
||||
)
|
||||
)
|
||||
)
|
||||
elif item: # Check if not None
|
||||
# Extract sender and date
|
||||
sender_name = item.get("from", {}).get("name", item.get("from", {}).get("addr", "Unknown"))
|
||||
sender_name = item.get("from", {}).get(
|
||||
"name", item.get("from", {}).get("addr", "Unknown")
|
||||
)
|
||||
if not sender_name:
|
||||
sender_name = item.get("from", {}).get("addr", "Unknown")
|
||||
|
||||
|
||||
# Truncate sender name
|
||||
max_sender_len = 25 # Adjust as needed
|
||||
max_sender_len = 25 # Adjust as needed
|
||||
if len(sender_name) > max_sender_len:
|
||||
sender_name = sender_name[:max_sender_len-3] + "..."
|
||||
sender_name = sender_name[: max_sender_len - 3] + "..."
|
||||
|
||||
message_date_str = item.get("date", "")
|
||||
formatted_date = ""
|
||||
@@ -403,25 +401,26 @@ class EmailViewerApp(App):
|
||||
formatted_date = "Invalid Date"
|
||||
|
||||
list_item = ListItem(
|
||||
Vertical(
|
||||
Horizontal(
|
||||
Label("☐", classes="checkbox"), # Placeholder for checkbox
|
||||
Container(
|
||||
Container(
|
||||
Label("☐", classes="checkbox"), # Placeholder for checkbox
|
||||
Label(sender_name, classes="sender_name"),
|
||||
Label(formatted_date, classes="message_date"),
|
||||
classes="envelope_header_row"
|
||||
classes="envelope_header_row",
|
||||
),
|
||||
Horizontal(
|
||||
Container(
|
||||
Label(
|
||||
str(item.get("subject", "")).strip(),
|
||||
classes="email_subject",
|
||||
markup=False,
|
||||
)
|
||||
),
|
||||
classes="envelope_subject_row",
|
||||
),
|
||||
classes="envelope_item_row"
|
||||
classes="envelope_item_row",
|
||||
)
|
||||
)
|
||||
envelopes_list.append(list_item)
|
||||
self.refresh_list_view_items() # Initial refresh of item states
|
||||
self.refresh_list_view_items() # Initial refresh of item states
|
||||
|
||||
def refresh_list_view_items(self) -> None:
|
||||
"""Update the visual state of existing ListItems without clearing the list."""
|
||||
@@ -429,48 +428,44 @@ class EmailViewerApp(App):
|
||||
for i, list_item in enumerate(envelopes_list.children):
|
||||
if isinstance(list_item, ListItem):
|
||||
item_data = self.message_store.envelopes[i]
|
||||
|
||||
|
||||
# Find the checkbox label within the ListItem's children
|
||||
checkbox_label = None
|
||||
for child in list_item.walk_children():
|
||||
if isinstance(child, Label) and "checkbox" in child.classes:
|
||||
checkbox_label = child
|
||||
break
|
||||
# checkbox_label = list_item.query_one(".checkbox", Label)
|
||||
if item_data and item_data.get("type") != "header":
|
||||
message_id = int(item_data["id"])
|
||||
is_selected = message_id in self.selected_messages or False
|
||||
list_item.set_class(is_selected, "selection")
|
||||
|
||||
if checkbox_label:
|
||||
if item_data and item_data.get("type") != "header":
|
||||
message_id = int(item_data["id"])
|
||||
is_selected = message_id in self.selected_messages
|
||||
# if checkbox_label:
|
||||
# checkbox_label.update("\uf4a7" if is_selected else "\ue640")
|
||||
# checkbox_label.display = True # Always display checkbox
|
||||
|
||||
checkbox_label.update("\uf4a7" if is_selected else "\ue640")
|
||||
checkbox_label.display = True # Always display checkbox
|
||||
# list_item.highlighted = is_selected
|
||||
|
||||
list_item.highlighted = is_selected
|
||||
# # Update sender and date labels
|
||||
# sender_name = item_data.get("from", {}).get("name", item_data.get("from", {}).get("addr", "Unknown"))
|
||||
# if not sender_name:
|
||||
# sender_name = item_data.get("from", {}).get("addr", "Unknown")
|
||||
# max_sender_len = 25
|
||||
# if len(sender_name) > max_sender_len:
|
||||
# sender_name = sender_name[:max_sender_len-3] + "..."
|
||||
# list_item.query_one(".sender_name", Label).update(sender_name)
|
||||
|
||||
# Update sender and date labels
|
||||
sender_name = item_data.get("from", {}).get("name", item_data.get("from", {}).get("addr", "Unknown"))
|
||||
if not sender_name:
|
||||
sender_name = item_data.get("from", {}).get("addr", "Unknown")
|
||||
max_sender_len = 25
|
||||
if len(sender_name) > max_sender_len:
|
||||
sender_name = sender_name[:max_sender_len-3] + "..."
|
||||
list_item.query_one(".sender_name", Label).update(sender_name)
|
||||
# message_date_str = item_data.get("date", "")
|
||||
# formatted_date = ""
|
||||
# if message_date_str:
|
||||
# try:
|
||||
# dt_object = datetime.fromisoformat(message_date_str)
|
||||
# formatted_date = dt_object.strftime("%m/%d %H:%M")
|
||||
# except ValueError:
|
||||
# formatted_date = "Invalid Date"
|
||||
# list_item.query_one(".message_date", Label).update(formatted_date)
|
||||
|
||||
message_date_str = item_data.get("date", "")
|
||||
formatted_date = ""
|
||||
if message_date_str:
|
||||
try:
|
||||
dt_object = datetime.fromisoformat(message_date_str)
|
||||
formatted_date = dt_object.strftime("%m/%d %H:%M")
|
||||
except ValueError:
|
||||
formatted_date = "Invalid Date"
|
||||
list_item.query_one(".message_date", Label).update(formatted_date)
|
||||
|
||||
else:
|
||||
# For header items, checkbox should be unchecked and visible
|
||||
checkbox_label.update("\ue640") # Always unchecked for headers
|
||||
checkbox_label.display = True # Always display checkbox
|
||||
list_item.highlighted = False # Headers are never highlighted for selection
|
||||
# else:
|
||||
# # For header items, checkbox should be unchecked and visible
|
||||
# checkbox_label.update("\ue640") # Always unchecked for headers
|
||||
# checkbox_label.display = True # Always display checkbox
|
||||
# list_item.highlighted = False # Headers are never highlighted for selection
|
||||
|
||||
# Update total messages count (this is still fine here)
|
||||
# self.total_messages = self.message_store.total_messages
|
||||
@@ -501,7 +496,7 @@ class EmailViewerApp(App):
|
||||
async def action_toggle_mode(self) -> None:
|
||||
"""Toggle the content mode between plaintext and markdown."""
|
||||
content_container = self.query_one(ContentContainer)
|
||||
await content_container.toggle_mode()
|
||||
await content_container.action_toggle_mode()
|
||||
|
||||
def action_next(self) -> None:
|
||||
if not self.current_message_index >= 0:
|
||||
@@ -543,7 +538,9 @@ class EmailViewerApp(App):
|
||||
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"])
|
||||
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"]
|
||||
@@ -555,7 +552,7 @@ class EmailViewerApp(App):
|
||||
)
|
||||
|
||||
if success:
|
||||
self.show_status(message)
|
||||
self.show_status(message or "Success archived")
|
||||
self.selected_messages.clear()
|
||||
else:
|
||||
self.show_status(f"Failed to archive messages: {message}", "error")
|
||||
@@ -575,10 +572,10 @@ class EmailViewerApp(App):
|
||||
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)
|
||||
message, success = await himalaya_client.archive_messages([str(current_id)])
|
||||
|
||||
if success:
|
||||
self.show_status(message)
|
||||
self.show_status(message or "Archived")
|
||||
else:
|
||||
self.show_status(
|
||||
f"Failed to archive message {current_id}: {message}", "error"
|
||||
@@ -642,32 +639,27 @@ class EmailViewerApp(App):
|
||||
|
||||
def action_toggle_selection(self) -> None:
|
||||
"""Toggle selection for the current message."""
|
||||
current_item_data = self.message_store.envelopes[self.current_message_index]
|
||||
current_item_data = self.message_store.envelopes[self.highlighted_message_index]
|
||||
if current_item_data and current_item_data.get("type") != "header":
|
||||
message_id = int(current_item_data["id"])
|
||||
envelopes_list = self.query_one("#envelopes_list", ListView)
|
||||
current_list_item = envelopes_list.children[self.highlighted_message_index]
|
||||
checkbox_label = current_list_item.query_one(".checkbox", Label)
|
||||
if message_id in self.selected_messages:
|
||||
self.selected_messages.remove(message_id)
|
||||
checkbox_label.remove_class("x-list")
|
||||
checkbox_label.update("\ue640")
|
||||
else:
|
||||
self.selected_messages.add(message_id)
|
||||
|
||||
# Manually update the current ListItem
|
||||
envelopes_list = self.query_one("#envelopes_list", ListView)
|
||||
current_list_item = envelopes_list.children[self.current_message_index]
|
||||
if isinstance(current_list_item, ListItem):
|
||||
checkbox_label = None
|
||||
for child in current_list_item.walk_children():
|
||||
if isinstance(child, Label) and "checkbox" in child.classes:
|
||||
checkbox_label = child
|
||||
break
|
||||
if checkbox_label:
|
||||
checkbox_label.update("\uf4a7" if message_id in self.selected_messages else "\ue640")
|
||||
current_list_item.highlighted = (message_id in self.selected_messages)
|
||||
checkbox_label.add_class("x-list")
|
||||
checkbox_label.update("\uf4a7")
|
||||
|
||||
self._update_list_view_subtitle()
|
||||
|
||||
def action_clear_selection(self) -> None:
|
||||
"""Clear all selected messages."""
|
||||
self.selected_messages.clear()
|
||||
self.refresh_list_view_items() # Refresh all items to uncheck checkboxes
|
||||
self.refresh_list_view_items() # Refresh all items to uncheck checkboxes
|
||||
self._update_list_view_subtitle()
|
||||
|
||||
def action_oldest(self) -> None:
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
}
|
||||
|
||||
.envelope-selected {
|
||||
tint: $accent 20%;
|
||||
}
|
||||
|
||||
#sidebar:focus-within {
|
||||
background: $panel;
|
||||
.list_view:blur {
|
||||
@@ -112,6 +116,7 @@ Markdown {
|
||||
ListItem:even {
|
||||
background: rgb(50, 50, 56);
|
||||
}
|
||||
|
||||
& > ListItem {
|
||||
&.-highlight, .selection {
|
||||
color: $block-cursor-blurred-foreground;
|
||||
@@ -124,11 +129,18 @@ Markdown {
|
||||
.envelope_item_row {
|
||||
height: auto;
|
||||
width: 1fr;
|
||||
Horizontal {
|
||||
|
||||
.envelope_header_row, .envelope_subject_row {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.x-list {
|
||||
tint: $accent 20%;
|
||||
}
|
||||
|
||||
#open_message_container, #create_task_container {
|
||||
border: panel $border;
|
||||
dock: right;
|
||||
|
||||
@@ -7,7 +7,7 @@ from .client import (
|
||||
list_accounts,
|
||||
list_folders,
|
||||
delete_message,
|
||||
archive_message,
|
||||
archive_messages,
|
||||
get_message_content,
|
||||
)
|
||||
|
||||
@@ -16,6 +16,6 @@ __all__ = [
|
||||
"list_accounts",
|
||||
"list_folders",
|
||||
"delete_message",
|
||||
"archive_message",
|
||||
"archive_messages",
|
||||
"get_message_content",
|
||||
]
|
||||
|
||||
@@ -116,28 +116,28 @@ async def delete_message(message_id: int) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def archive_message(message_id: int) -> bool:
|
||||
"""
|
||||
Archive a message by its ID.
|
||||
# async def archive_message(message_id: int) -> [str, bool]:
|
||||
# """
|
||||
# Archive a message by its ID.
|
||||
|
||||
Args:
|
||||
message_id: The ID of the message to archive
|
||||
# Args:
|
||||
# message_id: The ID of the message to archive
|
||||
|
||||
Returns:
|
||||
True if archiving was successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
process = await asyncio.create_subprocess_shell(
|
||||
f"himalaya message move Archives {message_id}",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
# Returns:
|
||||
# True if archiving was successful, False otherwise
|
||||
# """
|
||||
# try:
|
||||
# process = await asyncio.create_subprocess_shell(
|
||||
# f"himalaya message move Archives {message_id}",
|
||||
# stdout=asyncio.subprocess.PIPE,
|
||||
# stderr=asyncio.subprocess.PIPE,
|
||||
# )
|
||||
# stdout, stderr = await process.communicate()
|
||||
|
||||
return process.returncode == 0
|
||||
except Exception as e:
|
||||
logging.error(f"Exception during message archiving: {e}")
|
||||
return False
|
||||
# return [stdout.decode(), process.returncode == 0]
|
||||
# except Exception as e:
|
||||
# logging.error(f"Exception during message archiving: {e}")
|
||||
# return False
|
||||
|
||||
|
||||
async def archive_messages(message_ids: List[str]) -> Tuple[Optional[str], bool]:
|
||||
@@ -197,8 +197,7 @@ async def get_message_content(message_id: int) -> Tuple[Optional[str], bool]:
|
||||
content = stdout.decode()
|
||||
return content, True
|
||||
else:
|
||||
logging.error(f"Error retrieving message content: {
|
||||
stderr.decode()}")
|
||||
logging.error(f"Error retrieving message content: {stderr.decode()}")
|
||||
return None, False
|
||||
except Exception as e:
|
||||
logging.error(f"Exception during message content retrieval: {e}")
|
||||
|
||||
Reference in New Issue
Block a user