diff --git a/meli/src/conf/shortcuts.rs b/meli/src/conf/shortcuts.rs index e5656291..95d9bdc5 100644 --- a/meli/src/conf/shortcuts.rs +++ b/meli/src/conf/shortcuts.rs @@ -170,6 +170,7 @@ shortcut_key_values! { "listing", toggle_mailbox_collapse |> "Toggle mailbox collapse in menu." |> Key::Char(' '), prev_page |> "Go to previous page." |> Key::PageUp, search |> "Search within list of e-mails." |> Key::Char('/'), + jump_to |> "Search and jump to entry matches." |> Key::Char(' '), refresh |> "Manually request a mailbox refresh." |> Key::F(5), set_seen |> "Set thread as seen." |> Key::Char('n'), union_modifier |> "Union modifier." |> Key::Ctrl('u'), diff --git a/meli/src/mail/listing.rs b/meli/src/mail/listing.rs index 55120322..738a03ca 100644 --- a/meli/src/mail/listing.rs +++ b/meli/src/mail/listing.rs @@ -1005,6 +1005,8 @@ pub trait ListingTrait: Component { ), } } + + fn jump_to(&mut self, _text: &str, _context: &mut Context) {} } #[derive(Debug)] @@ -1028,11 +1030,12 @@ impl From<(String, JoinHandle>>)> for Backg struct BackgroundJobs { pub search_job: Option, pub select_job: Option, + pub jump_to_job: Option, } impl BackgroundJobs { pub fn is_match(&self, job_id: &JobId) -> bool { - [&self.search_job, &self.select_job].iter().any( + [&self.search_job, &self.select_job, &self.jump_to_job].iter().any( |bg| matches!(bg, Some(BackgroundSearch { handle, .. }) if handle.job_id == *job_id), ) } @@ -1123,6 +1126,66 @@ enum ShowMenuScrollbar { False, } +#[derive(Debug)] +enum CmdBuf { + Command(String), + JumpTo(String), +} + +impl CmdBuf { + #[inline] + fn clone(&self) -> String { + match self { + Self::Command(ref v) | Self::JumpTo(ref v) => v.clone(), + } + } + + #[inline] + fn pop(&mut self) -> Option { + match self { + Self::Command(ref mut v) | Self::JumpTo(ref mut v) => v.pop(), + } + } + + #[inline] + fn push(&mut self, c: char) -> bool { + match self { + Self::Command(_) if !c.is_ascii_digit() => false, + Self::Command(ref mut v) | Self::JumpTo(ref mut v) => { + v.push(c); + true + } + } + } + + #[inline] + fn is_empty(&self) -> bool { + match self { + Self::Command(ref v) | Self::JumpTo(ref v) => v.is_empty(), + } + } + + #[inline] + fn clear(&mut self) { + *self = Self::Command({ + match self { + Self::Command(ref mut v) | Self::JumpTo(ref mut v) => { + v.clear(); + std::mem::take(v) + } + } + }); + } + + #[inline] + fn parse_usize(&self) -> Option { + let Self::Command(ref v) = self else { + return None; + }; + v.parse::().ok() + } +} + #[derive(Debug)] pub struct Listing { component: ListingComponent, @@ -1142,7 +1205,7 @@ pub struct Listing { sidebar_divider_theme: ThemeAttribute, menu_visibility: bool, - cmd_buf: String, + cmd_buf: CmdBuf, /// This is the width of the right container to the entire width. ratio: usize, // right/(container width) * 100 prev_ratio: usize, @@ -1617,7 +1680,7 @@ impl Component for Listing { { let amount = if self.cmd_buf.is_empty() { 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { + } else if let Some(amount) = self.cmd_buf.parse_usize() { self.cmd_buf.clear(); self.component.set_modifier_active(false); context @@ -1673,7 +1736,7 @@ impl Component for Listing { { let amount = if self.cmd_buf.is_empty() { 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { + } else if let Some(amount) = self.cmd_buf.parse_usize() { self.cmd_buf.clear(); self.component.set_modifier_active(false); context @@ -1756,6 +1819,35 @@ impl Component for Listing { if self.status.is_none() { match event { + UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) + if !self.cmd_buf.is_empty() => + { + self.cmd_buf.clear(); + self.component.set_modifier_active(false); + if let CmdBuf::JumpTo(ref s) = self.cmd_buf { + self.component.jump_to(s, context); + } + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); + return true; + } + UIEvent::Input(Key::Char(c)) + if matches!(self.cmd_buf, CmdBuf::JumpTo(_)) + || matches!(self.cmd_buf, CmdBuf::Command(_) if c.is_ascii_digit()) => + { + self.cmd_buf.push(*c); + self.component.set_modifier_active(true); + if let CmdBuf::JumpTo(ref s) = self.cmd_buf { + self.component.jump_to(s, context); + } + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::BufSet( + self.cmd_buf.clone(), + ))); + return true; + } UIEvent::Action(ref action) => match action { Action::Listing(ListingAction::SetPlain) => { self.set_index_style(IndexStyle::Plain, context); @@ -1843,7 +1935,7 @@ impl Component for Listing { { let amount = if self.cmd_buf.is_empty() { 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { + } else if let Some(amount) = self.cmd_buf.parse_usize() { self.cmd_buf.clear(); self.component.set_modifier_active(false); context @@ -1866,7 +1958,7 @@ impl Component for Listing { { let amount = if self.cmd_buf.is_empty() { 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { + } else if let Some(amount) = self.cmd_buf.parse_usize() { self.cmd_buf.clear(); self.component.set_modifier_active(false); context @@ -1889,7 +1981,7 @@ impl Component for Listing { { let amount = if self.cmd_buf.is_empty() { 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { + } else if let Some(amount) = self.cmd_buf.parse_usize() { self.cmd_buf.clear(); self.component.set_modifier_active(false); context @@ -1912,7 +2004,7 @@ impl Component for Listing { { let amount = if self.cmd_buf.is_empty() { 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { + } else if let Some(amount) = self.cmd_buf.parse_usize() { self.cmd_buf.clear(); self.component.set_modifier_active(false); context @@ -1935,7 +2027,7 @@ impl Component for Listing { { let mult = if self.cmd_buf.is_empty() { 1 - } else if let Ok(mult) = self.cmd_buf.parse::() { + } else if let Some(mult) = self.cmd_buf.parse_usize() { self.cmd_buf.clear(); self.component.set_modifier_active(false); context @@ -1958,7 +2050,7 @@ impl Component for Listing { { let mult = if self.cmd_buf.is_empty() { 1 - } else if let Ok(mult) = self.cmd_buf.parse::() { + } else if let Some(mult) = self.cmd_buf.parse_usize() { self.cmd_buf.clear(); self.component.set_modifier_active(false); context @@ -2071,6 +2163,15 @@ impl Component for Listing { self.component.prev_entry(context); return true; } + UIEvent::Input(ref k) + if shortcut!(k == shortcuts[Shortcuts::LISTING]["jump_to"]) => + { + if let CmdBuf::Command(ref mut v) = self.cmd_buf { + v.clear(); + self.cmd_buf = CmdBuf::JumpTo(std::mem::take(v)); + } + return true; + } UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) if !self.component.unfocused() => { @@ -2086,26 +2187,6 @@ impl Component for Listing { self.component.set_dirty(true); return true; } - UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) - if !self.cmd_buf.is_empty() => - { - self.cmd_buf.clear(); - self.component.set_modifier_active(false); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); - return true; - } - UIEvent::Input(Key::Char(c)) if c.is_ascii_digit() => { - self.cmd_buf.push(*c); - self.component.set_modifier_active(true); - context - .replies - .push_back(UIEvent::StatusEvent(StatusEvent::BufSet( - self.cmd_buf.clone(), - ))); - return true; - } _ => {} } } @@ -2192,7 +2273,7 @@ impl Component for Listing { { let mut amount = if self.cmd_buf.is_empty() { 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { + } else if let Some(amount) = self.cmd_buf.parse_usize() { self.cmd_buf.clear(); self.component.set_modifier_active(false); context @@ -2320,7 +2401,7 @@ impl Component for Listing { { let amount = if self.cmd_buf.is_empty() { 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { + } else if let Some(amount) = self.cmd_buf.parse_usize() { self.cmd_buf.clear(); self.component.set_modifier_active(false); context @@ -2382,7 +2463,7 @@ impl Component for Listing { { let amount = if self.cmd_buf.is_empty() { 1 - } else if let Ok(amount) = self.cmd_buf.parse::() { + } else if let Some(amount) = self.cmd_buf.parse_usize() { self.cmd_buf.clear(); self.component.set_modifier_active(false); context @@ -2618,7 +2699,8 @@ impl Component for Listing { .push_back(UIEvent::StatusEvent(StatusEvent::BufClear)); return true; } - UIEvent::Input(Key::Char(c)) if c.is_ascii_digit() => { + UIEvent::Input(Key::Char(c)) if matches!(self.cmd_buf, CmdBuf::JumpTo(_) | CmdBuf::Command(_) if c.is_ascii_digit()) => + { self.cmd_buf.push(c); self.component.set_modifier_active(true); context @@ -2850,7 +2932,7 @@ impl Listing { prev_ratio: *account_settings!(context[first_account_hash].listing.sidebar_ratio), menu_width: WidgetWidth::Unset, focus: ListingFocus::Mailbox, - cmd_buf: String::with_capacity(4), + cmd_buf: CmdBuf::Command(String::with_capacity(8)), }; ret.component.realize(ret.id().into(), context); ret.change_account(context); diff --git a/meli/src/mail/listing/compact.rs b/meli/src/mail/listing/compact.rs index 1ee89622..a4204d3d 100644 --- a/meli/src/mail/listing/compact.rs +++ b/meli/src/mail/listing/compact.rs @@ -19,7 +19,7 @@ * along with meli. If not, see . */ -use std::{collections::BTreeMap, convert::TryInto, iter::FromIterator}; +use std::{collections::BTreeMap, convert::TryInto, iter::FromIterator, time::Instant}; use indexmap::IndexSet; use melib::{Address, SortField, SortOrder, TagHash, Threads}; @@ -855,6 +855,34 @@ impl ListingTrait for CompactListing { fn focus(&self) -> Focus { self.focus } + + fn jump_to(&mut self, text: &str, context: &mut Context) { + if text.is_empty() { + self.bg_jobs.jump_to_job = None; + return; + } + match context.accounts[&self.cursor_pos.0].search( + &format!("subject:\"{}\"", text), + self.sort, + self.cursor_pos.1, + ) { + Ok(job) => { + let handle = context.accounts[&self.cursor_pos.0] + .main_loop_handler + .job_executor + .spawn_specialized("search".into(), job); + self.bg_jobs.jump_to_job = Some((text.to_string(), handle).into()); + } + Err(err) => { + context.replies.push_back(UIEvent::Notification { + title: Some("Could not perform search".into()), + body: err.to_string().into(), + kind: Some(crate::types::NotificationType::Error(err.kind)), + source: Some(err), + }); + } + }; + } } impl std::fmt::Display for CompactListing { @@ -2123,6 +2151,54 @@ impl Component for CompactListing { return false; } self.bg_jobs.select_job = job; + let job = self.bg_jobs.jump_to_job.take(); + if job + .as_ref() + .map(|bg| bg.handle.job_id == *job_id) + .unwrap_or(false) + { + let BackgroundSearch { mut handle, .. } = job.unwrap(); + match handle.chan.try_recv() { + Err(_) => { /* search was canceled */ } + Ok(None) => { /* something happened, perhaps a worker thread panicked */ } + Ok(Some(Err(err))) => { + context.replies.push_back(UIEvent::Notification { + title: Some("Could not perform search".into()), + body: err.to_string().into(), + kind: Some(crate::types::NotificationType::Error(err.kind)), + source: Some(err), + }); + } + Ok(Some(Ok(results))) => { + let account = &context.accounts[&self.cursor_pos.0]; + let threads = account.collection.get_threads(self.cursor_pos.1); + for env_hash in results.into_iter().rev() { + if !account.collection.contains_key(&env_hash) { + continue; + } + let env_thread_node_hash = + account.collection.get_env(env_hash).thread(); + if !threads.thread_nodes.contains_key(&env_thread_node_hash) { + continue; + } + let thread = threads + .find_group(threads.thread_nodes[&env_thread_node_hash].group); + if self.filtered_order.contains_key(&thread) { + continue; + } + if self.rows.all_threads.contains(&thread) { + self.new_cursor_pos.2 = self.rows.thread_order[&thread]; + self.force_draw = true; + self.dirty = true; + break; + } + } + } + } + self.set_dirty(true); + return false; + } + self.bg_jobs.jump_to_job = job; } _ => {} }