Add IMAP search with / keybinding and live results pane
- Press / to enter search mode; status bar shows query input - IMAP SEARCH OR SUBJECT/FROM sent in background worker thread - Results replace inbox list with match count in title - Navigation and body preview work the same as in regular inbox - Esc clears search and returns to normal inbox view Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c3a7c62214
commit
7883b35dad
2 changed files with 306 additions and 95 deletions
39
src/inbox.rs
39
src/inbox.rs
|
|
@ -253,6 +253,45 @@ fn clean_markdown(text: &str) -> String {
|
||||||
result.trim().to_string()
|
result.trim().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Search emails on the server using IMAP SEARCH. Matches subject or sender.
|
||||||
|
pub(crate) fn search(
|
||||||
|
session: &mut Option<ImapSession>,
|
||||||
|
query: &str,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<Vec<Email>, String> {
|
||||||
|
ensure_session(session, config)?;
|
||||||
|
|
||||||
|
let escaped = query.replace('"', "\\\"");
|
||||||
|
let search_query = format!("OR SUBJECT \"{}\" FROM \"{}\"", escaped, escaped);
|
||||||
|
|
||||||
|
let seq_set: std::collections::HashSet<u32> = {
|
||||||
|
let s = session.as_mut().unwrap();
|
||||||
|
match s {
|
||||||
|
ImapSession::Plain(inner) => {
|
||||||
|
inner.select("INBOX").map_err(|e| e.to_string())?;
|
||||||
|
inner.search(&search_query).map_err(|e| e.to_string())?
|
||||||
|
}
|
||||||
|
ImapSession::Tls(inner) => {
|
||||||
|
inner.select("INBOX").map_err(|e| e.to_string())?;
|
||||||
|
inner.search(&search_query).map_err(|e| e.to_string())?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if seq_set.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut seqs: Vec<u32> = seq_set.into_iter().collect();
|
||||||
|
seqs.sort_unstable();
|
||||||
|
let range = seqs.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(",");
|
||||||
|
|
||||||
|
let s = session.as_mut().unwrap();
|
||||||
|
let mut emails = fetch_range_emails(s, &range)?;
|
||||||
|
emails.sort_by(|a, b| b.seq.cmp(&a.seq)); // newest first
|
||||||
|
Ok(emails)
|
||||||
|
}
|
||||||
|
|
||||||
fn find_part(mail: &mailparse::ParsedMail, mime_type: &str) -> Option<String> {
|
fn find_part(mail: &mailparse::ParsedMail, mime_type: &str) -> Option<String> {
|
||||||
let content_type = mail.ctype.mimetype.to_lowercase();
|
let content_type = mail.ctype.mimetype.to_lowercase();
|
||||||
if content_type == mime_type {
|
if content_type == mime_type {
|
||||||
|
|
|
||||||
210
src/lib.rs
210
src/lib.rs
|
|
@ -25,6 +25,12 @@ enum Focus {
|
||||||
Message,
|
Message,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
enum Mode {
|
||||||
|
Normal,
|
||||||
|
Search,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct Email {
|
pub(crate) struct Email {
|
||||||
pub seq: u32,
|
pub seq: u32,
|
||||||
pub subject: String,
|
pub subject: String,
|
||||||
|
|
@ -37,6 +43,7 @@ enum WorkerCmd {
|
||||||
FetchMore { oldest_seq: u32 },
|
FetchMore { oldest_seq: u32 },
|
||||||
FetchBody { seq: u32 },
|
FetchBody { seq: u32 },
|
||||||
Delete { seq: u32 },
|
Delete { seq: u32 },
|
||||||
|
Search { query: String },
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,6 +52,7 @@ enum WorkerResult {
|
||||||
FetchedMore(Result<(Vec<Email>, u32), String>),
|
FetchedMore(Result<(Vec<Email>, u32), String>),
|
||||||
Body { seq: u32, result: Result<String, String> },
|
Body { seq: u32, result: Result<String, String> },
|
||||||
Deleted(Result<(), String>),
|
Deleted(Result<(), String>),
|
||||||
|
Searched(Result<Vec<Email>, String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn worker_loop(
|
fn worker_loop(
|
||||||
|
|
@ -77,6 +85,10 @@ fn worker_loop(
|
||||||
let result = inbox::delete_email(&mut session, seq, &config);
|
let result = inbox::delete_email(&mut session, seq, &config);
|
||||||
let _ = result_tx.send(WorkerResult::Deleted(result));
|
let _ = result_tx.send(WorkerResult::Deleted(result));
|
||||||
}
|
}
|
||||||
|
WorkerCmd::Search { query } => {
|
||||||
|
let result = inbox::search(&mut session, &query, &config);
|
||||||
|
let _ = result_tx.send(WorkerResult::Searched(result));
|
||||||
|
}
|
||||||
WorkerCmd::Quit => break,
|
WorkerCmd::Quit => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,6 +127,13 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
let mut message_scroll: u16 = 0;
|
let mut message_scroll: u16 = 0;
|
||||||
let mut pending_fetch_seq: Option<u32> = None;
|
let mut pending_fetch_seq: Option<u32> = None;
|
||||||
let mut last_nav = Instant::now();
|
let mut last_nav = Instant::now();
|
||||||
|
// Search state
|
||||||
|
let mut mode = Mode::Normal;
|
||||||
|
let mut search_query = String::new();
|
||||||
|
let mut search_results: Vec<Email> = Vec::new();
|
||||||
|
let mut search_results_state = ListState::default();
|
||||||
|
let mut search_active = false;
|
||||||
|
let mut search_loading = false;
|
||||||
|
|
||||||
// --- Main loop ---
|
// --- Main loop ---
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -126,9 +145,7 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
.and_then(|i| emails.get(i))
|
.and_then(|i| emails.get(i))
|
||||||
.map(|e| e.seq);
|
.map(|e| e.seq);
|
||||||
|
|
||||||
// Preserve emails older than the refresh batch (previously loaded via
|
// Preserve emails older than the refresh batch (previously loaded via FetchMore)
|
||||||
// FetchMore). Without this, scrolling into older emails and then
|
|
||||||
// triggering a refresh would drop those emails and reset selection to 0.
|
|
||||||
let refresh_oldest = inbox.emails.last().map(|e| e.seq).unwrap_or(0);
|
let refresh_oldest = inbox.emails.last().map(|e| e.seq).unwrap_or(0);
|
||||||
let mut merged = inbox.emails;
|
let mut merged = inbox.emails;
|
||||||
for e in emails.drain(..) {
|
for e in emails.drain(..) {
|
||||||
|
|
@ -138,7 +155,6 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
}
|
}
|
||||||
emails = merged;
|
emails = merged;
|
||||||
|
|
||||||
// Recalculate from the merged list
|
|
||||||
oldest_seq = emails.last().map(|e| e.seq);
|
oldest_seq = emails.last().map(|e| e.seq);
|
||||||
has_older = oldest_seq.map_or(false, |s| s > 1);
|
has_older = oldest_seq.map_or(false, |s| s > 1);
|
||||||
error = None;
|
error = None;
|
||||||
|
|
@ -149,8 +165,6 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
list_state.select(Some(new_idx));
|
list_state.select(Some(new_idx));
|
||||||
let seq = emails[new_idx].seq;
|
let seq = emails[new_idx].seq;
|
||||||
// Fetch if the selected email changed OR a debounce fetch is pending
|
|
||||||
// (pending means the body shown may be stale from a previous email)
|
|
||||||
if message_seq != Some(seq) || pending_fetch_seq.is_some() {
|
if message_seq != Some(seq) || pending_fetch_seq.is_some() {
|
||||||
message_seq = Some(seq);
|
message_seq = Some(seq);
|
||||||
message_body.clear();
|
message_body.clear();
|
||||||
|
|
@ -199,6 +213,30 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
error = Some(format!("Delete failed: {}", e));
|
error = Some(format!("Delete failed: {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WorkerResult::Searched(Ok(results)) => {
|
||||||
|
search_results = results;
|
||||||
|
search_results_state
|
||||||
|
.select(if search_results.is_empty() { None } else { Some(0) });
|
||||||
|
search_loading = false;
|
||||||
|
search_active = true;
|
||||||
|
// Fetch body of the first result
|
||||||
|
if let Some(first) = search_results.first() {
|
||||||
|
let seq = first.seq;
|
||||||
|
message_seq = Some(seq);
|
||||||
|
message_body.clear();
|
||||||
|
message_rendered = None;
|
||||||
|
message_scroll = 0;
|
||||||
|
body_loading = true;
|
||||||
|
pending_fetch_seq = None;
|
||||||
|
wanted_body_seq.store(seq, Ordering::Relaxed);
|
||||||
|
let _ = cmd_tx.send(WorkerCmd::FetchBody { seq });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WorkerResult::Searched(Err(e)) => {
|
||||||
|
error = Some(format!("Search failed: {}", e));
|
||||||
|
search_loading = false;
|
||||||
|
search_active = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,7 +270,7 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
])
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
// --- Top: Inbox list ---
|
// --- Top: Inbox / Search results ---
|
||||||
let (inbox_title_style, inbox_prefix) = if focus == Focus::Inbox {
|
let (inbox_title_style, inbox_prefix) = if focus == Focus::Inbox {
|
||||||
(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), "▶ ")
|
(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), "▶ ")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -250,19 +288,81 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
Line::from(Span::styled(format!("{}{}", inbox_prefix, label), inbox_title_style))
|
Line::from(Span::styled(format!("{}{}", inbox_prefix, label), inbox_title_style))
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(e) = &error {
|
if search_loading {
|
||||||
|
let p = Paragraph::new(format!("Searching for \"{}\"...", search_query))
|
||||||
|
.block(Block::default()
|
||||||
|
.title(inbox_title("Search"))
|
||||||
|
.borders(Borders::BOTTOM)
|
||||||
|
.border_style(inbox_border))
|
||||||
|
.style(Style::default().fg(Color::Yellow));
|
||||||
|
frame.render_widget(p, layout[0]);
|
||||||
|
} else if search_active {
|
||||||
|
let title_str = format!(
|
||||||
|
"Search: \"{}\" ({} result{})",
|
||||||
|
search_query,
|
||||||
|
search_results.len(),
|
||||||
|
if search_results.len() == 1 { "" } else { "s" }
|
||||||
|
);
|
||||||
|
if search_results.is_empty() {
|
||||||
|
let p = Paragraph::new("No results found.")
|
||||||
|
.block(Block::default()
|
||||||
|
.title(inbox_title(&title_str))
|
||||||
|
.borders(Borders::BOTTOM)
|
||||||
|
.border_style(inbox_border))
|
||||||
|
.style(Style::default().fg(Color::Yellow));
|
||||||
|
frame.render_widget(p, layout[0]);
|
||||||
|
} else {
|
||||||
|
let max_from = search_results.iter()
|
||||||
|
.map(|e| e.from.chars().count())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(20)
|
||||||
|
.min(40);
|
||||||
|
let items: Vec<ListItem> = search_results
|
||||||
|
.iter()
|
||||||
|
.map(|e| {
|
||||||
|
let from_len = e.from.chars().count();
|
||||||
|
let from = if from_len > max_from {
|
||||||
|
format!("{}…", e.from.chars().take(max_from.saturating_sub(1)).collect::<String>())
|
||||||
|
} else {
|
||||||
|
format!("{:<width$}", e.from, width = max_from)
|
||||||
|
};
|
||||||
|
ListItem::new(format!("{} | {} | {}", e.date, from, e.subject))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let list = List::new(items)
|
||||||
|
.block(Block::default()
|
||||||
|
.title(inbox_title(&title_str))
|
||||||
|
.borders(Borders::BOTTOM)
|
||||||
|
.border_style(inbox_border))
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
.highlight_style(
|
||||||
|
Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD),
|
||||||
|
)
|
||||||
|
.highlight_symbol(">> ");
|
||||||
|
frame.render_stateful_widget(list, layout[0], &mut search_results_state);
|
||||||
|
}
|
||||||
|
} else if let Some(e) = &error {
|
||||||
let p = Paragraph::new(format!("IMAP error: {}", e))
|
let p = Paragraph::new(format!("IMAP error: {}", e))
|
||||||
.block(Block::default().title(inbox_title("Inbox")).borders(Borders::BOTTOM).border_style(inbox_border))
|
.block(Block::default()
|
||||||
|
.title(inbox_title("Inbox"))
|
||||||
|
.borders(Borders::BOTTOM)
|
||||||
|
.border_style(inbox_border))
|
||||||
.style(Style::default().fg(Color::Red));
|
.style(Style::default().fg(Color::Red));
|
||||||
frame.render_widget(p, layout[0]);
|
frame.render_widget(p, layout[0]);
|
||||||
} else if emails.is_empty() && loading {
|
} else if emails.is_empty() && loading {
|
||||||
let p = Paragraph::new("Loading...")
|
let p = Paragraph::new("Loading...")
|
||||||
.block(Block::default().title(inbox_title("Inbox")).borders(Borders::BOTTOM).border_style(inbox_border))
|
.block(Block::default()
|
||||||
|
.title(inbox_title("Inbox"))
|
||||||
|
.borders(Borders::BOTTOM)
|
||||||
|
.border_style(inbox_border))
|
||||||
.style(Style::default().fg(Color::Yellow));
|
.style(Style::default().fg(Color::Yellow));
|
||||||
frame.render_widget(p, layout[0]);
|
frame.render_widget(p, layout[0]);
|
||||||
} else if emails.is_empty() {
|
} else if emails.is_empty() {
|
||||||
let p = Paragraph::new("No messages in inbox.")
|
let p = Paragraph::new("No messages in inbox.")
|
||||||
.block(Block::default().title(inbox_title("Inbox")).borders(Borders::BOTTOM).border_style(inbox_border))
|
.block(Block::default()
|
||||||
|
.title(inbox_title("Inbox"))
|
||||||
|
.borders(Borders::BOTTOM)
|
||||||
|
.border_style(inbox_border))
|
||||||
.style(Style::default().fg(Color::Yellow));
|
.style(Style::default().fg(Color::Yellow));
|
||||||
frame.render_widget(p, layout[0]);
|
frame.render_widget(p, layout[0]);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -342,7 +442,15 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
.scroll((message_scroll, 0));
|
.scroll((message_scroll, 0));
|
||||||
frame.render_widget(preview, layout[1]);
|
frame.render_widget(preview, layout[1]);
|
||||||
|
|
||||||
let status = Paragraph::new(" 'q' quit | 'r' refresh | 'd' delete | ↑/↓ navigate | Tab switch pane")
|
// Status bar
|
||||||
|
let status_text = if mode == Mode::Search {
|
||||||
|
format!(" Search: {}_", search_query)
|
||||||
|
} else if search_active {
|
||||||
|
" / new search | Esc clear | q quit | ↑/↓ navigate | Tab switch pane".to_string()
|
||||||
|
} else {
|
||||||
|
" / search | q quit | r refresh | d delete | ↑/↓ navigate | Tab switch pane".to_string()
|
||||||
|
};
|
||||||
|
let status = Paragraph::new(status_text)
|
||||||
.style(Style::default().fg(Color::DarkGray));
|
.style(Style::default().fg(Color::DarkGray));
|
||||||
frame.render_widget(status, layout[2]);
|
frame.render_widget(status, layout[2]);
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -350,8 +458,41 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
// --- Input handling ---
|
// --- Input handling ---
|
||||||
if event::poll(Duration::from_millis(200))? {
|
if event::poll(Duration::from_millis(200))? {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if mode == Mode::Search {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') | KeyCode::Esc => break,
|
KeyCode::Char(c) => search_query.push(c),
|
||||||
|
KeyCode::Backspace => { search_query.pop(); }
|
||||||
|
KeyCode::Enter => {
|
||||||
|
if !search_query.is_empty() {
|
||||||
|
search_loading = true;
|
||||||
|
search_active = true;
|
||||||
|
search_results.clear();
|
||||||
|
search_results_state.select(None);
|
||||||
|
let _ = cmd_tx.send(WorkerCmd::Search { query: search_query.clone() });
|
||||||
|
}
|
||||||
|
mode = Mode::Normal;
|
||||||
|
}
|
||||||
|
KeyCode::Esc => {
|
||||||
|
mode = Mode::Normal;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') => break,
|
||||||
|
KeyCode::Esc => {
|
||||||
|
if search_active {
|
||||||
|
search_active = false;
|
||||||
|
search_results.clear();
|
||||||
|
search_results_state.select(None);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('/') => {
|
||||||
|
search_query.clear();
|
||||||
|
mode = Mode::Search;
|
||||||
|
}
|
||||||
KeyCode::Tab => {
|
KeyCode::Tab => {
|
||||||
focus = match focus {
|
focus = match focus {
|
||||||
Focus::Inbox => Focus::Message,
|
Focus::Inbox => Focus::Message,
|
||||||
|
|
@ -359,7 +500,7 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
KeyCode::Char('r') => {
|
KeyCode::Char('r') => {
|
||||||
if !loading {
|
if !loading && !search_active {
|
||||||
let _ = cmd_tx.send(WorkerCmd::Refresh);
|
let _ = cmd_tx.send(WorkerCmd::Refresh);
|
||||||
loading = true;
|
loading = true;
|
||||||
last_fetch = Instant::now();
|
last_fetch = Instant::now();
|
||||||
|
|
@ -367,6 +508,22 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
}
|
}
|
||||||
KeyCode::Down | KeyCode::Char('j') => match focus {
|
KeyCode::Down | KeyCode::Char('j') => match focus {
|
||||||
Focus::Inbox => {
|
Focus::Inbox => {
|
||||||
|
if search_active {
|
||||||
|
let len = search_results.len();
|
||||||
|
if len > 0 {
|
||||||
|
let i = search_results_state
|
||||||
|
.selected()
|
||||||
|
.map_or(0, |i| (i + 1).min(len - 1));
|
||||||
|
search_results_state.select(Some(i));
|
||||||
|
let seq = search_results[i].seq;
|
||||||
|
if message_seq != Some(seq) {
|
||||||
|
message_seq = Some(seq);
|
||||||
|
message_scroll = 0;
|
||||||
|
pending_fetch_seq = Some(seq);
|
||||||
|
last_nav = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let len = emails.len();
|
let len = emails.len();
|
||||||
if len > 0 {
|
if len > 0 {
|
||||||
let i = list_state.selected().map_or(0, |i| i + 1);
|
let i = list_state.selected().map_or(0, |i| i + 1);
|
||||||
|
|
@ -387,12 +544,28 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Focus::Message => {
|
Focus::Message => {
|
||||||
message_scroll = message_scroll.saturating_add(1);
|
message_scroll = message_scroll.saturating_add(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Up | KeyCode::Char('k') => match focus {
|
KeyCode::Up | KeyCode::Char('k') => match focus {
|
||||||
Focus::Inbox => {
|
Focus::Inbox => {
|
||||||
|
if search_active {
|
||||||
|
let i = search_results_state
|
||||||
|
.selected()
|
||||||
|
.map_or(0, |i| i.saturating_sub(1));
|
||||||
|
search_results_state.select(Some(i));
|
||||||
|
if !search_results.is_empty() {
|
||||||
|
let seq = search_results[i].seq;
|
||||||
|
if message_seq != Some(seq) {
|
||||||
|
message_seq = Some(seq);
|
||||||
|
message_scroll = 0;
|
||||||
|
pending_fetch_seq = Some(seq);
|
||||||
|
last_nav = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let i = list_state.selected().map_or(0, |i| i.saturating_sub(1));
|
let i = list_state.selected().map_or(0, |i| i.saturating_sub(1));
|
||||||
list_state.select(Some(i));
|
list_state.select(Some(i));
|
||||||
if !emails.is_empty() {
|
if !emails.is_empty() {
|
||||||
|
|
@ -405,18 +578,17 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Focus::Message => {
|
Focus::Message => {
|
||||||
message_scroll = message_scroll.saturating_sub(1);
|
message_scroll = message_scroll.saturating_sub(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('d') if focus == Focus::Inbox => {
|
KeyCode::Char('d') if focus == Focus::Inbox && !search_active => {
|
||||||
if let Some(idx) = list_state.selected() {
|
if let Some(idx) = list_state.selected() {
|
||||||
if idx < emails.len() {
|
if idx < emails.len() {
|
||||||
let seq = emails[idx].seq;
|
let seq = emails[idx].seq;
|
||||||
// Remove from UI immediately
|
|
||||||
emails.remove(idx);
|
emails.remove(idx);
|
||||||
// IMAP expunge shifts sequence numbers: every message
|
// IMAP expunge shifts sequence numbers down by 1 for all seqs > deleted
|
||||||
// that had seq > deleted seq is renumbered down by 1.
|
|
||||||
for e in emails.iter_mut() {
|
for e in emails.iter_mut() {
|
||||||
if e.seq > seq {
|
if e.seq > seq {
|
||||||
e.seq -= 1;
|
e.seq -= 1;
|
||||||
|
|
@ -443,7 +615,6 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
let _ = cmd_tx.send(WorkerCmd::FetchBody { seq: new_seq });
|
let _ = cmd_tx.send(WorkerCmd::FetchBody { seq: new_seq });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Delete on server in background
|
|
||||||
let _ = cmd_tx.send(WorkerCmd::Delete { seq });
|
let _ = cmd_tx.send(WorkerCmd::Delete { seq });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -453,6 +624,7 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up worker
|
// Clean up worker
|
||||||
let _ = cmd_tx.send(WorkerCmd::Quit);
|
let _ = cmd_tx.send(WorkerCmd::Quit);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue