drive viewer
This commit is contained in:
@@ -28,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 widgets.ContentContainer import ContentContainer
|
||||
from maildir_gtd.utils import group_envelopes_by_date
|
||||
|
||||
logging.basicConfig(
|
||||
@@ -106,6 +107,7 @@ class EmailViewerApp(App):
|
||||
Binding("1", "focus_1", "Focus Accounts Panel"),
|
||||
Binding("2", "focus_2", "Focus Folders Panel"),
|
||||
Binding("3", "focus_3", "Focus Envelopes Panel"),
|
||||
Binding("f", "toggle_mode", "Toggle Content Mode"),
|
||||
]
|
||||
|
||||
BINDINGS.extend(
|
||||
@@ -129,7 +131,7 @@ class EmailViewerApp(App):
|
||||
ListView(id="folders_list", classes="list_view"),
|
||||
id="sidebar",
|
||||
),
|
||||
ScrollableContainer(EnvelopeHeader(), Markdown(), id="main_content"),
|
||||
ContentContainer(id="main_content"),
|
||||
id="outer-wrapper",
|
||||
)
|
||||
yield Footer()
|
||||
@@ -218,33 +220,33 @@ class EmailViewerApp(App):
|
||||
if new_message_id == old_message_id:
|
||||
return
|
||||
self.msg_worker.cancel() if self.msg_worker else None
|
||||
headers = self.query_one(EnvelopeHeader)
|
||||
logging.info(f"new_message_id: {new_message_id}, type: {type(new_message_id)}")
|
||||
logging.info(f"message_metadata keys: {list(self.message_metadata.keys())}")
|
||||
|
||||
content_container = self.query_one("#main_content")
|
||||
content_container.display_content(new_message_id)
|
||||
|
||||
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", "")
|
||||
message_date = re.sub(r"[\+\-]\d\d:\d\d", "", metadata["date"])
|
||||
message_date = datetime.strptime(message_date, "%Y-%m-%d %H:%M").strftime(
|
||||
"%a %b %d %H:%M"
|
||||
)
|
||||
headers.date = message_date
|
||||
headers.cc = metadata["cc"].get("addr", "") if "cc" in metadata else ""
|
||||
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 "",
|
||||
)
|
||||
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
|
||||
msg = self.query_one(Markdown)
|
||||
msg.update(self.message_body_cache[new_message_id])
|
||||
return
|
||||
else:
|
||||
self.query_one("#main_content").loading = True
|
||||
self.msg_worker = self.fetch_one_message(new_message_id)
|
||||
|
||||
|
||||
|
||||
|
||||
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
||||
"""Called when an item in the list view is selected."""
|
||||
@@ -257,31 +259,31 @@ class EmailViewerApp(App):
|
||||
return
|
||||
self.current_message_id = int(self.all_envelopes[event.list_view.index]["id"])
|
||||
|
||||
@work(exclusive=False)
|
||||
async def fetch_one_message(self, new_message_id: int) -> None:
|
||||
msg = self.query_one(Markdown)
|
||||
# @work(exclusive=False)
|
||||
# async def fetch_one_message(self, new_message_id: int) -> None:
|
||||
# content_container = self.query_one(ContentContainer)
|
||||
|
||||
try:
|
||||
process = await asyncio.create_subprocess_shell(
|
||||
f"himalaya message read {str(new_message_id)}",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
logging.info(f"stdout: {stdout.decode()[0:50]}...")
|
||||
if process.returncode == 0:
|
||||
# Render the email content as Markdown
|
||||
fixedText = stdout.decode().replace("(https://urldefense.com/v3/", "(")
|
||||
fixedText = re.sub(r"atlOrigin.+?\)", ")", fixedText)
|
||||
logging.info(f"rendering fixedText: {fixedText[0:50]}")
|
||||
self.message_body_cache[new_message_id] = fixedText
|
||||
await msg.update(fixedText)
|
||||
self.query_one("#main_content").loading = False
|
||||
logging.info(fixedText)
|
||||
# try:
|
||||
# process = await asyncio.create_subprocess_shell(
|
||||
# f"himalaya message read {str(new_message_id)} -p",
|
||||
# stdout=asyncio.subprocess.PIPE,
|
||||
# stderr=asyncio.subprocess.PIPE,
|
||||
# )
|
||||
# stdout, stderr = await process.communicate()
|
||||
# logging.info(f"stdout: {stdout.decode()[0:50]}...")
|
||||
# if process.returncode == 0:
|
||||
# # Render the email content as Markdown
|
||||
# fixedText = stdout.decode().replace("(https://urldefense.com/v3/", "(")
|
||||
# fixedText = re.sub(r"atlOrigin.+?\)", ")", fixedText)
|
||||
# logging.info(f"rendering fixedText: {fixedText[0:50]}")
|
||||
# self.message_body_cache[new_message_id] = fixedText
|
||||
# await content_container.display_content(new_message_id)
|
||||
# self.query_one("#main_content").loading = False
|
||||
# logging.info(fixedText)
|
||||
|
||||
except Exception as e:
|
||||
self.show_status(f"Error fetching message content: {e}", "error")
|
||||
logging.error(f"Error fetching message content: {e}")
|
||||
# except Exception as e:
|
||||
# self.show_status(f"Error fetching message content: {e}", "error")
|
||||
# logging.error(f"Error fetching message content: {e}")
|
||||
|
||||
@work(exclusive=False)
|
||||
async def fetch_envelopes(self) -> None:
|
||||
@@ -429,11 +431,7 @@ class EmailViewerApp(App):
|
||||
message, title="Status", severity=severity, timeout=2.6, markup=True
|
||||
)
|
||||
|
||||
def action_toggle_header(self) -> None:
|
||||
"""Toggle the visibility of the EnvelopeHeader panel."""
|
||||
header = self.query_one(EnvelopeHeader)
|
||||
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."""
|
||||
@@ -447,6 +445,11 @@ class EmailViewerApp(App):
|
||||
else:
|
||||
self.action_newest()
|
||||
|
||||
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()
|
||||
|
||||
def action_next(self) -> None:
|
||||
if not self.current_message_index >= 0:
|
||||
return
|
||||
|
||||
@@ -146,3 +146,22 @@ Label.group_header {
|
||||
width: 100%;
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
#plaintext_content {
|
||||
padding: 1 2;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#markdown_content {
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
ContentContainer {
|
||||
width: 100%;
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
127
maildir_gtd/widgets/ContentContainer.py
Normal file
127
maildir_gtd/widgets/ContentContainer.py
Normal file
@@ -0,0 +1,127 @@
|
||||
import re
|
||||
import asyncio
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
|
||||
from textual import work
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import Label, Markdown
|
||||
from textual.containers import ScrollableContainer
|
||||
|
||||
from widgets.EnvelopeHeader import EnvelopeHeader
|
||||
|
||||
class ContentContainer(ScrollableContainer):
|
||||
"""A custom container that can switch between plaintext and markdown rendering."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.plaintext_mode = True
|
||||
self.markup_worker = None
|
||||
self.current_text = ""
|
||||
self.current_id = None
|
||||
# LRU cache with a max size of 100 messages
|
||||
self.get_message_body = lru_cache(maxsize=100)(self._get_message_body)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the container with a label for plaintext and markdown for rich content."""
|
||||
yield EnvelopeHeader()
|
||||
yield Label(id="plaintext_content")
|
||||
yield Markdown(id="markdown_content", classes="hidden")
|
||||
|
||||
def update_header(self, subject: str = "", date: str = "", from_: str = "", to: str = "", cc: str = "", bcc: str = "") -> None:
|
||||
"""Update the header with the given email details."""
|
||||
header = self.query_one(EnvelopeHeader)
|
||||
header.subject = subject
|
||||
header.date = date
|
||||
header.from_ = from_
|
||||
header.to = to
|
||||
header.cc = cc
|
||||
header.bcc = bcc
|
||||
|
||||
def action_toggle_header(self) -> None:
|
||||
"""Toggle the visibility of the EnvelopeHeader panel."""
|
||||
header = self.query_one(EnvelopeHeader)
|
||||
header.styles.height = "1" if self.header_expanded else "auto"
|
||||
self.header_expanded = not self.header_expanded
|
||||
|
||||
async def display_content(self, message_id: int) -> None:
|
||||
"""Display content for the given message ID."""
|
||||
self.current_id = message_id
|
||||
|
||||
# Show loading state
|
||||
self.loading = True
|
||||
|
||||
# Get message body (from cache or fetch new)
|
||||
message_text = await self.get_message_body(message_id)
|
||||
self.current_text = message_text
|
||||
|
||||
# Update the plaintext content
|
||||
plaintext = self.query_one("#plaintext_content", Label)
|
||||
await plaintext.update(message_text)
|
||||
|
||||
if not self.plaintext_mode:
|
||||
# We're in markdown mode, so render the markdown
|
||||
await self.render_markdown()
|
||||
else:
|
||||
# Hide markdown, show plaintext
|
||||
plaintext.remove_class("hidden")
|
||||
self.query_one("#markdown_content").add_class("hidden")
|
||||
|
||||
self.loading = False
|
||||
|
||||
@work(exclusive=True)
|
||||
async def _get_message_body(self, message_id: int) -> str:
|
||||
"""Fetch the message body from Himalaya CLI."""
|
||||
try:
|
||||
process = await asyncio.create_subprocess_shell(
|
||||
f"himalaya message read {str(message_id)} -p",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
logging.info(f"stdout: {stdout.decode()[0:50]}...")
|
||||
|
||||
if process.returncode == 0:
|
||||
# Process the email content
|
||||
fixedText = stdout.decode().replace("https://urldefense.com/v3/", "")
|
||||
fixedText = re.sub(r"atlOrigin.+?\w", "", fixedText)
|
||||
logging.info(f"rendering fixedText: {fixedText[0:50]}")
|
||||
return fixedText
|
||||
else:
|
||||
logging.error(f"Error fetching message: {stderr.decode()}")
|
||||
return f"Error fetching message content: {stderr.decode()}"
|
||||
except Exception as e:
|
||||
logging.error(f"Error fetching message content: {e}")
|
||||
return f"Error fetching message content: {e}"
|
||||
|
||||
async def render_markdown(self) -> None:
|
||||
"""Render the markdown content asynchronously."""
|
||||
if self.markup_worker:
|
||||
self.markup_worker.cancel()
|
||||
|
||||
markdown = self.query_one("#markdown_content", Markdown)
|
||||
plaintext = self.query_one("#plaintext_content", Label)
|
||||
|
||||
await markdown.update(self.current_text)
|
||||
|
||||
# Show markdown, hide plaintext
|
||||
markdown.remove_class("hidden")
|
||||
plaintext.add_class("hidden")
|
||||
|
||||
async def toggle_mode(self) -> None:
|
||||
"""Toggle between plaintext and markdown mode."""
|
||||
self.plaintext_mode = not self.plaintext_mode
|
||||
|
||||
if self.plaintext_mode:
|
||||
# Switch to plaintext
|
||||
self.query_one("#plaintext_content").remove_class("hidden")
|
||||
self.query_one("#markdown_content").add_class("hidden")
|
||||
else:
|
||||
# Switch to markdown
|
||||
await self.render_markdown()
|
||||
|
||||
return self.plaintext_mode
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""Clear the message body cache."""
|
||||
self.get_message_body.cache_clear()
|
||||
Reference in New Issue
Block a user