diff --git a/src/inbox.rs b/src/inbox.rs index a80df32..0bf1e07 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -229,16 +229,39 @@ fn clean_text(text: &str) -> String { /// - Strip image references ![...](...) /// - Simplify links [text](url) → text /// - Remove bare long URLs +/// - Remove pipe characters from HTML table remnants /// - Collapse runs of 3+ blank lines to 2 fn clean_markdown(text: &str) -> String { let re_img = regex::Regex::new(r"!\[[^\]]*\]\([^)]*\)").unwrap(); let re_link = regex::Regex::new(r"\[([^\]]*)\]\([^)]*\)").unwrap(); let re_bare_url = regex::Regex::new(r"https?://\S{80,}").unwrap(); let re_blank_lines = regex::Regex::new(r"\n{3,}").unwrap(); + // Two or more pipes possibly separated by whitespace (table borders / empty cells) + let re_multi_pipes = regex::Regex::new(r"\|[\s|]*\|").unwrap(); + // Single remaining pipe with surrounding whitespace + let re_pipe = regex::Regex::new(r"\s*\|\s*").unwrap(); + let re_multi_space = regex::Regex::new(r"[ \t]{2,}").unwrap(); let result = re_img.replace_all(text, ""); let result = re_link.replace_all(&result, "$1"); let result = re_bare_url.replace_all(&result, ""); + + // Process line by line to strip HTML table pipe remnants + let result: String = result + .lines() + .map(|line| { + // Drop lines that are entirely pipes and whitespace (table dividers) + if line.chars().all(|c| c == '|' || c.is_whitespace()) { + return String::new(); + } + let line = re_multi_pipes.replace_all(line, " "); + let line = re_pipe.replace_all(&line, " "); + let line = re_multi_space.replace_all(&line, " "); + line.trim().to_string() + }) + .collect::>() + .join("\n"); + let result = re_blank_lines.replace_all(&result, "\n\n"); result.trim().to_string() } diff --git a/src/lib.rs b/src/lib.rs index 7dc72f7..bd939b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ use crossterm::event; use crossterm::event::{Event, KeyCode}; use ratatui::backend::CrosstermBackend; use ratatui::layout::{Constraint, Direction, Layout}; -use ratatui::prelude::{Color, Modifier, Style}; +use ratatui::prelude::{Color, Line, Modifier, Span, Style}; use ratatui::Terminal; use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; use crate::config::Config; @@ -21,7 +21,7 @@ const POLL_INTERVAL: Duration = Duration::from_secs(30); #[derive(PartialEq)] enum Focus { Inbox, - Preview, + Message, } pub(crate) struct Email { @@ -106,11 +106,11 @@ pub fn main(config: &Config, terminal: &mut Terminal>) let mut loading = true; let mut list_state = ListState::default(); let mut last_fetch = Instant::now(); - let mut preview_body: String = String::new(); - let mut preview_seq: Option = None; + let mut message_body: String = String::new(); + let mut message_seq: Option = None; let mut body_loading = false; let mut focus = Focus::Inbox; - let mut preview_scroll: u16 = 0; + let mut message_scroll: u16 = 0; // --- Main loop --- loop { @@ -132,18 +132,18 @@ pub fn main(config: &Config, terminal: &mut Terminal>) .unwrap_or(0); list_state.select(Some(new_idx)); let seq = emails[new_idx].seq; - if preview_seq != Some(seq) { - preview_seq = Some(seq); - preview_body.clear(); - preview_scroll = 0; + if message_seq != Some(seq) { + message_seq = Some(seq); + message_body.clear(); + message_scroll = 0; body_loading = true; wanted_body_seq.store(seq, Ordering::Relaxed); let _ = cmd_tx.send(WorkerCmd::FetchBody { seq }); } } else { list_state.select(None); - preview_seq = None; - preview_body.clear(); + message_seq = None; + message_body.clear(); } } WorkerResult::Refreshed(Err(e)) => { @@ -162,11 +162,11 @@ pub fn main(config: &Config, terminal: &mut Terminal>) } WorkerResult::Body { seq, result } => { body_loading = false; - if preview_seq == Some(seq) { - preview_scroll = 0; + if message_seq == Some(seq) { + message_scroll = 0; match result { - Ok(body) => preview_body = body, - Err(e) => preview_body = format!("Error loading body: {}", e), + Ok(body) => message_body = body, + Err(e) => message_body = format!("Error loading body: {}", e), } } } @@ -197,30 +197,36 @@ pub fn main(config: &Config, terminal: &mut Terminal>) .split(area); // --- Top: Inbox list --- - let inbox_border = if focus == Focus::Inbox { - Style::default().fg(Color::Cyan) + let (inbox_title_style, inbox_prefix) = if focus == Focus::Inbox { + (Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), "▶ ") } else { - Style::default().fg(Color::DarkGray) + (Style::default().fg(Color::DarkGray), " ") }; - let preview_border = if focus == Focus::Preview { - Style::default().fg(Color::Cyan) + let (message_title_style, message_prefix) = if focus == Focus::Message { + (Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), "▶ ") } else { - Style::default().fg(Color::DarkGray) + (Style::default().fg(Color::DarkGray), " ") + }; + let inbox_border = inbox_title_style; + let message_border = message_title_style; + + let inbox_title = |label: &str| { + Line::from(Span::styled(format!("{}{}", inbox_prefix, label), inbox_title_style)) }; if let Some(e) = &error { let p = Paragraph::new(format!("IMAP error: {}", e)) - .block(Block::default().title("Inbox").borders(Borders::ALL).border_style(inbox_border)) + .block(Block::default().title(inbox_title("Inbox")).borders(Borders::BOTTOM).border_style(inbox_border)) .style(Style::default().fg(Color::Red)); frame.render_widget(p, layout[0]); } else if emails.is_empty() && loading { let p = Paragraph::new("Loading...") - .block(Block::default().title("Inbox").borders(Borders::ALL).border_style(inbox_border)) + .block(Block::default().title(inbox_title("Inbox")).borders(Borders::BOTTOM).border_style(inbox_border)) .style(Style::default().fg(Color::Yellow)); frame.render_widget(p, layout[0]); } else if emails.is_empty() { let p = Paragraph::new("No messages in inbox.") - .block(Block::default().title("Inbox").borders(Borders::ALL).border_style(inbox_border)) + .block(Block::default().title(inbox_title("Inbox")).borders(Borders::BOTTOM).border_style(inbox_border)) .style(Style::default().fg(Color::Yellow)); frame.render_widget(p, layout[0]); } else { @@ -231,7 +237,7 @@ pub fn main(config: &Config, terminal: &mut Terminal>) }) .collect(); - let title = if loading { + let title_str = if loading { format!("Inbox ({} messages) [loading...]", emails.len()) } else { format!("Inbox ({} messages)", emails.len()) @@ -240,8 +246,8 @@ pub fn main(config: &Config, terminal: &mut Terminal>) let list = List::new(items) .block( Block::default() - .title(title) - .borders(Borders::ALL) + .title(inbox_title(&title_str)) + .borders(Borders::BOTTOM) .border_style(inbox_border), ) .style(Style::default().fg(Color::White)) @@ -255,17 +261,21 @@ pub fn main(config: &Config, terminal: &mut Terminal>) frame.render_stateful_widget(list, layout[0], &mut list_state); } - // --- Bottom: Email preview --- - let preview_text = if body_loading { + // --- Bottom: Message --- + let message_text = if body_loading { ratatui::text::Text::raw("Loading...") } else { - tui_markdown::from_str(&preview_body) + tui_markdown::from_str(&message_body) }; - let preview = Paragraph::new(preview_text) - .block(Block::default().title("Preview").borders(Borders::ALL).border_style(preview_border)) + let message_title = Line::from(Span::styled( + format!("{}Message", message_prefix), + message_title_style, + )); + let preview = Paragraph::new(message_text) + .block(Block::default().title(message_title).borders(Borders::BOTTOM).border_style(message_border)) .style(Style::default().fg(Color::White)) .wrap(ratatui::widgets::Wrap { trim: false }) - .scroll((preview_scroll, 0)); + .scroll((message_scroll, 0)); frame.render_widget(preview, layout[1]); let status = Paragraph::new(" 'q' quit | 'r' refresh | 'd' delete | ↑/↓ navigate | Tab switch pane") @@ -280,8 +290,8 @@ pub fn main(config: &Config, terminal: &mut Terminal>) KeyCode::Char('q') | KeyCode::Esc => break, KeyCode::Tab => { focus = match focus { - Focus::Inbox => Focus::Preview, - Focus::Preview => Focus::Inbox, + Focus::Inbox => Focus::Message, + Focus::Message => Focus::Inbox, }; } KeyCode::Char('r') => { @@ -305,18 +315,18 @@ pub fn main(config: &Config, terminal: &mut Terminal>) let new_idx = i.min(len.saturating_sub(1)); list_state.select(Some(new_idx)); let seq = emails[new_idx].seq; - if preview_seq != Some(seq) { - preview_seq = Some(seq); - preview_body.clear(); - preview_scroll = 0; + if message_seq != Some(seq) { + message_seq = Some(seq); + message_body.clear(); + message_scroll = 0; body_loading = true; wanted_body_seq.store(seq, Ordering::Relaxed); let _ = cmd_tx.send(WorkerCmd::FetchBody { seq }); } } } - Focus::Preview => { - preview_scroll = preview_scroll.saturating_add(1); + Focus::Message => { + message_scroll = message_scroll.saturating_add(1); } } KeyCode::Up | KeyCode::Char('k') => match focus { @@ -325,18 +335,18 @@ pub fn main(config: &Config, terminal: &mut Terminal>) list_state.select(Some(i)); if !emails.is_empty() { let seq = emails[i].seq; - if preview_seq != Some(seq) { - preview_seq = Some(seq); - preview_body.clear(); - preview_scroll = 0; + if message_seq != Some(seq) { + message_seq = Some(seq); + message_body.clear(); + message_scroll = 0; body_loading = true; wanted_body_seq.store(seq, Ordering::Relaxed); let _ = cmd_tx.send(WorkerCmd::FetchBody { seq }); } } } - Focus::Preview => { - preview_scroll = preview_scroll.saturating_sub(1); + Focus::Message => { + message_scroll = message_scroll.saturating_sub(1); } } KeyCode::Char('d') if focus == Focus::Inbox => { @@ -347,16 +357,16 @@ pub fn main(config: &Config, terminal: &mut Terminal>) emails.remove(idx); if emails.is_empty() { list_state.select(None); - preview_seq = None; - preview_body.clear(); + message_seq = None; + message_body.clear(); } else { let new_idx = idx.min(emails.len().saturating_sub(1)); list_state.select(Some(new_idx)); let new_seq = emails[new_idx].seq; - if preview_seq != Some(new_seq) { - preview_seq = Some(new_seq); - preview_body.clear(); - preview_scroll = 0; + if message_seq != Some(new_seq) { + message_seq = Some(new_seq); + message_body.clear(); + message_scroll = 0; body_loading = true; wanted_body_seq.store(new_seq, Ordering::Relaxed); let _ = cmd_tx.send(WorkerCmd::FetchBody { seq: new_seq });