613 lines
23 KiB
Rust
613 lines
23 KiB
Rust
#[allow(unused_must_use)]
|
|
pub mod shell {
|
|
use std::io::Stdout;
|
|
use std::process::Command;
|
|
use std::io::stdin;
|
|
use std::io::stdout;
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
use std::env;
|
|
use std::str;
|
|
use std::fs::File;
|
|
use std::process::Stdio;
|
|
use termion::event::Key;
|
|
use termion::input::TermRead;
|
|
use termion::raw::IntoRawMode;
|
|
use termion::raw::RawTerminal;
|
|
use termion::cursor;
|
|
use termion;
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
use std::collections::HashMap;
|
|
|
|
mod history;
|
|
use crate::shell::history::*;
|
|
|
|
mod autocomplete;
|
|
use crate::shell::autocomplete::*;
|
|
|
|
mod config;
|
|
use crate::shell::config::*;
|
|
|
|
//used for home directory
|
|
extern crate dirs;
|
|
extern crate shell_words;
|
|
extern crate ctrlc;
|
|
|
|
|
|
fn process_line(input: &str) -> Result<String, shell_words::ParseError> {
|
|
let mut commands = input.trim().split("|").peekable();
|
|
let mut previous_command = None;
|
|
let mut resultat = String::new();
|
|
|
|
while let Some(command) = commands.next() {
|
|
let parts = shell_words::split(&command.trim())?;
|
|
let command = parts[0].as_str();
|
|
let args = Vec::from(&parts[1..]);
|
|
|
|
match command {
|
|
"cd" => {
|
|
let homedir = dirs::home_dir().unwrap().into_os_string().into_string().unwrap();
|
|
//let new_dir = args.peekable().peek().map_or(homedir.as_str(), |x| *x);
|
|
let mut new_dir = String::new();
|
|
if args.len() > 0 {
|
|
*&mut new_dir = args.join(" ");
|
|
} else {
|
|
*&mut new_dir = String::from(homedir.as_str());
|
|
}
|
|
|
|
let root = Path::new(&new_dir);
|
|
if let Err(e) = env::set_current_dir(&root) {
|
|
eprintln!(" Err: {}", e);
|
|
} else {
|
|
println!("");
|
|
}
|
|
previous_command = None;
|
|
},
|
|
"history" => {
|
|
//let stdout = Stdio::inherit();
|
|
let res = get_history();
|
|
match res {
|
|
Ok(r) => {
|
|
let filepath = dirs::home_dir().unwrap().join(".history.sqish");
|
|
let file = File::open(filepath).unwrap();
|
|
*&mut previous_command = Some(Stdio::from(file));
|
|
if !commands.peek().is_some() {
|
|
print!("{}", r);
|
|
}
|
|
},
|
|
Err(e) => eprintln!(" Err reading history: {}", e),
|
|
}
|
|
},
|
|
command => {
|
|
if commands.peek().is_some() {
|
|
let stdin = match previous_command {
|
|
None => Stdio::inherit(),
|
|
Some(o) => o,
|
|
};
|
|
let stdout = Stdio::piped();
|
|
|
|
let output = Command::new(command)
|
|
.args(args)
|
|
.stdout(stdout)
|
|
.stdin(stdin)
|
|
.spawn();
|
|
|
|
match output {
|
|
Ok(o) => previous_command = Some(Stdio::from(o.stdout.unwrap())),
|
|
Err(e) => {
|
|
previous_command = None;
|
|
eprintln!(" Err: {}", e);
|
|
},
|
|
};
|
|
} else {
|
|
let stdin = match previous_command {
|
|
None => Stdio::inherit(),
|
|
Some(o) => o,
|
|
};
|
|
|
|
print!("\r\n");
|
|
|
|
let child = match Command::new(command)
|
|
.args(args)
|
|
.stdin(stdin)
|
|
.spawn() {
|
|
Ok(h) => h,
|
|
Err(_) => {
|
|
let err_string = format!("Not found : {}", command);
|
|
return Ok(String::from(err_string));
|
|
},
|
|
};
|
|
|
|
let output = child
|
|
.wait_with_output()
|
|
.expect("Failed to wait on child");
|
|
let status = output.status
|
|
.code()
|
|
.unwrap_or(255)
|
|
.to_string();
|
|
previous_command = None;
|
|
let format_res = format!("{}",
|
|
status);
|
|
let _ = &mut resultat.push_str(&format_res);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
return Ok(resultat);
|
|
}
|
|
|
|
fn replace_signs(line: &String) -> String {
|
|
let mut ayo = String::from(line);
|
|
if ayo.contains('~') {
|
|
let homedir = dirs::home_dir().unwrap().into_os_string().into_string().unwrap();
|
|
let result = str::replace(&ayo, "~", &homedir);
|
|
ayo = String::from(result);
|
|
}
|
|
|
|
if ayo.contains("!!") {
|
|
let prev_comm = get_hist_from_number(&get_history_number().unwrap()).unwrap();
|
|
let result = str::replace(&ayo, "!!", &prev_comm);
|
|
ayo = String::from(result);
|
|
}
|
|
|
|
return ayo;
|
|
}
|
|
|
|
fn handle_input(line: &str) -> String {
|
|
//history callback
|
|
if (line.len() > 0) && (line.starts_with('!')) {
|
|
let command_found = treat_history_callback(line);
|
|
match command_found {
|
|
Some(c) => {
|
|
write_to_history(&c);
|
|
let outp = process_line(&c).unwrap();
|
|
return outp;
|
|
},
|
|
None => return String::from(" "),
|
|
};
|
|
//Regular command
|
|
} else if line.len() > 0 {
|
|
write_to_history(line);
|
|
return process_line(line).unwrap();
|
|
//Nothing
|
|
} else {
|
|
//Command was empty, new line
|
|
return String::from("");
|
|
}
|
|
}
|
|
|
|
fn delete_current_cmd(current_cmd: String) -> String {
|
|
//Deletes currently written command
|
|
|
|
let mut stdout = stdout().into_raw_mode().unwrap();
|
|
//write!(stdout, "{}", cursor::Save);
|
|
for _c in current_cmd.graphemes(true) {
|
|
write!(stdout, "\x1b[D").unwrap();
|
|
write!(stdout, "\x1b[K").unwrap();
|
|
}
|
|
stdout.flush();
|
|
//write!(stdout, "{}", cursor::Restore);
|
|
return String::new();
|
|
}
|
|
|
|
fn get_cmd_curs_pos(command: &String) -> (usize, usize) {
|
|
let mut max_pos: usize = 0;
|
|
let mut current_pos: usize = 0;
|
|
|
|
for _c in command.graphemes(true) {
|
|
max_pos += 1;
|
|
current_pos += 1;
|
|
}
|
|
|
|
return (max_pos, current_pos);
|
|
}
|
|
|
|
fn clear_line(max_pos: &usize, current_pos: &usize) {
|
|
//clears currently written command from the screen
|
|
//...NOT from the variable !
|
|
let mut stdout = stdout().into_raw_mode().unwrap();
|
|
let chars_left = max_pos - current_pos;
|
|
if current_pos < max_pos {
|
|
write!(stdout, "{}", cursor::Right(chars_left as u16));
|
|
}
|
|
for _i in 0..*max_pos {
|
|
write!(stdout, "{}", cursor::Left(1));
|
|
write!(stdout, "\x1b[K").unwrap();
|
|
}
|
|
//write!(stdout, "{}", cursor::Left(1));
|
|
//write!(stdout, "\x1b[K").unwrap();
|
|
stdout.flush();
|
|
}
|
|
|
|
fn transform_alias(conf: &SqishConf, full_command: String) -> String {
|
|
let binding = full_command.clone();
|
|
let mut pieces = binding.split_whitespace()
|
|
.collect::<Vec<&str>>();
|
|
let command_temp = match pieces.first() {
|
|
Some(c) => c,
|
|
None => {
|
|
return full_command;
|
|
},
|
|
};
|
|
let command = String::from(*command_temp);
|
|
|
|
pieces.remove(0);
|
|
let arg = match pieces.len() {
|
|
0 => String::from(""),
|
|
_ => format!(" {}", pieces.join(" ")),
|
|
};
|
|
|
|
if conf.aliases.contains_key(&command) && command.chars().count() > 0 {
|
|
let ret_string = format!("{}{}", conf.aliases[&command], arg);
|
|
return ret_string;
|
|
} else {
|
|
return full_command;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn run_cmd(mycommand: &mut String,
|
|
current_number: &mut i32,
|
|
conf: &mut SqishConf,
|
|
stdout: &mut RawTerminal<Stdout>) {
|
|
if (mycommand != &String::from("")) && (mycommand != &String::from("exit")) {
|
|
let comm = replace_signs(&mycommand);
|
|
RawTerminal::suspend_raw_mode(&stdout);
|
|
let res = handle_input(&comm);
|
|
RawTerminal::activate_raw_mode(&stdout);
|
|
mycommand.clear();
|
|
for line in res.split("\r\n") {
|
|
if line != "" && line.starts_with('N'){
|
|
write!(stdout, "{}\r\n", line);
|
|
}
|
|
}
|
|
conf.update_prompt(get_curr_history_number());
|
|
write!(stdout, "{}", conf.promptline).unwrap();
|
|
stdout.flush();
|
|
*current_number += 1;
|
|
} else if mycommand == &String::from("exit") {
|
|
write!(stdout, "\r\nThanks for using Sqish!\r\nSayonara \r\n");
|
|
RawTerminal::suspend_raw_mode(&stdout);
|
|
std::process::exit(0);
|
|
} else {
|
|
conf.update_prompt(get_curr_history_number());
|
|
write!(stdout, "\r\n{}", conf.promptline).unwrap();
|
|
}
|
|
stdout.flush();
|
|
|
|
}
|
|
|
|
fn write_letter(mycommand: &mut String, current_pos: &mut usize, max_pos: &mut usize, c: char) {
|
|
let mut stdout = stdout().into_raw_mode().unwrap();
|
|
if *current_pos == *max_pos {
|
|
mycommand.push(c);
|
|
*current_pos += 1;
|
|
*max_pos += 1;
|
|
write!(stdout, "{}", c).unwrap();
|
|
} else if *current_pos < *max_pos {
|
|
//Inserting a char inside the command...
|
|
write!(stdout, "{}", cursor::Save);
|
|
mycommand.insert(*current_pos, c);
|
|
let command_piece = &mycommand.as_str()[*current_pos..];
|
|
write!(stdout, "{}", command_piece);
|
|
stdout.flush();
|
|
write!(stdout, "{}", cursor::Restore);
|
|
write!(stdout, "{}", cursor::Right(1));
|
|
*max_pos += 1;
|
|
*current_pos += 1;
|
|
}
|
|
}
|
|
|
|
fn append_prev_arg(mycommand: &mut String, current_pos: &mut usize, max_pos: &mut usize) {
|
|
let prev_arg = get_prev_arg();
|
|
match prev_arg {
|
|
Some(a) => {
|
|
for letter in a.chars() {
|
|
write_letter(mycommand, current_pos, max_pos, letter);
|
|
}
|
|
},
|
|
None => (),
|
|
};
|
|
}
|
|
|
|
fn find_next_space(current_pos: &usize, max_pos: &usize, command: &String) -> usize {
|
|
let mut steps: usize = 0;
|
|
if *current_pos == *max_pos { return steps; }
|
|
for letter in command.chars().skip(*current_pos) {
|
|
//println!("L : {:?}, S : {:?} C: {:?} M {:?}", letter, steps, current_pos, max_pos);
|
|
//Skip if we start on a space
|
|
if steps == 0 && (letter == ' ' || letter == ',' || letter == ';') {
|
|
continue;
|
|
} else if letter != ' ' && *current_pos + steps < *max_pos {
|
|
steps += 1;
|
|
} else {
|
|
return steps;
|
|
}
|
|
}
|
|
let max: usize = (0 + max_pos) - current_pos;
|
|
return max;
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn run_raw() {
|
|
let stdin = stdin();
|
|
let mut stdout = stdout().into_raw_mode().unwrap();
|
|
//RawTerminal::suspend_raw_mode(&stdout);
|
|
let mut mycommand = String::new();
|
|
|
|
//Used in conjunction with Up arrow to go back in history
|
|
//Resetted by pressing Enter
|
|
let mut current_number = get_curr_history_number();
|
|
|
|
//Used to track the location of the cursor inside the command
|
|
let mut current_pos: usize = 0;
|
|
let mut max_pos: usize = 0;
|
|
|
|
|
|
//Handle Ctrl+C
|
|
ctrlc::set_handler(|| {
|
|
();
|
|
}).unwrap();
|
|
|
|
let mut conf = match SqishConf::from_sqishrc() {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
let conf = SqishConf {
|
|
promptline: String::from("!$HISTNUMBER$COLORGREEN_[$USER_@$HOSTNAME_]$COLORRESET_ "),
|
|
promptline_base: String::from("!$HISTNUMBER_$COLORGREEN_[$USER_@$HOSTNAME_]$COLORRESET_ "),
|
|
aliases: HashMap::new(),
|
|
hotkeys: HashMap::new(),
|
|
init: String::new(),
|
|
};
|
|
let ret_line = format!("Could not build conf, got {}\
|
|
\r\nUsing default promptline", e);
|
|
write!(stdout, "{}", ret_line);
|
|
conf
|
|
},
|
|
};
|
|
&conf.update_prompt(get_curr_history_number());
|
|
|
|
//Initializing
|
|
write!(stdout, "\r\n ---Sqish initializing--- \r\n{}", conf.promptline);
|
|
stdout.flush();
|
|
|
|
if conf.init != String::from("") {
|
|
run_cmd(&mut String::from(&conf.init), &mut current_number, &mut conf, &mut stdout);
|
|
}
|
|
|
|
for c in stdin.keys() {
|
|
let k = c.unwrap();
|
|
match k {
|
|
Key::Char('\t') => {
|
|
//Do NOT search on an empty command
|
|
if *&mycommand.len() == 0 {
|
|
continue;
|
|
}
|
|
//Search
|
|
*&mut mycommand = replace_signs(&mycommand);
|
|
let (res, list) = Search::build(&mycommand).unwrap().autocomplete();
|
|
if list.len() > 0 { write!(stdout, "{}", list); }
|
|
mycommand.clear();
|
|
mycommand.push_str(&res);
|
|
max_pos = res.len();
|
|
current_pos = max_pos;
|
|
write!(stdout, "\r\n{}{}", conf.promptline, res);
|
|
stdout.flush();
|
|
}
|
|
Key::Char('\n') => {
|
|
current_number = get_curr_history_number();
|
|
current_pos = 0;
|
|
max_pos = 0;
|
|
mycommand = transform_alias(&conf, mycommand);
|
|
run_cmd(&mut mycommand,
|
|
&mut current_number,
|
|
&mut conf,
|
|
&mut stdout);
|
|
},
|
|
|
|
Key::Ctrl('q'|'d') => {
|
|
RawTerminal::suspend_raw_mode(&stdout);
|
|
writeln!(stdout, "\r\nThanks for using Sqish !\r\nSayonara \r\n");
|
|
break;
|
|
},
|
|
|
|
Key::Ctrl('c') => {
|
|
current_pos = 0;
|
|
max_pos = 0;
|
|
current_number = get_curr_history_number();
|
|
mycommand.clear();
|
|
write!(stdout, "\r\n--CANCEL--\r\n");
|
|
write!(stdout, "{}", conf.promptline);
|
|
},
|
|
|
|
Key::Ctrl('u') => {
|
|
clear_line(&max_pos, ¤t_pos);
|
|
max_pos = 0;
|
|
current_pos = 0;
|
|
mycommand.clear();
|
|
},
|
|
|
|
Key::Ctrl('a') => {
|
|
if current_pos != 0 {
|
|
write!(stdout, "{}", cursor::Left(current_pos as u16));
|
|
current_pos = 0;
|
|
}
|
|
},
|
|
|
|
Key::Ctrl('e') => {
|
|
if current_pos != max_pos {
|
|
let chars_left = max_pos - current_pos;
|
|
write!(stdout, "{}", cursor::Right(chars_left as u16));
|
|
current_pos = max_pos;
|
|
}
|
|
},
|
|
|
|
Key::Char(c) => {
|
|
write_letter(&mut mycommand, &mut current_pos, &mut max_pos, c);
|
|
}
|
|
|
|
Key::Delete => {
|
|
if mycommand.chars().count() == 1
|
|
&& current_pos == 0 {
|
|
clear_line(&max_pos, ¤t_pos);
|
|
mycommand.clear();
|
|
max_pos = 0;
|
|
current_pos = 0;
|
|
} else if current_pos < max_pos {
|
|
mycommand.remove(current_pos);
|
|
if current_pos > 0 {
|
|
current_pos -= 1;
|
|
write!(stdout, "{}", cursor::Left(1));
|
|
}
|
|
max_pos -= 1;
|
|
write!(stdout, "{}", cursor::Save);
|
|
clear_line(&max_pos, ¤t_pos);
|
|
write!(stdout, "{}", mycommand);
|
|
write!(stdout, "{}", cursor::Restore);
|
|
stdout.flush();
|
|
}
|
|
},
|
|
|
|
Key::Backspace => {
|
|
if current_pos == 0 {
|
|
continue;
|
|
}
|
|
if mycommand.chars().count() == 1 {
|
|
clear_line(&max_pos, ¤t_pos);
|
|
mycommand.clear();
|
|
current_pos = 0;
|
|
max_pos = 0;
|
|
} else {
|
|
current_pos -= 1;
|
|
max_pos -= 1;
|
|
mycommand.remove(current_pos);
|
|
write!(stdout, "{}", cursor::Left(1));
|
|
write!(stdout, "{}", cursor::Save);
|
|
clear_line(&max_pos, ¤t_pos);
|
|
write!(stdout, "{}", mycommand);
|
|
write!(stdout, "{}", cursor::Restore);
|
|
stdout.flush();
|
|
}
|
|
},
|
|
|
|
Key::Up => {
|
|
if current_number > 2 {
|
|
mycommand = delete_current_cmd(mycommand);
|
|
current_number -= 1;
|
|
let lastcmd = match get_hist_from_number(¤t_number) {
|
|
Some(c) => c,
|
|
None => continue
|
|
};
|
|
mycommand = lastcmd.trim().to_string();
|
|
(max_pos, current_pos) = get_cmd_curs_pos(&mycommand);
|
|
write!(stdout, "{}", mycommand);
|
|
}
|
|
},
|
|
|
|
Key::Down => {
|
|
if current_number < get_curr_history_number() {
|
|
mycommand = delete_current_cmd(mycommand);
|
|
current_number += 1;
|
|
let lastcmd = match get_hist_from_number(¤t_number) {
|
|
Some(c) => c,
|
|
None => continue
|
|
};
|
|
mycommand = lastcmd.trim().to_string();
|
|
(max_pos, current_pos) = get_cmd_curs_pos(&mycommand);
|
|
write!(stdout, "{}", mycommand);
|
|
}
|
|
},
|
|
|
|
Key::Ctrl('x') => {
|
|
clear_line(&max_pos, ¤t_pos);
|
|
mycommand.clear();
|
|
current_pos = 0;
|
|
max_pos = 0;
|
|
}
|
|
|
|
Key::Left => {
|
|
if current_pos > 0 {
|
|
current_pos -= 1;
|
|
write!(stdout, "{}", cursor::Left(1));
|
|
}
|
|
},
|
|
|
|
Key::Right => {
|
|
if current_pos < max_pos {
|
|
print!("{}", cursor::Right(1));
|
|
current_pos += 1;
|
|
}
|
|
},
|
|
|
|
Key::Ctrl('p') => {
|
|
let steps = find_next_space(¤t_pos, &max_pos, &mycommand);
|
|
//println!("steps: {:?} cur {:?} max {:?}", steps, current_pos, max_pos);
|
|
if steps > 0 {
|
|
print!("{}", cursor::Right(steps as u16));
|
|
current_pos += steps;
|
|
}
|
|
},
|
|
|
|
Key::Ctrl('o') => {
|
|
let command_rev = mycommand.chars().rev().collect::<String>();
|
|
let curr_rev = max_pos - current_pos + 1;
|
|
let steps = find_next_space(&curr_rev, &max_pos, &command_rev);
|
|
if steps > 0 {
|
|
print!("{}", cursor::Left(steps as u16));
|
|
current_pos -= steps;
|
|
}
|
|
},
|
|
|
|
Key::Alt(x) => {
|
|
match x {
|
|
'a'..='z' => {
|
|
let x = String::from(x);
|
|
if conf.hotkeys.contains_key(&x) {
|
|
let hotcmd = &conf.hotkeys[&x].clone();
|
|
for c in hotcmd.chars() {
|
|
if c != '\n' {
|
|
write_letter(&mut mycommand,
|
|
&mut current_pos,
|
|
&mut max_pos,
|
|
c);
|
|
} else {
|
|
current_pos = 0;
|
|
max_pos = 0;
|
|
mycommand = transform_alias(&conf, mycommand);
|
|
run_cmd(&mut mycommand,
|
|
&mut current_number,
|
|
&mut conf,
|
|
&mut stdout);
|
|
}
|
|
|
|
}
|
|
}
|
|
},
|
|
'.' => {
|
|
append_prev_arg(&mut mycommand, &mut current_pos, &mut max_pos);
|
|
},
|
|
'!' => {
|
|
let mut cmd = format!("\r\n-- HOTKEYS --> \r\n{:?}\r\n", conf.hotkeys);
|
|
cmd = format!("{}\r\n-- ALIASES -->\r\n{:?}\r\n", cmd, conf.aliases);
|
|
cmd = cmd.replace(",", "\r\n");
|
|
cmd = cmd.replace('"', "");
|
|
write!(stdout, "{}", cmd);
|
|
},
|
|
|
|
_ => (),
|
|
};
|
|
},
|
|
|
|
_ => (),
|
|
};
|
|
|
|
stdout.flush().unwrap();
|
|
}
|
|
|
|
}
|
|
}
|