use crate::config::Config;
use crate::messages::{MessageDb, MessageType};
use anyhow::Result;
use dialoguer::{theme::ColorfulTheme, Select};
use std::io::Read;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Key {
Up,
Down,
Enter,
Esc,
Char(char),
}
fn enable_raw_mode_cmd() -> Result<()> {
std::process::Command::new("stty")
.arg("-icanon")
.arg("-echo")
.arg("min")
.arg("1")
.arg("time")
.arg("0")
.status()?;
Ok(())
}
fn disable_raw_mode_cmd() -> Result<()> {
std::process::Command::new("stty").arg("sane").status()?;
Ok(())
}
fn read_key() -> Result<Key> {
let mut stdin = io::stdin();
let mut buf = [0u8; 1];
stdin.read_exact(&mut buf)?;
match buf[0] {
b'\n' | b'\r' => Ok(Key::Enter),
0x1B => {
let mut seq = [0u8; 2];
if stdin.read_exact(&mut seq).is_ok() && seq[0] == b'[' {
return match seq[1] {
b'A' => Ok(Key::Up),
b'B' => Ok(Key::Down),
_ => Ok(Key::Esc),
};
}
Ok(Key::Esc)
}
b => Ok(Key::Char(b as char)),
}
}
use std::io::{self, Write};
pub struct ListFilters {
pub sent: bool,
pub all: bool,
pub unread: bool,
pub projects: bool,
pub message_type: Option<String>,
pub from: Option<String>,
pub search: Option<String>,
}
pub fn list(config: &Config, filters: ListFilters) -> Result<()> {
let db_path = super::messages::get_message_db_path(config)?;
let db = MessageDb::new(&db_path)?;
let messages = if filters.all {
db.list_messages(Some(100))?
} else if filters.sent {
db.list_sent_messages(Some(100))?
} else if filters.unread {
db.list_unread_messages()?
} else if filters.projects {
db.list_messages_by_type("project", Some(100))?
} else if let Some(ref search_term) = filters.search {
db.search_messages(search_term, Some(100))?
} else if let Some(ref msg_type) = filters.message_type {
db.list_messages_by_type(msg_type, Some(100))?
} else {
db.list_inbox_messages(Some(100))?
};
let messages = if let Some(ref sender) = filters.from {
messages
.into_iter()
.filter(|m| m.from.contains(sender))
.collect()
} else {
messages
};
if messages.is_empty() {
println!("No messages found");
return Ok(());
}
println!("\nđŦ Messages ({} total):", messages.len());
println!("âââââââââââââââââââââââââââââââââââââââ");
for (i, msg) in messages.iter().enumerate() {
let status_icon = match msg.status {
crate::messages::MessageStatus::Draft => "đ",
crate::messages::MessageStatus::Sent => "đ¤",
crate::messages::MessageStatus::Received => "đĨ",
crate::messages::MessageStatus::Read => "đī¸",
crate::messages::MessageStatus::Deleted => "đī¸",
crate::messages::MessageStatus::Archived => "đ",
};
let type_icon = match &msg.message_type {
MessageType::Text => "âī¸",
MessageType::Project { .. } => "đĻ",
MessageType::Request { .. } => "đ",
};
println!(
"\n{}. {} {} [{}]",
i + 1,
status_icon,
type_icon,
&msg.id[..8]
);
println!(" From: {}", msg.from);
println!(" To: {}", msg.to);
if let Some(ref subject) = msg.subject {
if !subject.is_empty() {
println!(" Subject: {}", subject);
}
}
let local_time = msg.created_at.with_timezone(&chrono::Local);
println!(" Date: {}", local_time.format("%Y-%m-%d %H:%M"));
let preview_len = 80;
let preview = if msg.body.len() > preview_len {
format!("{}...", &msg.body[..preview_len])
} else {
msg.body.clone()
};
println!(" {}", preview);
}
println!("\n-------------------------------------");
println!("Tip: use 'bv inbox --plain' for non-interactive output");
Ok(())
}
pub fn interactive(config: &Config, initial_view: Option<String>) -> Result<()> {
let db_path = super::messages::get_message_db_path(config)?;
let db = MessageDb::new(&db_path)?;
let sync = super::messages::init_message_system(config)?.1;
let _ = sync.sync_quiet();
let mut current_view = initial_view.unwrap_or_else(|| "inbox".to_string());
enable_raw_mode_cmd()?;
let mut selected: usize = 0; loop {
let messages = match current_view.as_str() {
"inbox" => db.list_inbox_messages(Some(200))?,
"sent" => db.list_sent_messages(Some(200))?,
"all" => db.list_messages(Some(200))?,
"unread" => db.list_unread_messages()?,
"projects" => db.list_messages_by_type("project", Some(200))?,
_ => db.list_inbox_messages(Some(200))?,
};
if selected > messages.len() {
selected = messages.len();
}
print!("\x1B[2J\x1B[1;1H");
io::stdout().flush()?;
println!("======================================================");
println!(
"BioVault Inbox - {} ({} messages)",
current_view.to_uppercase(),
messages.len()
);
println!("======================================================");
println!("(Press '?' for shortcuts)");
println!("------------------------------------------------------");
if messages.is_empty() {
println!("(No messages in this view)");
} else {
for (i, msg) in messages.iter().enumerate() {
let status = match msg.status {
crate::messages::MessageStatus::Draft => "DRAFT",
crate::messages::MessageStatus::Sent => "SENT",
crate::messages::MessageStatus::Received => "RECV",
crate::messages::MessageStatus::Read => "READ",
crate::messages::MessageStatus::Deleted => "DEL",
crate::messages::MessageStatus::Archived => "ARCH",
};
let who = if current_view == "sent" {
&msg.to
} else {
&msg.from
};
let subject = msg.subject.as_deref().unwrap_or("(No Subject)");
let mut line = format!(
"{} {status} {} - {}",
if i == selected { ">" } else { " " },
who,
subject
);
if line.len() > 80 {
line.truncate(80);
}
println!("{}", line);
}
}
println!(
"{} Quit",
if selected == messages.len() { ">" } else { " " }
);
match read_key()? {
Key::Char('q') | Key::Esc => {
disable_raw_mode_cmd()?;
break;
}
Key::Char('?') | Key::Char('h') | Key::Char('H') => {
print!("\x1B[2J\x1B[1;1H");
println!("Shortcuts:\n ? / h : Help\n n : New Message\n s : Sync Messages\n v : Change View (menu)\n q / Esc: Quit\n 1..5 : Tabs (Inbox, Sent, All, Unread, Projects)\n\nArrows to move, Enter to open.");
println!("\nPress any key to return...");
let _ = read_key();
}
Key::Char('n') | Key::Char('N') => {
disable_raw_mode_cmd()?;
compose_new_message(config)?;
enable_raw_mode_cmd()?;
}
Key::Char('s') | Key::Char('S') => {
disable_raw_mode_cmd()?;
println!("\nSyncing messages...");
let _ = sync.sync();
println!("Sync complete. Press Enter...");
let mut t = String::new();
io::stdin().read_line(&mut t).ok();
enable_raw_mode_cmd()?;
}
Key::Char('v') | Key::Char('V') => {
disable_raw_mode_cmd()?;
let views = ["inbox", "sent", "all", "unread", "projects"];
let view_names = ["Inbox", "Sent", "All Messages", "Unread", "Projects"];
println!("\nSelect view:");
let view_selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Choose a view")
.default(0)
.items(&view_names)
.interact_opt()?;
if let Some(sel) = view_selection {
current_view = views[sel].to_string();
selected = 0;
}
enable_raw_mode_cmd()?;
}
Key::Char('1') => {
current_view = "inbox".to_string();
selected = 0;
}
Key::Char('2') => {
current_view = "sent".to_string();
selected = 0;
}
Key::Char('3') => {
current_view = "all".to_string();
selected = 0;
}
Key::Char('4') => {
current_view = "unread".to_string();
selected = 0;
}
Key::Char('5') => {
current_view = "projects".to_string();
selected = 0;
}
Key::Up => {
selected = selected.saturating_sub(1);
}
Key::Down => {
if selected < messages.len() {
selected += 1;
}
}
Key::Enter => {
if selected == messages.len() {
disable_raw_mode_cmd()?;
break;
}
if !messages.is_empty() {
let msg = &messages[selected];
disable_raw_mode_cmd()?;
let _ = message_actions(config, &db, msg)?;
enable_raw_mode_cmd()?;
}
}
Key::Char(_) => {}
}
}
Ok(())
}
fn message_actions(
config: &Config,
db: &MessageDb,
msg: &crate::messages::Message,
) -> Result<bool> {
let actions = vec![
"Read",
"Reply",
"Delete",
"Mark as Read/Unread",
"Back to Messages",
];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select action")
.default(0)
.items(&actions)
.interact_opt()?;
match selection {
Some(0) => {
super::messages::read_message(config, &msg.id)?;
println!("\nPress Enter to continue...");
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(false) }
Some(1) => {
println!("\nEnter your reply (press Enter when done):");
print!("> ");
io::stdout().flush()?;
let mut reply_body = String::new();
io::stdin().read_line(&mut reply_body)?;
if !reply_body.trim().is_empty() {
super::messages::reply_message(config, &msg.id, reply_body.trim())?;
println!("â
Reply sent!");
} else {
println!("Reply cancelled (empty message)");
}
println!("Press Enter to continue...");
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(false)
}
Some(2) => {
super::messages::delete_message(config, &msg.id)?;
println!("đī¸ Message deleted. Press Enter to continue...");
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(false)
}
Some(3) => {
if msg.status == crate::messages::MessageStatus::Received {
db.mark_as_read(&msg.id)?;
println!("â
Marked as read");
} else if msg.status == crate::messages::MessageStatus::Read {
println!("âšī¸ Message is already read (mark as unread not implemented yet)");
} else {
println!("âšī¸ Cannot change read status for this message type");
}
println!("Press Enter to continue...");
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(false)
}
Some(4) | None => {
Ok(false)
}
_ => Ok(false),
}
}
fn compose_new_message(config: &Config) -> Result<()> {
println!("\nCompose New Message");
println!("--------------------");
print!("Recipient email: ");
io::stdout().flush()?;
let mut recipient = String::new();
io::stdin().read_line(&mut recipient)?;
let recipient = recipient.trim().to_string();
if recipient.is_empty() {
println!("Cancelled (no recipient)");
println!("Press Enter to continue...");
let mut input = String::new();
io::stdin().read_line(&mut input)?;
return Ok(());
}
print!("Subject (optional): ");
io::stdout().flush()?;
let mut subject = String::new();
io::stdin().read_line(&mut subject)?;
let subject = subject.trim().to_string();
let subject_opt = if subject.is_empty() {
None
} else {
Some(subject.as_str())
};
println!("Body (single line, press Enter to finish):");
print!("> ");
io::stdout().flush()?;
let mut body = String::new();
io::stdin().read_line(&mut body)?;
let body = body.trim();
if body.is_empty() {
println!("Cancelled (empty body)");
println!("Press Enter to continue...");
let mut input = String::new();
io::stdin().read_line(&mut input)?;
return Ok(());
}
super::messages::send_message(config, &recipient, body, subject_opt)?;
println!("\nMessage sent. Press Enter to continue...");
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(())
}