Compare commits

...

2 Commits

Author SHA1 Message Date
Manos Pitsidianakis f1ee8aca1c
Add jump to search function WIP 3 weeks ago
Manos Pitsidianakis 74d98202d6
listing: add BackgroundSearch struct WIP
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
3 weeks ago

@ -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'),

@ -39,6 +39,7 @@ use super::*;
use crate::{
accounts::{JobRequest, MailboxStatus},
components::ExtendShortcutsMaps,
jobs::{JobId, JoinHandle},
};
pub const DEFAULT_ATTACHMENT_FLAG: &str = concat!("📎", emoji_text_presentation_selector!());
@ -1004,6 +1005,40 @@ pub trait ListingTrait: Component {
),
}
}
fn jump_to(&mut self, _text: &str, _context: &mut Context) {}
}
#[derive(Debug)]
pub struct BackgroundSearch {
pub text: String,
pub timestamp: std::time::Instant,
pub handle: JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>,
}
impl From<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)> for BackgroundSearch {
fn from((text, handle): (String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)) -> Self {
Self {
text,
handle,
timestamp: std::time::Instant::now(),
}
}
}
#[derive(Default, Debug)]
struct BackgroundJobs {
pub search_job: Option<BackgroundSearch>,
pub select_job: Option<BackgroundSearch>,
pub jump_to_job: Option<BackgroundSearch>,
}
impl BackgroundJobs {
pub fn is_match(&self, job_id: &JobId) -> bool {
[&self.search_job, &self.select_job, &self.jump_to_job].iter().any(
|bg| matches!(bg, Some(BackgroundSearch { handle, .. }) if handle.job_id == *job_id),
)
}
}
#[derive(Debug)]
@ -1091,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<char> {
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<usize> {
let Self::Command(ref v) = self else {
return None;
};
v.parse::<usize>().ok()
}
}
#[derive(Debug)]
pub struct Listing {
component: ListingComponent,
@ -1110,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,
@ -1585,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::<usize>() {
} else if let Some(amount) = self.cmd_buf.parse_usize() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
@ -1641,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::<usize>() {
} else if let Some(amount) = self.cmd_buf.parse_usize() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
@ -1724,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);
@ -1811,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::<usize>() {
} else if let Some(amount) = self.cmd_buf.parse_usize() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
@ -1834,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::<usize>() {
} else if let Some(amount) = self.cmd_buf.parse_usize() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
@ -1857,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::<usize>() {
} else if let Some(amount) = self.cmd_buf.parse_usize() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
@ -1880,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::<usize>() {
} else if let Some(amount) = self.cmd_buf.parse_usize() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
@ -1903,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::<usize>() {
} else if let Some(mult) = self.cmd_buf.parse_usize() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
@ -1926,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::<usize>() {
} else if let Some(mult) = self.cmd_buf.parse_usize() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
@ -2039,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() =>
{
@ -2054,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;
}
_ => {}
}
}
@ -2160,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::<usize>() {
} else if let Some(amount) = self.cmd_buf.parse_usize() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
@ -2288,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::<usize>() {
} else if let Some(amount) = self.cmd_buf.parse_usize() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
@ -2350,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::<usize>() {
} else if let Some(amount) = self.cmd_buf.parse_usize() {
self.cmd_buf.clear();
self.component.set_modifier_active(false);
context
@ -2586,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
@ -2818,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);

@ -19,7 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
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};
@ -142,10 +142,7 @@ pub struct CompactListing {
rows_drawn: SegmentTree,
rows: RowsState<(ThreadHash, EnvelopeHash)>,
#[allow(clippy::type_complexity)]
search_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
#[allow(clippy::type_complexity)]
select_job: Option<(String, JoinHandle<Result<SmallVec<[EnvelopeHash; 512]>>>)>,
bg_jobs: BackgroundJobs,
filter_term: String,
filtered_selection: Vec<ThreadHash>,
filtered_order: HashMap<ThreadHash, usize>,
@ -213,6 +210,7 @@ impl MailListingTrait for CompactListing {
fn refresh_mailbox(&mut self, context: &mut Context, force: bool) {
self.set_dirty(true);
self.rows.clear();
self.bg_jobs = BackgroundJobs::default();
let old_cursor_pos = self.cursor_pos;
if !(self.cursor_pos.0 == self.new_cursor_pos.0
&& self.cursor_pos.1 == self.new_cursor_pos.1)
@ -556,6 +554,7 @@ impl ListingTrait for CompactListing {
fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.focus = Focus::None;
self.bg_jobs = BackgroundJobs::default();
self.filtered_selection.clear();
self.filtered_order.clear();
self.filter_term.clear();
@ -856,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 {
@ -873,8 +900,7 @@ impl CompactListing {
sort: (Default::default(), Default::default()),
sortcmd: false,
subsort: (SortField::Date, SortOrder::Desc),
search_job: None,
select_job: None,
bg_jobs: BackgroundJobs::default(),
filter_term: String::new(),
filtered_selection: Vec::new(),
filtered_order: HashMap::default(),
@ -1524,9 +1550,9 @@ impl CompactListing {
log::error!("{}", message);
context.replies.push_back(UIEvent::Notification {
title: Some("Could not perform search".into()),
source: None,
body: message.into(),
kind: Some(crate::types::NotificationType::Error(err.kind)),
source: Some(err),
});
}
}
@ -2030,14 +2056,14 @@ impl Component for CompactListing {
.main_loop_handler
.job_executor
.spawn_specialized("search".into(), job);
self.search_job = Some((filter_term.to_string(), handle));
self.bg_jobs.search_job = Some((filter_term.to_string(), handle).into());
}
Err(err) => {
context.replies.push_back(UIEvent::Notification {
title: Some("Could not perform search".into()),
source: None,
body: err.to_string().into(),
kind: Some(crate::types::NotificationType::Error(err.kind)),
source: Some(err),
});
}
};
@ -2058,15 +2084,16 @@ impl Component for CompactListing {
if let Ok(Some(search_result)) = try_recv_timeout!(&mut handle.chan) {
self.select(search_term, search_result, context);
} else {
self.select_job = Some((search_term.to_string(), handle));
self.bg_jobs.select_job =
Some((search_term.to_string(), handle).into());
}
}
Err(err) => {
context.replies.push_back(UIEvent::Notification {
title: Some("Could not perform search".into()),
source: None,
body: err.to_string().into(),
kind: Some(crate::types::NotificationType::Error(err.kind)),
source: Some(err),
});
}
};
@ -2074,42 +2101,104 @@ impl Component for CompactListing {
return true;
}
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
if self
.search_job
.as_ref()
.map(|(_, j)| j == job_id)
.unwrap_or(false) =>
if self.bg_jobs.is_match(job_id) =>
{
let (filter_term, mut handle) = self.search_job.take().unwrap();
match handle.chan.try_recv() {
Err(_) => { /* search was canceled */ }
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
Ok(Some(Ok(results))) => self.filter(filter_term, results, context),
Ok(Some(Err(err))) => {
context.replies.push_back(UIEvent::Notification {
title: Some("Could not perform search".into()),
source: None,
body: err.to_string().into(),
kind: Some(crate::types::NotificationType::Error(err.kind)),
});
let job = self.bg_jobs.search_job.take();
if job
.as_ref()
.map(|bg| bg.handle.job_id == *job_id)
.unwrap_or(false)
{
let BackgroundSearch {
text: filter_term,
mut handle,
..
} = job.unwrap();
match handle.chan.try_recv() {
Err(_) => { /* search was canceled */ }
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
Ok(Some(Ok(results))) => self.filter(filter_term, results, context),
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),
});
}
}
self.set_dirty(true);
return false;
}
self.set_dirty(true);
}
UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id))
if self
.select_job
self.bg_jobs.search_job = job;
let job = self.bg_jobs.select_job.take();
if job
.as_ref()
.map(|(_, j)| j == job_id)
.unwrap_or(false) =>
{
let (search_term, mut handle) = self.select_job.take().unwrap();
match handle.chan.try_recv() {
Err(_) => { /* search was canceled */ }
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
Ok(Some(results)) => self.select(&search_term, results, context),
.map(|bg| bg.handle.job_id == *job_id)
.unwrap_or(false)
{
let BackgroundSearch {
text: search_term,
mut handle,
..
} = job.unwrap();
match handle.chan.try_recv() {
Err(_) => { /* search was canceled */ }
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
Ok(Some(results)) => self.select(&search_term, results, context),
}
self.set_dirty(true);
return false;
}
self.set_dirty(true);
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;
}
_ => {}
}

Loading…
Cancel
Save