basic email tui working
This commit is contained in:
15
email_viewer.tcss
Normal file
15
email_viewer.tcss
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* Basic stylesheet for the Textual Email Viewer App */
|
||||||
|
|
||||||
|
Label#task_prompt {
|
||||||
|
padding: 1;
|
||||||
|
color: rgb(128,128,128);
|
||||||
|
}
|
||||||
|
|
||||||
|
Label#task_prompt_label {
|
||||||
|
padding: 1;
|
||||||
|
color: rgb(255, 216, 102);
|
||||||
|
}
|
||||||
|
|
||||||
|
Label#message_label {
|
||||||
|
padding: 1;
|
||||||
|
}
|
||||||
@@ -31,7 +31,6 @@ def save_sync_timestamp():
|
|||||||
with open(sync_timestamp_file, 'w') as f:
|
with open(sync_timestamp_file, 'w') as f:
|
||||||
json.dump({'last_sync': time.time()}, f)
|
json.dump({'last_sync': time.time()}, f)
|
||||||
|
|
||||||
# Function to synchronize maildir with the server
|
|
||||||
def synchronize_maildir(maildir_path, headers):
|
def synchronize_maildir(maildir_path, headers):
|
||||||
last_sync = load_last_sync_timestamp()
|
last_sync = load_last_sync_timestamp()
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
@@ -67,6 +66,37 @@ def synchronize_maildir(maildir_path, headers):
|
|||||||
if response.status_code != 204: # 204 No Content indicates success
|
if response.status_code != 204: # 204 No Content indicates success
|
||||||
print(f"Failed to move message to trash: {message_id}, {response.status_code}, {response.text}")
|
print(f"Failed to move message to trash: {message_id}, {response.status_code}, {response.text}")
|
||||||
|
|
||||||
|
# Find messages moved to ".Archives/**/*" and move them to the "Archive" folder on the server
|
||||||
|
archive_dir = os.path.join(maildir_path, '.Archives')
|
||||||
|
archive_files = glob.glob(os.path.join(archive_dir, '**', '*.eml'), recursive=True)
|
||||||
|
|
||||||
|
# Fetch the list of folders to find the "Archive" folder ID
|
||||||
|
print("Fetching server folders to locate 'Archive' folder...")
|
||||||
|
folder_response = requests.get('https://graph.microsoft.com/v1.0/me/mailFolders', headers=headers)
|
||||||
|
if folder_response.status_code != 200:
|
||||||
|
raise Exception(f"Failed to fetch mail folders: {folder_response.status_code}, {folder_response.text}")
|
||||||
|
|
||||||
|
folders = folder_response.json().get('value', [])
|
||||||
|
archive_folder_id = None
|
||||||
|
for folder in folders:
|
||||||
|
if folder.get('displayName', '').lower() == 'archive':
|
||||||
|
archive_folder_id = folder.get('id')
|
||||||
|
break
|
||||||
|
|
||||||
|
if not archive_folder_id:
|
||||||
|
raise Exception("No folder named 'Archive' found on the server.")
|
||||||
|
|
||||||
|
for filepath in archive_files:
|
||||||
|
message_id = os.path.basename(filepath).split('.')[0] # Extract the Message-ID from the filename
|
||||||
|
print(f"Moving message to 'Archive' folder: {message_id}")
|
||||||
|
response = requests.post(
|
||||||
|
f'https://graph.microsoft.com/v1.0/me/messages/{message_id}/move',
|
||||||
|
headers=headers,
|
||||||
|
json={'destinationId': archive_folder_id}
|
||||||
|
)
|
||||||
|
if response.status_code != 201: # 201 Created indicates success
|
||||||
|
print(f"Failed to move message to 'Archive': {message_id}, {response.status_code}, {response.text}")
|
||||||
|
|
||||||
# Save the current sync timestamp
|
# Save the current sync timestamp
|
||||||
save_sync_timestamp()
|
save_sync_timestamp()
|
||||||
|
|
||||||
@@ -115,7 +145,7 @@ def save_email_to_maildir(maildir_path, email_data, attachments_dir):
|
|||||||
if email_data.get('body', {}).get('contentType', '').lower() == 'html':
|
if email_data.get('body', {}).get('contentType', '').lower() == 'html':
|
||||||
markdown_converter = html2text.HTML2Text()
|
markdown_converter = html2text.HTML2Text()
|
||||||
markdown_converter.ignore_images = True
|
markdown_converter.ignore_images = True
|
||||||
markdown_converter.ignore_links = False
|
markdown_converter.ignore_links = True
|
||||||
body_markdown = markdown_converter.handle(body_html)
|
body_markdown = markdown_converter.handle(body_html)
|
||||||
else:
|
else:
|
||||||
body_markdown = body_html
|
body_markdown = body_html
|
||||||
|
|||||||
140
run_himalaya.sh
Executable file
140
run_himalaya.sh
Executable file
@@ -0,0 +1,140 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check if an argument is provided
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Usage: $0 <message_number>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to refresh the vim-himalaya buffer
|
||||||
|
refresh_vim_himalaya() {
|
||||||
|
nvim --server /tmp/nvim-server --remote-send "Himalaya<CR>"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to read a single character without waiting for Enter
|
||||||
|
read_char() {
|
||||||
|
stty -echo -icanon time 0 min 1
|
||||||
|
char=$(dd bs=1 count=1 2>/dev/null)
|
||||||
|
stty echo icanon
|
||||||
|
echo "$char"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to safely run the himalaya command and handle failures
|
||||||
|
run_himalaya_message_read() {
|
||||||
|
himalaya message read "$1" | glow -p
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to open message $1."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to prompt the user for an action
|
||||||
|
prompt_action() {
|
||||||
|
echo "What would you like to do?"
|
||||||
|
|
||||||
|
# Step 1: Ask if the user wants to create a task
|
||||||
|
echo -n "Would you like to create a task for this message? (y/n): "
|
||||||
|
create_task=$(read_char)
|
||||||
|
echo "$create_task" # Echo the character for feedback
|
||||||
|
if [[ "$create_task" == "y" || "$create_task" == "Y" ]]; then
|
||||||
|
read -p "Task title: " task_title
|
||||||
|
task add "Followup on email $1 - $task_title" --project "Email" --due "$(date -d '+1 week' +%Y-%m-%d)" --priority "P3" --tags "email"
|
||||||
|
echo "Task created for message $1."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 2: Ask if the user wants to delete or archive the message
|
||||||
|
echo "d) Delete the message"
|
||||||
|
echo "a) Move the message to the archive folder"
|
||||||
|
echo "x) Skip delete/archive step"
|
||||||
|
echo -n "Enter your choice (d/a/x): "
|
||||||
|
archive_or_delete=$(read_char)
|
||||||
|
echo "$archive_or_delete" # Echo the character for feedback
|
||||||
|
|
||||||
|
case $archive_or_delete in
|
||||||
|
d)
|
||||||
|
echo "Deleting message $1..."
|
||||||
|
himalaya message delete "$1"
|
||||||
|
refresh_vim_himalaya
|
||||||
|
;;
|
||||||
|
a)
|
||||||
|
echo "Archiving message $1..."
|
||||||
|
himalaya message move Archives "$1"
|
||||||
|
refresh_vim_himalaya
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid choice. Skipping delete/archive step."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Step 3: Ask if the user wants to open the next message or exit
|
||||||
|
echo -e "\n"
|
||||||
|
echo "n) Open the next message"
|
||||||
|
echo "p) Open the previous message"
|
||||||
|
echo "x) Exit"
|
||||||
|
echo -n "Enter your choice (o/x): "
|
||||||
|
next_or_exit=$(read_char)
|
||||||
|
echo "$next_or_exit" # Echo the character for feedback
|
||||||
|
|
||||||
|
case $next_or_exit in
|
||||||
|
n)
|
||||||
|
# Try opening the next message, retrying up to 5 times if necessary
|
||||||
|
attempts=0
|
||||||
|
success=false
|
||||||
|
while [ $attempts -lt 5 ]; do
|
||||||
|
next_id=$(( $1 + attempts + 1 ))
|
||||||
|
echo "Attempting to open next message: $next_id"
|
||||||
|
if run_himalaya_message_read "$next_id"; then
|
||||||
|
success=true
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "Failed to open message $next_id. Retrying..."
|
||||||
|
attempts=$((attempts + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$success" = false ]; then
|
||||||
|
echo "Unable to open any messages after 5 attempts. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
p)
|
||||||
|
# Try opening the previous message, retrying up to 5 times if necessary
|
||||||
|
attempts=0
|
||||||
|
success=false
|
||||||
|
while [ $attempts -lt 5 ]; do
|
||||||
|
prev_id=$(( $1 - attempts - 1 ))
|
||||||
|
echo "Attempting to open previous message: $prev_id"
|
||||||
|
if $0 $prev_id; then
|
||||||
|
success=true
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "Failed to open message $prev_id. Retrying..."
|
||||||
|
attempts=$((attempts + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$success" = false ]; then
|
||||||
|
echo "Unable to open any messages after 5 attempts. Exiting."
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
x)
|
||||||
|
echo "Exiting."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid choice. Exiting."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the himalaya command with the provided message number
|
||||||
|
run_himalaya_message_read "$1"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error reading message $1. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prompt the user for the next action
|
||||||
|
prompt_action "$1"
|
||||||
18
tui.py
Normal file
18
tui.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import Header, Footer, Static, Label
|
||||||
|
|
||||||
|
class MSALApp(App):
|
||||||
|
"""A Textual app for MSAL authentication."""
|
||||||
|
|
||||||
|
CSS_PATH = "msal_app.tcss" # Optional: For styling
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Create child widgets for the app."""
|
||||||
|
yield Header(show_clock=True)
|
||||||
|
yield Footer()
|
||||||
|
yield Static(Label("MSAL Authentication App"), id="main_content")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = MSALApp()
|
||||||
|
app.run()
|
||||||
303
tui_email_viewer.py
Normal file
303
tui_email_viewer.py
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Iterable
|
||||||
|
from textual import on
|
||||||
|
from textual.app import App, ComposeResult, SystemCommand
|
||||||
|
from textual.logging import TextualHandler
|
||||||
|
from textual.screen import Screen
|
||||||
|
from textual.widgets import Header, Footer, Static, Label, Input, Button
|
||||||
|
from textual.reactive import Reactive
|
||||||
|
from textual.binding import Binding
|
||||||
|
from textual.timer import Timer
|
||||||
|
from textual.containers import ScrollableContainer, Horizontal, Vertical, Grid
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level="NOTSET",
|
||||||
|
handlers=[TextualHandler()],
|
||||||
|
)
|
||||||
|
|
||||||
|
class OpenMessageScreen(Screen[int]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Horizontal(
|
||||||
|
Label("📨", id="message_label"),
|
||||||
|
Input(placeholder="Enter message ID (integer only)", type="integer", id="open_message_input"),
|
||||||
|
Button("Open", variant="primary", id="open_message_button")
|
||||||
|
)
|
||||||
|
|
||||||
|
@on(Input.Submitted)
|
||||||
|
def handle_message_id(self) -> None:
|
||||||
|
logging.info("Open message")
|
||||||
|
input_widget = self.query_one("#open_message_input", Input)
|
||||||
|
self.disabled = True
|
||||||
|
self.loading = True
|
||||||
|
message_id = int(input_widget.value)
|
||||||
|
self.dismiss(message_id)
|
||||||
|
|
||||||
|
@on(Input._on_key)
|
||||||
|
def handle_close(self, event) -> None:
|
||||||
|
if (event.key == "escape" or event.key == "ctrl+c"):
|
||||||
|
self.dismiss()
|
||||||
|
|
||||||
|
|
||||||
|
class CreateTaskScreen(Screen[str]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Vertical(
|
||||||
|
Label("$>", id="task_prompt"),
|
||||||
|
Label("task ", id="task_prompt_label"),
|
||||||
|
Input(placeholder="arguments", id="task_input")
|
||||||
|
)
|
||||||
|
|
||||||
|
@on(Input.Submitted)
|
||||||
|
def handle_task_args(self) -> None:
|
||||||
|
input_widget = self.query_one("#task_input", Input)
|
||||||
|
self.disabled = True
|
||||||
|
self.loading = True
|
||||||
|
task_args = input_widget.value
|
||||||
|
self.dismiss(task_args)
|
||||||
|
|
||||||
|
@on(Input._on_key)
|
||||||
|
def handle_close(self, event) -> None:
|
||||||
|
if (event.key == "escape" or event.key == "ctrl+c"):
|
||||||
|
self.dismiss()
|
||||||
|
|
||||||
|
|
||||||
|
class EmailViewerApp(App):
|
||||||
|
"""A Textual app for viewing and managing emails."""
|
||||||
|
title = "Mail Reader"
|
||||||
|
CSS_PATH = "email_viewer.tcss" # Optional: For styling
|
||||||
|
|
||||||
|
current_message_id: Reactive[int] = Reactive(1)
|
||||||
|
|
||||||
|
def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
|
||||||
|
yield from super().get_system_commands(screen)
|
||||||
|
yield SystemCommand("Next Message", "Navigate to Next ID", self.action_next)
|
||||||
|
yield SystemCommand("Previous Message", "Navigate to Previous ID", self.action_previous)
|
||||||
|
yield SystemCommand("Delete Message", "Delete the current message", self.action_delete)
|
||||||
|
yield SystemCommand("Archive Message", "Archive the current message", self.action_archive)
|
||||||
|
yield SystemCommand("Open Message", "Open a specific message by ID", self.action_open)
|
||||||
|
yield SystemCommand("Create Task", "Create a task using the task CLI", self.action_create_task)
|
||||||
|
|
||||||
|
BINDINGS = [
|
||||||
|
Binding("n", "next", "Next message"),
|
||||||
|
Binding("p", "previous", "Previous message"),
|
||||||
|
Binding("d", "delete", "Delete message"),
|
||||||
|
Binding("a", "archive", "Archive message"),
|
||||||
|
Binding("o", "open", "Open message", show=False),
|
||||||
|
Binding("q", "quit", "Quit application"),
|
||||||
|
Binding("c", "create_task", "Create Task")
|
||||||
|
]
|
||||||
|
|
||||||
|
BINDINGS.extend([
|
||||||
|
Binding("j", "scroll_down", "Scroll down"),
|
||||||
|
Binding("k", "scroll_up", "Scroll up"),
|
||||||
|
Binding("down", "scroll_down", "Scroll down"),
|
||||||
|
Binding("up", "scroll_up", "Scroll up"),
|
||||||
|
Binding("space", "scroll_page_down", "Scroll page down"),
|
||||||
|
Binding("b", "scroll_page_up", "Scroll page up")
|
||||||
|
])
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Create child widgets for the app."""
|
||||||
|
yield Header(show_clock=True)
|
||||||
|
yield Footer(Label("[n] Next | [p] Previous | [d] Delete | [a] Archive | [o] Open | [q] Quit | [c] Create Task"))
|
||||||
|
yield ScrollableContainer(Static(Label("Email Viewer App"), id="main_content"))
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
"""Called when the app is mounted."""
|
||||||
|
self.alert_timer: Timer | None = None # Timer to throttle alerts
|
||||||
|
# Fetch the ID of the most recent message using the Himalaya CLI
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["himalaya", "envelope", "list", "-o", "json"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
import json
|
||||||
|
envelopes = json.loads(result.stdout)
|
||||||
|
if envelopes:
|
||||||
|
self.current_message_id = int(envelopes[0]['id']) # Get the first envelope's ID
|
||||||
|
else:
|
||||||
|
self.query_one("#main_content", Static).update("Failed to fetch the most recent message ID.")
|
||||||
|
except Exception as e:
|
||||||
|
self.query_one("#main_content", Static).update(f"Error: {e}")
|
||||||
|
|
||||||
|
self.show_message(self.current_message_id)
|
||||||
|
|
||||||
|
def show_message(self, message_id: int) -> None:
|
||||||
|
self.query_one("#main_content", Static).loading = True
|
||||||
|
"""Fetch and display the email message by ID."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["himalaya", "message", "read", str(message_id)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
# Render the email content as Markdown
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
markdown_content = Markdown(result.stdout, justify=True )
|
||||||
|
self.query_one("#main_content", Static).loading = False
|
||||||
|
self.query_one("#main_content", Static).update(markdown_content)
|
||||||
|
else:
|
||||||
|
self.query_one("#main_content", Static).update(f"Failed to fetch message {message_id}.")
|
||||||
|
except Exception as e:
|
||||||
|
self.query_one("#main_content", Static).update(f"Error: {e}")
|
||||||
|
|
||||||
|
def show_status(self, message: str, severity: str = "information") -> None:
|
||||||
|
"""Display a status message using the built-in notify function."""
|
||||||
|
self.notify(message, title="Status", severity=severity, timeout=1, markup=True)
|
||||||
|
|
||||||
|
def action_next(self) -> None:
|
||||||
|
"""Show the next email message, iterating until a valid one is found or giving up after 100 attempts."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["nvim", "--server", " /tmp/nvim-server", " --remote-send", "':Himalaya<CR>'"],
|
||||||
|
capture_output=False,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Error running nvim himalaya refresh command. Maybe the nvim server isn't started? {e}")
|
||||||
|
return
|
||||||
|
self.query_one("#main_content", Static).loading = True
|
||||||
|
attempts = 0
|
||||||
|
while attempts < 100:
|
||||||
|
self.current_message_id += 1
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["himalaya", "message", "read", str(self.current_message_id)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
self.query_one("#main_content", Static).loading = False
|
||||||
|
self.show_message(self.current_message_id)
|
||||||
|
self.show_status(f"Showing next message: {self.current_message_id}")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
attempts += 1
|
||||||
|
except Exception as e:
|
||||||
|
self.query_one("#main_content", Static).update(f"Error: {e}")
|
||||||
|
return
|
||||||
|
self.query_one("#main_content", Static).update("No more messages found after 100 attempts.")
|
||||||
|
|
||||||
|
def action_previous(self) -> None:
|
||||||
|
"""Show the previous email message, iterating until a valid one is found or giving up after 100 attempts."""
|
||||||
|
self.show_status("Loading previous message...")
|
||||||
|
attempts = 0
|
||||||
|
while attempts < 100 and self.current_message_id > 1:
|
||||||
|
self.current_message_id -= 1
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["himalaya", "message", "read", str(self.current_message_id)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
self.show_message(self.current_message_id)
|
||||||
|
self.show_status(f"Showing previous message: {self.current_message_id}")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
attempts += 1
|
||||||
|
except Exception as e:
|
||||||
|
self.query_one("#main_content", Static).update(f"Error: {e}")
|
||||||
|
return
|
||||||
|
self.query_one("#main_content", Static).update("No more messages found after 100 attempts.")
|
||||||
|
|
||||||
|
def action_delete(self) -> None:
|
||||||
|
"""Delete the current email message."""
|
||||||
|
self.show_status(f"Deleting message {self.current_message_id}...")
|
||||||
|
self.query_one("#main_content", Static).loading = True
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["himalaya", "message", "delete", str(self.current_message_id)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
self.query_one("#main_content", Static).loading = False
|
||||||
|
self.query_one("#main_content", Static).update(f"Message {self.current_message_id} deleted.")
|
||||||
|
self.show_status(f"Message {self.current_message_id} deleted.")
|
||||||
|
self.action_next() # Automatically show the next message
|
||||||
|
else:
|
||||||
|
self.query_one("#main_content", Static).update(f"Failed to delete message {self.current_message_id}.")
|
||||||
|
except Exception as e:
|
||||||
|
self.query_one("#main_content", Static).update(f"Error: {e}")
|
||||||
|
|
||||||
|
def action_archive(self) -> None:
|
||||||
|
"""Archive the current email message."""
|
||||||
|
self.show_status(f"Archiving message {self.current_message_id}...")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["himalaya", "message", "move", "Archives", str(self.current_message_id)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
self.query_one("#main_content", Static).update(f"Message {self.current_message_id} archived.")
|
||||||
|
self.show_status(f"Message {self.current_message_id} archived.")
|
||||||
|
self.action_next() # Automatically show the next message
|
||||||
|
else:
|
||||||
|
self.query_one("#main_content", Static).update(f"Failed to archive message {self.current_message_id}.")
|
||||||
|
except Exception as e:
|
||||||
|
self.query_one("#main_content", Static).update(f"Error: {e}")
|
||||||
|
|
||||||
|
def action_open(self) -> None:
|
||||||
|
"""Show the input modal for opening a specific message by ID."""
|
||||||
|
def check_id(message_id: str) -> bool:
|
||||||
|
try:
|
||||||
|
int(message_id)
|
||||||
|
self.app.show_status(f"Opening message {message_id}...")
|
||||||
|
self.app.current_message_id = message_id
|
||||||
|
self.app.show_message(self.app.current_message_id)
|
||||||
|
except ValueError:
|
||||||
|
self.app.show_status("Invalid message ID. Please enter an integer.", severity="error")
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
self.push_screen(OpenMessageScreen(), check_id)
|
||||||
|
|
||||||
|
|
||||||
|
def action_create_task(self) -> None:
|
||||||
|
"""Show the input modal for creating a task."""
|
||||||
|
def check_task(task_args: str) -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["task"] + task_args.split(),
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
self.show_status("Task created successfully.")
|
||||||
|
else:
|
||||||
|
self.show_status(f"Failed to create task: {result.stderr}")
|
||||||
|
except Exception as e:
|
||||||
|
self.show_status(f"Error: {e}", severity="error")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
self.push_screen(CreateTaskScreen(), check_task)
|
||||||
|
|
||||||
|
|
||||||
|
def action_scroll_down(self) -> None:
|
||||||
|
"""Scroll the main content down."""
|
||||||
|
self.query_one("#main_content", Static).scroll_down()
|
||||||
|
|
||||||
|
def action_scroll_up(self) -> None:
|
||||||
|
"""Scroll the main content up."""
|
||||||
|
self.query_one("#main_content", Static).scroll_up()
|
||||||
|
|
||||||
|
def action_scroll_page_down(self) -> None:
|
||||||
|
"""Scroll the main content down by a page."""
|
||||||
|
self.query_one("#main_content", Static).scroll_page_down()
|
||||||
|
|
||||||
|
def action_scroll_page_up(self) -> None:
|
||||||
|
"""Scroll the main content up by a page."""
|
||||||
|
self.query_one("#main_content", Static).scroll_page_up()
|
||||||
|
|
||||||
|
def action_quit(self) -> None:
|
||||||
|
"""Quit the application."""
|
||||||
|
self.exit()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = EmailViewerApp()
|
||||||
|
app.run()
|
||||||
Reference in New Issue
Block a user