642 lines
24 KiB
Rust
642 lines
24 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, Stdin};
|
|
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 change_dir(args: &Vec<String>, previous_command: &mut Option<Stdio>) {
|
|
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;
|
|
}
|
|
|
|
fn print_hist(previous_command: &mut Option<Stdio>, command_next: bool) {
|
|
//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();
|
|
*previous_command = Some(Stdio::from(file));
|
|
//if !commands.peek().is_some() {
|
|
if !command_next {
|
|
print!("{}", r);
|
|
}
|
|
},
|
|
Err(e) => eprintln!(" Err reading history: {}", e),
|
|
}
|
|
}
|
|
|
|
fn process_line(input: &str) -> String {
|
|
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 = match shell_words::split(&command.trim()) {
|
|
Ok(w) => w,
|
|
Err(e) => { return format!("Error parsing the command : {:?}", e); },
|
|
};
|
|
let command = parts[0].as_str();
|
|
let args = Vec::from(&parts[1..]);
|
|
|
|
match command {
|
|
"cd" => {
|
|
change_dir(&args, &mut previous_command);
|
|
},
|
|
"history" => {
|
|
let next = commands.peek().is_some();
|
|
print_hist(&mut previous_command, next);
|
|
},
|
|
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 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 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);
|
|
return outp;
|
|
},
|
|
None => return String::from(" "),
|
|
};
|
|
//Regular command
|
|
} else if line.len() > 0 {
|
|
write_to_history(line);
|
|
return process_line(line);
|
|
//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;
|
|
}
|
|
|
|
fn handle_conf(stdout: &mut Stdout) -> SqishConf {
|
|
let 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
|
|
},
|
|
};
|
|
return conf;
|
|
}
|
|
|
|
fn set_ctrlc() {
|
|
ctrlc::set_handler(|| {
|
|
();
|
|
}).unwrap();
|
|
}
|
|
|
|
struct TermElements {
|
|
//Stores currently typed command
|
|
command: String,
|
|
//Current cursor position in the line
|
|
cur_pos: usize,
|
|
//Max position of cursor in the line
|
|
max_pos: usize,
|
|
//In / out
|
|
stdin: Stdin,
|
|
stdout: RawTerminal<Stdout>,
|
|
//Current history number
|
|
current_number: i32,
|
|
//Configuration
|
|
conf: SqishConf,
|
|
}
|
|
|
|
pub fn run_raw() {
|
|
|
|
let mut elems = TermElements {
|
|
command: String::new(),
|
|
cur_pos: 0,
|
|
max_pos: 0,
|
|
stdin: stdin(),
|
|
stdout: stdout().into_raw_mode().unwrap(),
|
|
current_number: get_curr_history_number(),
|
|
conf: handle_conf(&mut stdout().into_raw_mode().unwrap()),
|
|
};
|
|
|
|
//Handle Ctrl+C
|
|
set_ctrlc();
|
|
|
|
&elems.conf.update_prompt(get_curr_history_number());
|
|
|
|
//Initializing
|
|
write!(elems.stdout, "\r\n ---Sqish initializing--- \r\n{}", elems.conf.promptline);
|
|
elems.stdout.flush();
|
|
|
|
if elems.conf.init != String::from("") {
|
|
run_cmd(&mut String::from(&elems.conf.init), &mut elems.current_number, &mut elems.conf, &mut elems.stdout);
|
|
}
|
|
|
|
for c in elems.stdin.keys() {
|
|
let k = c.unwrap();
|
|
match k {
|
|
Key::Char('\t') => {
|
|
//Do NOT search on an empty command
|
|
if *&elems.command.len() == 0 {
|
|
continue;
|
|
}
|
|
//Search
|
|
*&mut elems.command = replace_signs(&elems.command);
|
|
let (res, list) = Search::build(&elems.command).unwrap().autocomplete();
|
|
if list.len() > 1 { write!(elems.stdout, "\r\n{}", list); }
|
|
elems.command.clear();
|
|
elems.command.push_str(&res);
|
|
elems.max_pos = res.len();
|
|
elems.cur_pos = elems.max_pos;
|
|
write!(elems.stdout, "\r\n{}{}", elems.conf.promptline, res);
|
|
elems.stdout.flush();
|
|
}
|
|
Key::Char('\n') => {
|
|
elems.current_number = get_curr_history_number();
|
|
elems.cur_pos = 0;
|
|
elems.max_pos = 0;
|
|
elems.command = transform_alias(&elems.conf, elems.command);
|
|
run_cmd(&mut elems.command,
|
|
&mut elems.current_number,
|
|
&mut elems.conf,
|
|
&mut elems.stdout);
|
|
},
|
|
|
|
Key::Ctrl('q'|'d') => {
|
|
RawTerminal::suspend_raw_mode(&elems.stdout);
|
|
writeln!(elems.stdout, "\r\nThanks for using Sqish !\r\nSayonara \r\n");
|
|
break;
|
|
},
|
|
|
|
Key::Ctrl('c') => {
|
|
elems.cur_pos = 0;
|
|
elems.max_pos = 0;
|
|
elems.current_number = get_curr_history_number();
|
|
elems.command.clear();
|
|
write!(elems.stdout, "\r\n--CANCEL--\r\n");
|
|
write!(elems.stdout, "{}", elems.conf.promptline);
|
|
},
|
|
|
|
Key::Ctrl('u') => {
|
|
clear_line(&elems.max_pos, &elems.cur_pos);
|
|
elems.max_pos = 0;
|
|
elems.cur_pos = 0;
|
|
elems.command.clear();
|
|
},
|
|
|
|
Key::Ctrl('a') => {
|
|
if elems.cur_pos != 0 {
|
|
write!(elems.stdout, "{}", cursor::Left(elems.cur_pos as u16));
|
|
elems.cur_pos = 0;
|
|
}
|
|
},
|
|
|
|
Key::Ctrl('e') => {
|
|
if elems.cur_pos != elems.max_pos {
|
|
let chars_left = elems.max_pos - elems.cur_pos;
|
|
write!(elems.stdout, "{}", cursor::Right(chars_left as u16));
|
|
elems.cur_pos = elems.max_pos;
|
|
}
|
|
},
|
|
|
|
Key::Char(c) => {
|
|
write_letter(&mut elems.command, &mut elems.cur_pos, &mut elems.max_pos, c);
|
|
}
|
|
|
|
Key::Delete => {
|
|
if elems.command.chars().count() == 1
|
|
&& elems.cur_pos == 0 {
|
|
clear_line(&elems.max_pos, &elems.cur_pos);
|
|
elems.command.clear();
|
|
elems.max_pos = 0;
|
|
elems.cur_pos = 0;
|
|
} else if elems.cur_pos < elems.max_pos {
|
|
elems.command.remove(elems.cur_pos);
|
|
if elems.cur_pos > 0 {
|
|
elems.cur_pos -= 1;
|
|
write!(elems.stdout, "{}", cursor::Left(1));
|
|
}
|
|
elems.max_pos -= 1;
|
|
write!(elems.stdout, "{}", cursor::Save);
|
|
clear_line(&elems.max_pos, &elems.cur_pos);
|
|
write!(elems.stdout, "{}", elems.command);
|
|
write!(elems.stdout, "{}", cursor::Restore);
|
|
elems.stdout.flush();
|
|
}
|
|
},
|
|
|
|
Key::Backspace => {
|
|
if elems.cur_pos == 0 {
|
|
continue;
|
|
}
|
|
if elems.command.chars().count() == 1 {
|
|
clear_line(&elems.max_pos, &elems.cur_pos);
|
|
elems.command.clear();
|
|
elems.cur_pos = 0;
|
|
elems.max_pos = 0;
|
|
} else {
|
|
elems.cur_pos -= 1;
|
|
elems.max_pos -= 1;
|
|
elems.command.remove(elems.cur_pos);
|
|
write!(elems.stdout, "{}", cursor::Left(1));
|
|
write!(elems.stdout, "{}", cursor::Save);
|
|
clear_line(&elems.max_pos, &elems.cur_pos);
|
|
write!(elems.stdout, "{}", elems.command);
|
|
write!(elems.stdout, "{}", cursor::Restore);
|
|
elems.stdout.flush();
|
|
}
|
|
},
|
|
|
|
Key::Up => {
|
|
if elems.current_number > 2 {
|
|
elems.command = delete_current_cmd(elems.command);
|
|
elems.current_number -= 1;
|
|
let lastcmd = match get_hist_from_number(&elems.current_number) {
|
|
Some(c) => c,
|
|
None => continue
|
|
};
|
|
elems.command = lastcmd.trim().to_string();
|
|
(elems.max_pos, elems.cur_pos) = get_cmd_curs_pos(&elems.command);
|
|
write!(elems.stdout, "{}", elems.command);
|
|
}
|
|
},
|
|
|
|
Key::Down => {
|
|
if elems.current_number < get_curr_history_number() {
|
|
elems.command = delete_current_cmd(elems.command);
|
|
elems.current_number += 1;
|
|
let lastcmd = match get_hist_from_number(&elems.current_number) {
|
|
Some(c) => c,
|
|
None => continue
|
|
};
|
|
elems.command = lastcmd.trim().to_string();
|
|
(elems.max_pos, elems.cur_pos) = get_cmd_curs_pos(&elems.command);
|
|
write!(elems.stdout, "{}", elems.command);
|
|
}
|
|
},
|
|
|
|
Key::Left => {
|
|
if elems.cur_pos > 0 {
|
|
elems.cur_pos -= 1;
|
|
write!(elems.stdout, "{}", cursor::Left(1));
|
|
}
|
|
},
|
|
|
|
Key::Right => {
|
|
if elems.cur_pos < elems.max_pos {
|
|
print!("{}", cursor::Right(1));
|
|
elems.cur_pos += 1;
|
|
}
|
|
},
|
|
|
|
Key::Ctrl('l') => {
|
|
write!(elems.stdout, "{}{}", termion::clear::All, termion::cursor::Goto(1,1));
|
|
write!(elems.stdout, "{}{}", elems.conf.promptline, elems.command);
|
|
|
|
},
|
|
|
|
Key::Ctrl('p') => {
|
|
let steps = find_next_space(&elems.cur_pos, &elems.max_pos, &elems.command);
|
|
//println!("steps: {:?} cur {:?} max {:?}", steps, elems.cur_pos, elems.max_pos);
|
|
if steps > 0 {
|
|
print!("{}", cursor::Right(steps as u16));
|
|
elems.cur_pos += steps;
|
|
}
|
|
},
|
|
|
|
Key::Ctrl('o') => {
|
|
let command_rev = elems.command.chars().rev().collect::<String>();
|
|
let curr_rev = elems.max_pos - elems.cur_pos + 1;
|
|
let steps = find_next_space(&curr_rev, &elems.max_pos, &command_rev);
|
|
if steps > 0 {
|
|
print!("{}", cursor::Left(steps as u16));
|
|
elems.cur_pos -= steps;
|
|
}
|
|
},
|
|
|
|
Key::Alt(x) => {
|
|
match x {
|
|
'a'..='z' => {
|
|
let x = String::from(x);
|
|
if elems.conf.hotkeys.contains_key(&x) {
|
|
let hotcmd = &elems.conf.hotkeys[&x].clone();
|
|
for c in hotcmd.chars() {
|
|
if c != '\n' {
|
|
write_letter(&mut elems.command,
|
|
&mut elems.cur_pos,
|
|
&mut elems.max_pos,
|
|
c);
|
|
} else {
|
|
elems.cur_pos = 0;
|
|
elems.max_pos = 0;
|
|
elems.command = transform_alias(&elems.conf, elems.command);
|
|
run_cmd(&mut elems.command,
|
|
&mut elems.current_number,
|
|
&mut elems.conf,
|
|
&mut elems.stdout);
|
|
}
|
|
|
|
}
|
|
}
|
|
},
|
|
'.' => {
|
|
append_prev_arg(&mut elems.command, &mut elems.cur_pos, &mut elems.max_pos);
|
|
},
|
|
'!' => {
|
|
let mut cmd = format!("\r\n-- HOTKEYS --> \r\n{:?}\r\n", elems.conf.hotkeys);
|
|
cmd = format!("{}\r\n-- ALIASES -->\r\n{:?}\r\n", cmd, elems.conf.aliases);
|
|
cmd = cmd.replace(",", "\r\n");
|
|
cmd = cmd.replace('"', "");
|
|
write!(elems.stdout, "{}", cmd);
|
|
},
|
|
|
|
_ => (),
|
|
};
|
|
},
|
|
|
|
_ => (),
|
|
};
|
|
|
|
elems.stdout.flush().unwrap();
|
|
}
|
|
|
|
}
|
|
}
|