use super::*;
pub enum FindFiles {
Asking(FindForm),
Found(Search),
}
pub fn find_fields() -> [&'static str; 4] {
[t!("Name"), t!("Containing"), t!("Size"), t!("Modified")]
}
pub const FIND_REGEX_ROW: usize = 4;
#[derive(Clone)]
pub struct FindForm {
pub fields: [TextInput; 4],
pub focus: usize,
pub regex: bool,
}
impl FindForm {
fn text(&self, field: usize) -> &str {
&self.fields[field].text
}
}
impl Default for FindForm {
fn default() -> Self {
let mut name = TextInput::new();
name.set("*".to_string());
FindForm {
fields: [name, TextInput::new(), TextInput::new(), TextInput::new()],
focus: 0,
regex: false,
}
}
}
pub struct Search {
pub start: PathBuf,
pub label: String,
pub results: Vec<(PathBuf, bool)>,
pub cursor: usize,
pub scroll: usize,
pub looking_in: Option<PathBuf>,
pub done: bool,
pub unreadable: usize,
pub(super) cancel: Arc<AtomicBool>,
pub(super) updates: Receiver<crate::search::Update>,
}
impl Drop for Search {
fn drop(&mut self) {
self.cancel.store(true, Ordering::Relaxed);
}
}
impl AppState {
pub fn open_find_files(&mut self) {
if self.panel_archive(self.is_left_active).is_some() {
self.display_error(t!("Find files cannot search inside an archive yet").to_string());
return;
}
let mut form = self.find_files_last.clone();
form.focus = 0;
self.dialog = Some(Dialog::FindFiles(Box::new(FindFiles::Asking(form))));
}
pub fn find_files_switch_field(&mut self, forward: bool) {
if let Some(Dialog::FindFiles(find)) = &mut self.dialog
&& let FindFiles::Asking(form) = find.as_mut()
{
let rows = FIND_REGEX_ROW + 1;
form.focus = if forward { (form.focus + 1) % rows } else { (form.focus + rows - 1) % rows };
}
}
pub fn find_files_toggle_regex(&mut self) {
if let Some(Dialog::FindFiles(find)) = &mut self.dialog
&& let FindFiles::Asking(form) = find.as_mut()
{
form.regex = !form.regex;
}
}
pub fn start_find_files(&mut self) {
let Some(Dialog::FindFiles(find)) = &self.dialog else {
return;
};
let FindFiles::Asking(form) = find.as_ref() else {
return;
};
let form = form.clone();
self.find_files_last = form.clone();
let (name, containing, size, modified) = (form.text(0), form.text(1), form.text(2), form.text(3));
let parsed = (|| -> Result<_, String> { Ok((crate::search::Text::new(containing, form.regex)?, crate::search::SizeRange::parse(size)?, crate::search::TimeRange::parse(modified, chrono::Local::now())?)) })();
let (text, size_range, time_range) = match parsed {
Ok(parsed) => parsed,
Err(message) => {
self.display_error(message);
return;
}
};
let pattern = if name.trim().is_empty() { "*" } else { name };
let mut label = pattern.to_string();
if !containing.is_empty() {
label.push_str(&if form.regex { format!(" matching /{containing}/") } else { format!(" containing \"{containing}\"") });
}
if !size.trim().is_empty() {
label.push_str(&format!(", size {}", size.trim()));
}
if !modified.trim().is_empty() {
label.push_str(&format!(", modified {}", modified.trim()));
}
let start = self.panel_dir(self.is_left_active);
let query = crate::search::Query {
start: start.clone(),
name: crate::glob::Glob::new(pattern).unwrap_or_else(|| crate::glob::Glob::new("*").expect("* is a pattern")),
containing: text,
size: size_range,
modified: time_range,
show_hidden: self.options.show_hidden,
case_sensitive: self.options.case_sensitive,
};
let cancel = Arc::new(AtomicBool::new(false));
let (sender, updates) = mpsc::channel();
let worker_cancel = Arc::clone(&cancel);
std::thread::spawn(move || {
crate::search::search(&query, &worker_cancel, &mut |update| sender.send(update).is_ok());
});
self.dialog = Some(Dialog::FindFiles(Box::new(FindFiles::Found(Search {
looking_in: Some(start.clone()),
start,
label,
results: Vec::new(),
cursor: 0,
scroll: 0,
done: false,
unreadable: 0,
cancel,
updates,
}))));
}
pub fn poll_find_files(&mut self) {
let Some(Dialog::FindFiles(find)) = &mut self.dialog else {
return;
};
let FindFiles::Found(search) = find.as_mut() else {
return;
};
loop {
match search.updates.try_recv() {
Ok(crate::search::Update::Looking(dir)) => search.looking_in = Some(dir),
Ok(crate::search::Update::Found(path, is_dir)) => search.results.push((path, is_dir)),
Ok(crate::search::Update::Finished { unreadable }) => {
search.unreadable = unreadable;
search.looking_in = None;
search.done = true;
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
search.looking_in = None;
search.done = true;
break;
}
}
}
}
pub fn find_files_move(&mut self, by: isize) {
let page = self.find_files_page.max(1);
if let Some(Dialog::FindFiles(find)) = &mut self.dialog
&& let FindFiles::Found(search) = find.as_mut()
{
let last = search.results.len().saturating_sub(1);
search.cursor = search.cursor.saturating_add_signed(by).min(last);
if search.cursor < search.scroll {
search.scroll = search.cursor;
} else if search.cursor >= search.scroll + page {
search.scroll = search.cursor + 1 - page;
}
}
}
pub fn feed_find_files(&mut self) {
let Some(Dialog::FindFiles(find)) = &self.dialog else {
return;
};
let FindFiles::Found(search) = find.as_ref() else {
return;
};
let start = search.start.clone();
let feed: Vec<(PathBuf, bool)> = search.results.iter().filter_map(|(path, is_dir)| path.strip_prefix(&start).ok().map(|relative| (relative.to_path_buf(), *is_dir))).collect();
if feed.is_empty() {
self.display_error(t!("Nothing found to feed to the panel").to_string());
return;
}
let first = feed[0].0.clone().into_os_string();
self.dialog = None;
let is_left = self.is_left_active;
*self.flat_mut(is_left) = Some(Flat { dir: start.clone(), feed: Some(Arc::new(feed)) });
if self.panel_dir(is_left) == start {
self.reload_panel(is_left, Some(&first));
} else {
self.open_dir(is_left, start, Some(&first));
}
}
pub fn find_files_go(&mut self) {
let Some(Dialog::FindFiles(find)) = &self.dialog else {
return;
};
let FindFiles::Found(search) = find.as_ref() else {
return;
};
let Some((path, _)) = search.results.get(search.cursor) else {
return;
};
let (Some(dir), Some(name)) = (path.parent().map(Path::to_path_buf), path.file_name().map(OsString::from)) else {
return;
};
self.dialog = None;
self.open_dir(self.is_left_active, dir, Some(&name));
}
}