Skip to content

Instantly share code, notes, and snippets.

@mcastorina
Created August 6, 2020 02:03
Show Gist options
  • Save mcastorina/7ad4782f75e3707f9f534c05b72e390c to your computer and use it in GitHub Desktop.
Save mcastorina/7ad4782f75e3707f9f534c05b72e390c to your computer and use it in GitHub Desktop.
Clap and rustyline tab completion integration
[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"
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"]
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