use clap_v3::{load_yaml, App}; use rustyline::completion::{Completer, Pair}; use rustyline::config::OutputStreamType; use rustyline::error::ReadlineError; use rustyline::highlight::Highlighter; use rustyline::hint::Hinter; use rustyline::validate::Validator; use rustyline::{CompletionType, Config, Context, EditMode, Editor, Helper}; use serde_yaml::Value; pub struct LineReader { editor: Editor, } impl LineReader { pub fn new() -> LineReader { // setup rustyline let config = Config::builder() .completion_type(CompletionType::List) .edit_mode(EditMode::Vi) .output_stream(OutputStreamType::Stdout) .build(); let clap_yaml: Value = serde_yaml::from_str(include_str!("clap.yaml")).unwrap(); let h = LineReaderHelper::new(&clap_yaml); let mut rl = Editor::with_config(config); rl.set_helper(Some(h)); LineReader { editor: rl } } pub fn read_line(&mut self, input: &mut String, prompt: &str) -> Option<()> { let readline = self.editor.readline(prompt); match readline { Ok(line) => { *input = line; Some(()) } Err(ReadlineError::Interrupted) => Some(()), Err(ReadlineError::Eof) => None, Err(_) => None, } } } pub struct LineReaderHelper { root_yaml: Value, } impl LineReaderHelper { fn new(base_yaml: &Value) -> LineReaderHelper { LineReaderHelper { root_yaml: base_yaml.clone(), } } } impl Helper for LineReaderHelper {} impl Hinter for LineReaderHelper {} impl Highlighter for LineReaderHelper {} impl Validator for LineReaderHelper {} impl Completer for LineReaderHelper { type Candidate = Pair; fn complete( &self, line: &str, pos: usize, _ctx: &Context, ) -> rustyline::Result<(usize, Vec)> { let line = format!("{}_", line); let cmd = CommandStructure::from(&self.root_yaml); let mut cmd = &cmd; // split line let mut tokens = line.split_whitespace(); let mut last_token = String::from(tokens.next_back().unwrap()); last_token.pop(); for tok in tokens { let next_cmd = cmd.get_child(tok); if next_cmd.is_none() { return Ok((pos, vec![])); } cmd = next_cmd.unwrap(); } let candidates: Vec = cmd .completions .to_vec() .into_iter() .filter(|x| x.starts_with(&last_token)) .collect(); Ok(( line.len() - last_token.len() - 1, candidates .iter() .map(|cmd| Pair { display: String::from(cmd), replacement: format!("{} ", cmd), }) .collect(), )) } } struct CommandStructure { name: String, // command name aliases: Vec, // possible aliases for name completions: Vec, // subcommand names children: Vec>, // list of commands (name should match a completion) } impl CommandStructure { fn new(name: &str) -> CommandStructure { CommandStructure { name: String::from(name), aliases: vec![], completions: vec![], children: vec![], } } fn get_child<'a>(&'a self, name_or_alias: &str) -> Option<&'a CommandStructure> { for cs in self.children.iter() { if cs.name == name_or_alias || cs.aliases.iter().any(|e| e == name_or_alias) { return Some(cs); } } None } } impl From<&serde_yaml::Value> for CommandStructure { fn from(value: &serde_yaml::Value) -> CommandStructure { let (name, value) = get_map(value); let mut cs = CommandStructure::new(name); cs.aliases = get_aliases(&value) .into_iter() .map(|x| String::from(x)) .collect(); cs.completions = get_sub_names(&value) .into_iter() .map(|x| String::from(x)) .collect(); let subcommands = value.get("subcommands"); if subcommands.is_none() { return cs; } let subcommands = subcommands.unwrap(); if let Value::Sequence(cmds) = subcommands { for cmd in cmds { cs.children.push(Box::new(CommandStructure::from(cmd))); } } cs } } fn get_map(cmd: &Value) -> (&str, &Value) { let name = get_name(cmd).unwrap(); (name, cmd.get(name).unwrap_or(cmd)) } fn get_aliases(cmd: &Value) -> Vec<&str> { let mut names = vec![]; if let Value::Mapping(m) = cmd { for kv in m.iter() { let (k, v) = kv; match k.as_str().unwrap() { "aliases" | "visible_aliases" => { if let Value::Sequence(aliases) = v { for alias in aliases { names.push(alias.as_str().unwrap()); } } } _ => (), } } } names } fn get_name(cmd: &Value) -> Option<&str> { if let Some(v) = cmd.get("name") { if v.is_string() { return Some(v.as_str().unwrap()); } } if let Value::Mapping(m) = cmd { for kv in m.iter() { let (k, _) = kv; // should only be one mapping return k.as_str(); } } None } fn get_sub_names(cmd: &Value) -> Vec<&str> { let mut names = vec![]; let subcommands = cmd.get("subcommands"); if subcommands.is_none() { return names; } let subcommands = subcommands.unwrap(); if let Value::Sequence(cmds) = subcommands { for cmd in cmds { let name = get_name(cmd); if name.is_some() { names.push(name.unwrap()); } } } names } fn main() { let mut line_reader = LineReader::new(); let mut input = String::new(); let yaml = load_yaml!("clap.yaml"); loop { let app = App::from(yaml); if line_reader.read_line(&mut input, "> ") == None { break; } let matches = app.try_get_matches_from(input.split_whitespace()); if let Err(err) = matches { println!("{}", err); continue; } match matches.unwrap().subcommand() { ("create", Some(matches)) => match matches.subcommand() { ("request", _) => println!(" ** create request **"), ("variable", _) => println!(" ** create variable **"), _ => unreachable!(), }, ("show", Some(matches)) => match matches.subcommand() { ("requests", _) => println!(" ** show requests **"), ("variables", _) => println!(" ** show variables **"), ("options", _) => println!(" ** show options **"), ("environments", _) => println!(" ** show environments **"), ("workspaces", _) => println!(" ** show workspaces **"), ("response", _) => println!(" ** show response **"), _ => unreachable!(), }, ("set", Some(matches)) => match matches.subcommand() { ("workspace", _) => println!(" ** set workspace **"), ("environment", _) => println!(" ** set environment **"), ("request", _) => println!(" ** set request **"), ("option", _) => println!(" ** set option **"), ("variable", _) => println!(" ** set variable **"), _ => unreachable!(), }, ("delete", Some(matches)) => match matches.subcommand() { ("requests", _) => println!(" ** delete requests **"), ("variables", _) => println!(" ** delete variables **"), ("options", _) => println!(" ** delete options **"), _ => unreachable!(), }, _ => unreachable!(), } } }