sqish/src/lib.rs
2023-03-01 17:00:43 +01:00

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();
}
}
}