Clean up HTML table pipes in message body and improve focus indicators
- Strip pipe characters from HTML-to-markdown table remnants in email body - Add bold + arrow prefix (▶) to focused pane title for clear focus indication - Rename Focus::Preview and all preview_* variables to Message/message_* Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3e647dbe52
commit
e6ff04a97d
2 changed files with 86 additions and 53 deletions
23
src/inbox.rs
23
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::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
let result = re_blank_lines.replace_all(&result, "\n\n");
|
||||
result.trim().to_string()
|
||||
}
|
||||
|
|
|
|||
116
src/lib.rs
116
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<CrosstermBackend<Stdout>>)
|
|||
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<u32> = None;
|
||||
let mut message_body: String = String::new();
|
||||
let mut message_seq: Option<u32> = 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<CrosstermBackend<Stdout>>)
|
|||
.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<CrosstermBackend<Stdout>>)
|
|||
}
|
||||
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<CrosstermBackend<Stdout>>)
|
|||
.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<CrosstermBackend<Stdout>>)
|
|||
})
|
||||
.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<CrosstermBackend<Stdout>>)
|
|||
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<CrosstermBackend<Stdout>>)
|
|||
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<CrosstermBackend<Stdout>>)
|
|||
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<CrosstermBackend<Stdout>>)
|
|||
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<CrosstermBackend<Stdout>>)
|
|||
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<CrosstermBackend<Stdout>>)
|
|||
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 });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue