blob: bb39ad13cfffbce60f876e0e1bc9281c10d434da [file] [log] [blame] [edit]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use rustyline::completion::Completer;
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::Helper;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::fmt;
use std::str::FromStr;
/// Macro to generate a command enum and its impl.
/// A command consists of the command name, optional flags, arguments, and a help description.
macro_rules! gen_commands {
($name:ident {
$($variant:ident = ($val:expr, [$($flag:expr),*], [$($arg:expr),*], $help:expr)),*,
}) => {
/// Enum of all possible commands
#[derive(PartialEq)]
pub enum $name {
$($variant),*
}
impl $name {
/// Returns a list of the string representations of all variants
pub fn variants() -> Vec<String> {
vec![$($val.to_string(),)*]
}
pub fn arguments(&self) -> &'static str {
match self {
$(
$name::$variant => concat!($("<", $arg, "> ",)*)
),*
}
}
pub fn flags(&self) -> &'static str {
match self {
$(
$name::$variant => concat!($("[", $flag, "] ",)*)
),*
}
}
/// Help string for a given variant.
/// The format is "command [flag].. <arg>.. -- help message".
pub fn cmd_help(&self) -> &'static str {
match self {
$(
$name::$variant =>
concat!($val, " ", $("[", $flag, "] ",)* $("<", $arg, "> ",)* "-- ", $help)
),*
}
}
/// Multiline help string for `$name` including usage of all variants.
pub fn help_msg() -> &'static str {
concat!("Commands:\n", $(
"\t", $val, " ", $("[", $flag, "] ",)* $("<", $arg, "> ",)* "-- ", $help, "\n"
),*)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
$($name::$variant => write!(f, $val)),* ,
}
}
}
impl FromStr for $name {
type Err = ();
fn from_str(s: &str) -> Result<$name, ()> {
match s {
$($val => Ok($name::$variant)),* ,
_ => Err(()),
}
}
}
}
}
// `Cmd` is the declarative specification of all commands that bt-cli accepts.
gen_commands! {
Cmd {
Help = ("help", [], [], "Print this help message"),
List = ("list", [], [], "List discovered services"),
Connect = ("connect", [], ["index"], "Connect to a service"),
ReadChr = ("read-chr", [], ["id"], "Read a characteristic"),
ReadLongChr = ("read-long-chr", [], ["id", "offset", "max bytes"],
"Read a long characteristic."),
WriteChr = ("write-chr", ["-w"], ["id", "value"],
"Write to a characteristic.\n\tUse the -w flag to write without response."),
WriteLongChr = ("write-long-chr", ["-r"], ["id", "offset", "value"],
"Write to a long characteristic.\n\tUse the -r flag for a reliable write."),
ReadDesc = ("read-desc", [], ["id"], "Read a characteristic descriptor"),
ReadLongDesc = ("read-long-desc", [], ["id", "offset", "max bytes"],
"Read a long characteristic descriptor"),
WriteDesc = ("write-desc", [], ["id", "value"], "Write to a characteristic descriptor"),
WriteLongDesc = ("write-long-desc", [], ["id", "offset", "value"],
"Write to a long characteristic descriptor"),
ReadByType = ("read-by-type", [], ["uuid"], "Read a characteristic or descriptor by UUID"),
EnableNotify = ("enable-notify", [], ["id"], "Enable characteristic notifications"),
DisableNotify = ("disable-notify", [], ["id"], "Disable characteristic notifications"),
Quit = ("quit", [], [], "Quit and disconnect the peripheral"),
Exit = ("exit", [], [], "Quit and disconnect the peripheral"),
}
}
/// CmdHelper provides completion, hints, and highlighting for bt-cli
pub struct CmdHelper;
impl CmdHelper {
pub fn new() -> CmdHelper {
CmdHelper {}
}
}
impl Completer for CmdHelper {
type Candidate = String;
fn complete(&self, line: &str, _pos: usize) -> Result<(usize, Vec<String>), ReadlineError> {
let mut variants = Vec::new();
for variant in Cmd::variants() {
if variant.starts_with(line) {
variants.push(variant)
}
}
Ok((0, variants))
}
}
impl Hinter for CmdHelper {
/// CmdHelper provides hints for commands with arguments
fn hint(&self, line: &str, _pos: usize) -> Option<String> {
let needs_space = !line.ends_with(" ");
line.trim()
.parse::<Cmd>()
.map(|cmd| {
format!("{}{}{}", if needs_space { " " } else { "" }, cmd.flags(), cmd.arguments())
})
.ok()
}
}
impl Highlighter for CmdHelper {
/// CmdHelper provides highlights for commands with hints
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
if hint.trim().is_empty() {
Borrowed(hint)
} else {
Owned(format!("\x1b[90m{}\x1b[0m", hint))
}
}
}
/// CmdHelper can be used as an `Editor` helper for entering input commands
impl Helper for CmdHelper {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gen_commands_macro() {
assert!(Cmd::variants().contains(&"connect".to_string()));
assert_eq!(Cmd::WriteChr.arguments(), "<id> <value> ");
assert_eq!(Cmd::Help.arguments(), "");
assert_eq!(Cmd::Exit.arguments(), "");
assert_eq!(Cmd::Quit.arguments(), "");
assert_eq!(Cmd::WriteLongDesc.flags(), "");
assert_eq!(Cmd::WriteDesc.flags(), "");
assert_eq!(Cmd::WriteLongChr.flags(), "[-r] ");
assert_eq!(Cmd::WriteChr.flags(), "[-w] ");
assert!(Cmd::help_msg().starts_with("Commands:\n"));
}
#[test]
fn test_completer() {
let cmdhelper = CmdHelper::new();
assert!(cmdhelper
.complete("write-long-c", 0)
.unwrap()
.1
.contains(&"write-long-chr".to_string()));
assert!(cmdhelper.complete("he", 0).unwrap().1.contains(&"help".to_string()));
assert!(cmdhelper.complete("ex", 0).unwrap().1.contains(&"exit".to_string()));
assert!(cmdhelper.complete("read-d", 0).unwrap().1.contains(&"read-desc".to_string()));
assert!(cmdhelper.complete("en", 0).unwrap().1.contains(&"enable-notify".to_string()));
}
}