use which::which_re; use regex::Regex; use std::path::PathBuf; use std::{env, fs}; use termion::color; #[derive(Debug)] enum SearchType { FileSearch, CmdSearch, } ///Contains all necesseary info and methods to perform an autocomplete search. ///Built from user input. #[derive(Debug)] pub struct Search { //Left part of user input. May be empty. in "cat foo", command is cat and searchee is foo. command: String, //Where to search. Current_folder by default, but the user may have given a path //.eg "cat /foo/bar" search_path: PathBuf, //What to search for searchee: String, //Set to true if we are searching in current folder. local_search: bool, //the kind of search search_type: SearchType, //the original command original: String, } impl Search { ///Returns a Search instance, taking the command line input. pub fn build(input: &String) -> std::io::Result { let search_type = Self::discriminate_search_type(&input); //Getting cmd and searched string let mut words = input.split(' ').collect::>(); let buff_searchee = String::from(*words.last().unwrap_or(&"")); words.pop(); let mut input_cmd = words.join(" "); if input_cmd.len() > 0 { input_cmd.push_str(" "); } //Are we looking in a specific path ? //eg. am I doing cat mystuff.txt or cat folder/subfolder/mystuff.txt ? let buff_search = buff_searchee .replace(" ", ""); let mut buff_search = buff_search .split('/') .collect::>(); let searchee = String::from(*buff_search.last().unwrap_or(&"")); buff_search.pop(); let mut search_path = PathBuf::new(); let mut local = false; //We may need to modify the searchpath before storing it as a PathBuf let mut path_temp = String::new(); //NOT local path; if buff_search.len() > 0 { *&mut path_temp = buff_search.join("/"); //Making sure we have the final / in the search_path if !path_temp.ends_with('/') { *&mut path_temp.push_str("/"); } //local path; } else { *&mut path_temp = env::current_dir()? .into_os_string() .into_string() .unwrap(); if !&path_temp.ends_with('/') { *&mut path_temp.push_str("/"); } local = true; } search_path.push(&path_temp); let s = Search { command: input_cmd, search_path: search_path, searchee: searchee, local_search: local, search_type: search_type, original: String::from(input), }; return Ok(s); } ///Performs the search and returns (most likely result, result lines to display) ///A found file is preferred to a found command pub fn autocomplete(&self) -> (String, String) { let (mut res, res_lines) = match &self.search_type { SearchType::CmdSearch => autocomplete_cmd(&self.searchee, &self), SearchType::FileSearch => match autocomplete_file(&self) { Ok(t) => t, Err(_) => (String::new(), String::new()), } }; if res.len() < 1 { *&mut res = format!("{}{}", &self.command, &self.searchee); } return (res, res_lines); } fn discriminate_search_type(input: &String) -> SearchType { let tamere = input.clone(); let tamere2 = tamere.split("|") .collect::>(); let mut command = String::from(*tamere2.last().unwrap()); //Special cases //./Means we want to execute something in place if command.starts_with("./") || command.starts_with(" ./") { return SearchType::FileSearch; } if command.starts_with("watch ") { return SearchType::CmdSearch; } //sudo is not taken into account when autocompleting command = command.replace("sudo ", ""); let mut a = command.split(" ").collect::>(); let _y = String::from(a.pop().unwrap()); let mut x = String::from(a.join(" ").trim()); if x.len() > 0 { x.push_str(" ") }; let y = x.split(" ").collect::>(); if y.len() > 1 { return SearchType::FileSearch; } else { return SearchType::CmdSearch; } } } ///Look for a file using the string after the last space in the input, ///in the current folder. fn autocomplete_file(search: &Search) -> std::io::Result<(String, String)> { let mut all_res = String::new(); let mut nbr_found: u16 = 0; let mut last_found = String::new(); let mut results = Vec::new(); //This is pretty messy. for file in fs::read_dir(search.search_path.clone())? { let filepath = file.unwrap().path(); let filename = filepath .iter() .last() .unwrap() .to_str() .unwrap(); let re = format!("(?i)^{}.*$", search.searchee); let regex = Regex::new(&re).unwrap(); if regex.is_match(filename) { if search.local_search { *&mut results.push(format!("{}", filename)); } else { *&mut results.push(format!("{}{}", &search.search_path.display(), filename)); } *&mut nbr_found += 1; //if what we found is a folder, we add an / after let fullpath = format!("{}/{}", &search.search_path.display(), filename); let mut ending = String::new(); let mut matchline = format!("\r\n{}", filename); match fs::metadata(fullpath) { Ok(e) => { //Will be added to the end of the result if e.is_dir() { *&mut matchline = format!("{}{}/{}", color::Fg(color::Green), matchline, color::Fg(color::Reset) ); *&mut ending.push_str("/"); } }, Err(_) => (), }; &mut all_res.push_str(&matchline); *&mut last_found = format!("{}{}", filename, ending); } } //Handle returning the longest sequence of characters between two or more matching files if nbr_found > 1 { let longest = find_common_chars(results); *&mut last_found = longest; } if nbr_found == 0 { *&mut last_found = search.searchee.clone(); } //Not looking for anything in particular, this is a special case. if search.searchee == "" { return Ok((String::from(&search.original), all_res)); } //Remote search... is annoying if !search.local_search { last_found = last_found.replace("./", ""); //println!("{:?}", format!("{}{}", search.command, last_found)); if nbr_found < 2 { return Ok((format!("{}{}{}", search.command, search.search_path.display(), last_found), String::new())); } else { return Ok((format!("{}{}", search.command, last_found), all_res)); } } //last_found = last_found.replace("./", ""); //Otherwise... Handling the return as gracefully as I can let mut return_val = String::new(); if all_res.len() > 0 { let ret_string = replace_last_word(&String::from(&search.original), last_found); *&mut return_val = format!("{}", ret_string); } else { *&mut return_val = format!("{}", &search.original); } return Ok((return_val, all_res)); } //Matches on space or slash fn replace_last_word(sentence: &String, replacement: String) -> String { let lastword = sentence.split(|c| c == ' ' || c == '/').last().unwrap_or(""); let mut work_sent = sentence.clone(); work_sent.truncate(sentence.len() - lastword.len()); work_sent.push_str(&replacement); return work_sent; } //fn return_last_word(sentence: &String) -> String { // let lastword = sentence.split(|c| c == ' ' || c == '/').last().unwrap_or(""); // return String::from(lastword); //} fn autocomplete_cmd(input: &String, search: &Search) -> (String, String) { let found_bins = find_bin(input); if found_bins.len() == 1 { let aaa = found_bins.clone(); let bbb = &aaa[0] .iter() .last() .unwrap(); let res_string = format!("{}{}", search.command, String::from(bbb.to_str().unwrap())); let res_list = res_string.clone(); return (res_string, res_list); } else if found_bins.len() == 0 { let list = String::from("Nothing adequate."); let res = String::from(""); return (res, list); } else { let mut all_res = String::new(); let mut counter = 0; let mut bins = Vec::new(); for path in found_bins { //Show 40 results max if counter < 40 { let buff = String::from(path .iter() .last() .unwrap() .to_str() .unwrap()); *&mut bins.push(buff.clone()); let res_line = format!("* {}\r\n", buff); all_res.push_str(&res_line); counter += 1; } else { all_res.push_str("[...]\r\n"); break; } } let longest = find_common_chars(bins); let ret_line = format!("{}", longest); return(ret_line, all_res); } } ///Takes a string and returns a Vector of PathBuf containing all matchings ///commands fn find_bin(command: &String) -> Vec { let re = format!("(?i)^{}.*", command); let re = Regex::new(&re).unwrap(); let mut binaries: Vec = which_re(re).unwrap().collect(); binaries.sort(); return binaries; } fn find_common_chars(mut strvec: Vec) -> String { let first_word = strvec[0].clone(); strvec.remove(0); let mut answer = String::new(); let mut counter: usize = 0; let mut still_ok = true; for letter in first_word.chars() { for word in &strvec { if *&still_ok == true { match word.chars().nth(counter) { Some(l) => { if l != letter { *&mut still_ok = false; } }, None => { *&mut still_ok = false; }, }; } } if still_ok == true { *&mut answer.push(letter); *&mut counter += 1; } } //answer.pop(); return answer; }