#![feature(iter_intersperse)] #![feature(trait_alias)] use std::{ fs, io::{BufRead, Write}, path::Path, process::{self}, }; fn main() { let stdin = std::io::stdin(); let mut in_lock = stdin.lock(); let stdout = std::io::stdout(); let mut out_lock = stdout.lock(); let commands = &Command::commands(); println!("Loaded {} commands", commands.len()); loop { let mut buf = String::new(); let ps1 = "> ".as_bytes(); out_lock.write(ps1).unwrap(); out_lock.flush().unwrap(); in_lock .read_line(&mut buf) .expect("Failed to read in to buffer."); let mut args: Param = &buf.trim().split(' ').collect::>(); let mut options = commands; loop { match args.get(0) { Some(cmd_arg) => { // Try to find a subcommand to run match Command::choose(cmd_arg, options) { Some(choice) => { if choice.sub_commands.is_empty() { // No sub commands left to choose from, just pass the input choice.call(&args); break; } else { args = &args[1..]; options = &choice.sub_commands; // a sort of last chance before running into hella edgecases if args.is_empty() { choice.call(args); break; } } } // When no good choice presents it's self None => break, } } None => todo!("Couldn't get index 0 of the args"), } } } } type Param<'a, 'b> = &'b [&'a str]; trait Run = Fn(Param); type Cmd = Box; struct Command { name: String, sub_commands: Vec, cmd: Cmd, } impl Command { fn init(name: T, sub_commands: Vec, f: Cmd) -> Self where T: ToString, { Self { name: name.to_string(), sub_commands, cmd: f, } } fn call(&self, words: Param) { let x = &self.cmd; x(words) } fn choose<'a>(text: &str, choices: &'a Vec) -> Option<&'a Self> { // TODO there seems to be duplicates in here? // See what command the user is trying to call // iter thru all the commands let mut ranked = choices.iter().fold(Vec::new(), |mut v, cmd| { // count how many chars match from the command to the word let matches = text .chars() .zip(cmd.name.chars()) .fold(0, |mut f, (word, cmd)| { if word == cmd { f += 1; } f }); v.push((cmd, matches)); v }); // sort by most matches ranked.sort_by(|a, b| b.1.cmp(&a.1)); // Don't select commands just because they are first. The command also has to have a // confidence of at least 1 if let Some(first) = ranked.first() { if first.1 < 1 { return None; } } // TODO test when 1 100% match exists if ranked.len() > 1 { // If the top two command choices are ranked the same, we don't know which to choose! let top = ranked[0]; let second = ranked[1]; if top.1 == second.1 { println!("Ambigous command '{text}'. Could be any of: "); let iter: Vec<&(&Command, usize)> = ranked .iter() // We only care about 100% matches .filter(|f| f.1 >= text.len()) .collect(); let max_display = if 5 < iter.len() {5} else {iter.len()}; iter[..max_display].iter().for_each(|matching| { println!("\t- {}", matching.0.name); }); if iter.len() > max_display { println!("\tOmitting {} other entries.", iter.len() - max_display); } return None; } } if let Some((cmd, _)) = ranked.first() { return Some(cmd); } None } fn commands() -> Vec { let path = Path::new("/bin"); let res = fs::read_dir(path).expect("Failed to read /bin"); // load all the programs from /bin let bin = res.fold(Vec::new(), |mut v, contents| { if let Ok(cmd) = contents { if let Ok(t) = cmd.file_type() { if t.is_file() { // v.push(cmd.path()); let cmd = Command::init( cmd.file_name().into_string().unwrap(), vec![], Box::new(move |args| { let name = cmd.path().to_str().unwrap().to_owned(); let out = process::Command::new(name) .args(args) .output() .expect("Failed to call external program"); // FIXME not printing to out let y = out.stdout.iter().map(|b| *b as char).collect::(); // TODO load into env // let status = out.status.code(); println!("{y}"); }), ); v.push(cmd); } }; }; v }); // load custom programs let mut commands = vec![ Command::init( "exit", vec![], Box::new(|_| { std::process::exit(0); }), ), Command::init( "custom_echo", vec![], Box::new(|args| { let msg = &args[1..] .iter() .map(|s| s.to_owned()) .intersperse(" ") .collect::(); println!("{msg}"); }), ), Command::init( "random", vec![ Command::init( "number", vec![], Box::new(|_| { // how do you know it's not random? println!("8"); }), ), Command::init( "string", vec![], Box::new(|_| { // how do you know it's not random? println!("hello world"); }), ), ], Box::new(|args| { println!("random's debug: {:?}", args); }), ), ]; // combine with a move commands.extend(bin); commands } }