339 lines
11 KiB
Rust
339 lines
11 KiB
Rust
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<Search> {
|
|
|
|
let search_type = Self::discriminate_search_type(&input);
|
|
|
|
//Getting cmd and searched string
|
|
let mut words = input.split(' ').collect::<Vec<&str>>();
|
|
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::<Vec<&str>>();
|
|
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::<Vec<&str>>();
|
|
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::<Vec<&str>>();
|
|
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::<Vec<&str>>();
|
|
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<PathBuf> {
|
|
let re = format!("(?i)^{}.*", command);
|
|
let re = Regex::new(&re).unwrap();
|
|
let mut binaries: Vec<PathBuf> = which_re(re).unwrap().collect();
|
|
binaries.sort();
|
|
return binaries;
|
|
}
|
|
|
|
fn find_common_chars(mut strvec: Vec<String>) -> 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;
|
|
}
|
|
|
|
|
|
|