Created
August 6, 2020 02:03
-
-
Save mcastorina/7ad4782f75e3707f9f534c05b72e390c to your computer and use it in GitHub Desktop.
Clap and rustyline tab completion integration
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "tmp" | |
version = "0.1.0" | |
authors = ["Miccah Castorina <[email protected]>"] | |
edition = "2018" | |
[dependencies] | |
clap-v3 = { version = "3.0.0-beta.1", features = ["yaml"] } | |
rustyline = "6.2.0" | |
serde_yaml = "0.8" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: tmp | |
version: "0.1.0" | |
settings: | |
- NoBinaryName | |
- DisableVersion | |
- VersionlessSubcommands | |
subcommands: | |
- create: | |
settings: | |
- SubcommandRequiredElseHelp | |
- VersionlessSubcommands | |
about: Create an HTTP request or variable | |
visible_aliases: ["new", "add", "c"] | |
subcommands: | |
- request: | |
about: Create an HTTP request | |
visible_aliases: ["req", "r"] | |
- variable: | |
about: Create a variable | |
visible_aliases: ["var", "v"] | |
- show: | |
settings: | |
- SubcommandRequiredElseHelp | |
- VersionlessSubcommands | |
about: Print resources | |
visible_aliases: ["get", "print", "g", "p"] | |
subcommands: | |
- requests: | |
about: Print requests | |
visible_aliases: ["request", "reqs", "req", "r"] | |
- variables: | |
about: Print variables | |
visible_aliases: ["variable", "vars", "var", "v"] | |
- environments: | |
about: Print environments | |
visible_aliases: ["environment", "envs", "env", "e"] | |
- workspaces: | |
about: Print workspaces | |
visible_aliases: ["workspace", "ws", "w"] | |
- response: | |
about: Print information about the last request and response | |
visible_aliases: ["resp", "rr"] | |
- set: | |
settings: | |
- SubcommandRequiredElseHelp | |
- VersionlessSubcommands | |
about: Set workspace, environment, or request for contextual commands | |
visible_aliases: ["use", "load", "u"] | |
subcommands: | |
- environment: | |
about: Set the environment as used for variable substitution | |
visible_aliases: ["env", "e"] | |
- request: | |
about: Set the request to view and modify specific options | |
visible_aliases: ["req", "r"] | |
- workspace: | |
about: Set the workspace where all data is stored | |
visible_aliases: ["ws", "w"] | |
- variable: | |
about: Update or create variable values | |
visible_aliases: ["var", "v"] | |
- delete: | |
settings: | |
- SubcommandRequiredElseHelp | |
- VersionlessSubcommands | |
about: Delete named requests or variables | |
visible_aliases: ["remove", "del", "rm"] | |
subcommands: | |
- requests: | |
about: Delete the named HTTP requests | |
visible_aliases: ["request", "reqs", "req", "r"] | |
- variables: | |
about: Delete the named variables | |
visible_aliases: ["variable", "vars", "var", "v"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<LineReaderHelper>, | |
} | |
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<Self::Candidate>)> { | |
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<String> = 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<String>, // possible aliases for name | |
completions: Vec<String>, // subcommand names | |
children: Vec<Box<CommandStructure>>, // 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!(), | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment