fix link shortcut and mark as read
This commit is contained in:
@@ -244,9 +244,42 @@ class EmailViewerApp(App):
|
|||||||
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"]
|
||||||
|
|
||||||
|
# Mark message as read
|
||||||
|
await self._mark_message_as_read(message_id, metadata["index"])
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Message ID {message_id} not found in metadata.")
|
logging.warning(f"Message ID {message_id} not found in metadata.")
|
||||||
|
|
||||||
|
async def _mark_message_as_read(self, message_id: int, index: int) -> None:
|
||||||
|
"""Mark a message as read and update the UI."""
|
||||||
|
# Check if already read
|
||||||
|
envelope_data = self.message_store.envelopes[index]
|
||||||
|
if envelope_data and envelope_data.get("type") != "header":
|
||||||
|
flags = envelope_data.get("flags", [])
|
||||||
|
if "Seen" in flags:
|
||||||
|
return # Already read
|
||||||
|
|
||||||
|
# Mark as read via himalaya
|
||||||
|
_, success = await himalaya_client.mark_as_read(message_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Update the envelope flags in the store
|
||||||
|
if envelope_data:
|
||||||
|
if "flags" not in envelope_data:
|
||||||
|
envelope_data["flags"] = []
|
||||||
|
if "Seen" not in envelope_data["flags"]:
|
||||||
|
envelope_data["flags"].append("Seen")
|
||||||
|
|
||||||
|
# Update the visual state of the list item
|
||||||
|
try:
|
||||||
|
list_view = self.query_one("#envelopes_list", ListView)
|
||||||
|
list_item = list_view.children[index]
|
||||||
|
envelope_widget = list_item.query_one(EnvelopeListItem)
|
||||||
|
envelope_widget.is_read = True
|
||||||
|
envelope_widget.remove_class("unread")
|
||||||
|
except Exception:
|
||||||
|
pass # Widget may not exist
|
||||||
|
|
||||||
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."""
|
||||||
if event.list_view.index is None:
|
if event.list_view.index is None:
|
||||||
|
|||||||
@@ -411,6 +411,10 @@ class LinkPanel(ModalScreen):
|
|||||||
self._mnemonic_map: dict[str, LinkItem] = {
|
self._mnemonic_map: dict[str, LinkItem] = {
|
||||||
link.mnemonic: link for link in links if link.mnemonic
|
link.mnemonic: link for link in links if link.mnemonic
|
||||||
}
|
}
|
||||||
|
self._key_buffer: str = ""
|
||||||
|
self._key_timer = None
|
||||||
|
# Check if we have any multi-char mnemonics
|
||||||
|
self._has_multi_char = any(len(m) > 1 for m in self._mnemonic_map.keys())
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
with Container(id="link-panel-container"):
|
with Container(id="link-panel-container"):
|
||||||
@@ -436,18 +440,68 @@ class LinkPanel(ModalScreen):
|
|||||||
self.query_one("#link-list").focus()
|
self.query_one("#link-list").focus()
|
||||||
|
|
||||||
def on_key(self, event) -> None:
|
def on_key(self, event) -> None:
|
||||||
"""Handle mnemonic key presses."""
|
"""Handle mnemonic key presses with buffering for multi-char mnemonics."""
|
||||||
key = event.key.lower()
|
key = event.key.lower()
|
||||||
|
|
||||||
# Check for single-char mnemonic
|
# Only buffer alphabetic keys
|
||||||
if key in self._mnemonic_map:
|
if not key.isalpha() or len(key) != 1:
|
||||||
self._open_link(self._mnemonic_map[key])
|
return
|
||||||
|
|
||||||
|
# Cancel any pending timer
|
||||||
|
if self._key_timer:
|
||||||
|
self._key_timer.stop()
|
||||||
|
self._key_timer = None
|
||||||
|
|
||||||
|
# Add key to buffer
|
||||||
|
self._key_buffer += key
|
||||||
|
|
||||||
|
# Check for exact match with buffered keys
|
||||||
|
if self._key_buffer in self._mnemonic_map:
|
||||||
|
# If no multi-char mnemonics exist, open immediately
|
||||||
|
if not self._has_multi_char:
|
||||||
|
self._open_link(self._mnemonic_map[self._key_buffer])
|
||||||
|
self._key_buffer = ""
|
||||||
|
event.prevent_default()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if any longer mnemonic starts with our buffer
|
||||||
|
has_longer_match = any(
|
||||||
|
m.startswith(self._key_buffer) and len(m) > len(self._key_buffer)
|
||||||
|
for m in self._mnemonic_map.keys()
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_longer_match:
|
||||||
|
# Wait for possible additional keys
|
||||||
|
self._key_timer = self.set_timer(0.4, self._flush_key_buffer)
|
||||||
|
else:
|
||||||
|
# No longer matches possible, open immediately
|
||||||
|
self._open_link(self._mnemonic_map[self._key_buffer])
|
||||||
|
self._key_buffer = ""
|
||||||
|
|
||||||
event.prevent_default()
|
event.prevent_default()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check for two-char mnemonics (accumulate?)
|
# Check if buffer could still match something
|
||||||
# For simplicity, we'll just support single-char for now
|
could_match = any(
|
||||||
# A more sophisticated approach would use a timeout buffer
|
m.startswith(self._key_buffer) for m in self._mnemonic_map.keys()
|
||||||
|
)
|
||||||
|
|
||||||
|
if could_match:
|
||||||
|
# Wait for more keys
|
||||||
|
self._key_timer = self.set_timer(0.4, self._flush_key_buffer)
|
||||||
|
event.prevent_default()
|
||||||
|
else:
|
||||||
|
# No possible match, clear buffer
|
||||||
|
self._key_buffer = ""
|
||||||
|
|
||||||
|
def _flush_key_buffer(self) -> None:
|
||||||
|
"""Called after timeout to process buffered keys."""
|
||||||
|
self._key_timer = None
|
||||||
|
|
||||||
|
if self._key_buffer and self._key_buffer in self._mnemonic_map:
|
||||||
|
self._open_link(self._mnemonic_map[self._key_buffer])
|
||||||
|
|
||||||
|
self._key_buffer = ""
|
||||||
|
|
||||||
def action_open_selected(self) -> None:
|
def action_open_selected(self) -> None:
|
||||||
"""Open the currently selected link."""
|
"""Open the currently selected link."""
|
||||||
|
|||||||
@@ -216,6 +216,39 @@ async def get_message_content(message_id: int) -> Tuple[Optional[str], bool]:
|
|||||||
return None, False
|
return None, False
|
||||||
|
|
||||||
|
|
||||||
|
async def mark_as_read(message_id: int) -> Tuple[Optional[str], bool]:
|
||||||
|
"""
|
||||||
|
Mark a message as read by adding the 'seen' flag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message_id: The ID of the message to mark as read
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple containing:
|
||||||
|
- Result message or error
|
||||||
|
- Success status (True if operation was successful)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cmd = f"himalaya flag add seen {message_id}"
|
||||||
|
|
||||||
|
process = await asyncio.create_subprocess_shell(
|
||||||
|
cmd,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
stdout, stderr = await process.communicate()
|
||||||
|
|
||||||
|
if process.returncode == 0:
|
||||||
|
return stdout.decode().strip() or "Marked as read", True
|
||||||
|
else:
|
||||||
|
error_msg = stderr.decode().strip()
|
||||||
|
logging.error(f"Error marking message as read: {error_msg}")
|
||||||
|
return error_msg or "Unknown error", False
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Exception during marking message as read: {e}")
|
||||||
|
return str(e), False
|
||||||
|
|
||||||
|
|
||||||
def sync_himalaya():
|
def sync_himalaya():
|
||||||
"""This command does not exist. Halucinated by AI."""
|
"""This command does not exist. Halucinated by AI."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user