| // Copyright 2019 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, error::ReadlineError, highlight::Highlighter, hint::Hinter, Helper, |
| }, |
| std::{ |
| borrow::Cow::{self, Borrowed, Owned}, |
| fmt, |
| str::FromStr, |
| }, |
| }; |
| |
| /// Macro to generate a command enum and its impl. |
| macro_rules! gen_commands { |
| ($name:ident { |
| $($variant:ident = ($val: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> { |
| let mut variants = Vec::new(); |
| $(variants.push($val.to_string());)* |
| variants |
| } |
| |
| pub fn arguments(&self) -> &'static str { |
| match self { |
| $( |
| $name::$variant => concat!($("<", $arg, "> ",)*) |
| ),* |
| } |
| } |
| |
| /// Multiline help string for `$name` including usage of all variants. |
| pub fn help_msg() -> &'static str { |
| concat!("Commands:\n", $( |
| "\t", $val, " ", $("<", $arg, "> ",)* "-- ", $help, "\n" |
| ),*, "\nSee //src/connectivity/bluetooth/tools/bt-bredr-profile/README.md for documentation.\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(()), |
| } |
| } |
| } |
| |
| } |
| } |
| |
| gen_commands! { |
| Cmd { |
| Advertise = ("advertise", ["psm", "channel-mode", "max-rx-du-size"], |
| "\n\t\tRegister a service with the SDP server.\n\ |
| \t\t'psm' is a protocol id that this service will advertise support for.\n\ |
| \t\t'channel-mode' is {basic|ertm}.\n\ |
| \t\t'max-rx-sdu-size' is an integer in the range 0 - 65535.\n\n\ |
| \t\tExample: advertise 25 basic 672"), |
| SetupRfcomm = ("setup-rfcomm", [], |
| "\n\t\t Registers an advertisement for an example SPP service.\n\ |
| \t\t Registers a search for compatible SPP services."), |
| Services = ("services", [], "List registered services"), |
| RemoveService = ("remove-service", ["service-id"], |
| "\n\t\tUnregister service corresponding to 'service-id'\n\ |
| \t\tExample: remove-service 0"), |
| Channels = ("channels", [], "List connected L2CAP channels and their Ids assigned by the REPL"), |
| ConnectL2cap = ("connect-l2cap", ["peer-id", "psm", "channel-mode", "max-rx-sdu-size", "security-requirements"], |
| "\n\t\tCreate an l2cap channel to the remote device 'peer-id'. \n\ |
| \t\t'channel-mode' must be {basic|ertm}. 'psm' and 'max-rx-sdu-size' must be\n\ |
| \t\tpositive integers in the range 0 - 65535. 'security-requirements' must be\n\ |
| \t\t{none|auth|sc|auth-sc}.\n\n\ |
| \t\tExample: connect-l2cap 028565803f1368b2 1 basic 672 none"), |
| ConnectRfcomm = ("connect-rfcomm", ["peer-id", "server-channel"], |
| "\n\t\tCreate an RFCOMM channel to the remote device 'peer-id'. \n\ |
| \t\t'server-channel' must be in the range [1,30]. \n\ |
| \t\tExample: connect-rfcomm 028565803f1368b2 1"), |
| DisconnectL2cap = ("disconnect-l2cap", ["channel-id"], |
| "\n\t\tDrop socket corresponding to 'channel-id', which will disconnect\n\ |
| \t\tthe l2cap channel.\n\ |
| \t\t'channel-id' must correspond to a connected channel listed by the \n\ |
| \t\t'channels' command\n\n\ |
| \t\tExample: disconnect-l2cap 0"), |
| DisconnectRfcomm = ("disconnect-rfcomm", ["server-channel"], |
| "\n\t\tDisconnect the RFCOMM channel corresponding to `server-channel`.\n\ |
| \t\tExample: disconnect-rfcomm 5"), |
| WriteL2cap = ("write-l2cap", ["channel-id", "data"], |
| "\n\t\tWrite 'data' on the L2CAP socket/channel represented by 'channel-id'\n\n\ |
| \t\tExample: write 0 0123456789abcd"), |
| WriteRfcomm = ("write-rfcomm", ["channel-id", "data"], |
| "\n\t\tWrite 'data' on the RFCOMM channel represented by 'server-channel'\n\n\ |
| \t\tExample: write 1 0123456789abcd"), |
| Help = ("help", [], "Print command help"), |
| Exit = ("exit", [], "Remove all services, close all channels, and exit the REPL."), |
| Quit = ("quit", [], "Alias for 'exit'."), |
| } |
| } |
| |
| /// Represents either continuation or breaking out of a read-evaluate-print loop. |
| pub enum ReplControl { |
| Break, |
| Continue, |
| } |
| |
| /// CmdHelper provides completion, hints, and highlighting. |
| 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 components: Vec<_> = line.trim_start().split_whitespace().collect(); |
| |
| // Check whether we have entered a command and either whitespace or a partial argument. |
| // If yes, complete arguments; if no, complete commands |
| let mut variants = Vec::new(); |
| let should_complete_arguments = (components.len() == 1 && line.ends_with(" ")) |
| || (components.len() == 2 && !line.ends_with(" ")); |
| if should_complete_arguments { |
| let candidates = vec![]; |
| Ok((0, candidates)) |
| } else { |
| for variant in Cmd::variants() { |
| if variant.starts_with(line) { |
| variants.push(variant) |
| } |
| } |
| Ok((0, variants)) |
| } |
| } |
| } |
| |
| impl Hinter for CmdHelper { |
| /// Provide a hint for what argument should be presented next. |
| /// Returns None if no hint is available. |
| 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.arguments().to_string(),) |
| }) |
| .ok() |
| } |
| } |
| |
| impl Highlighter for CmdHelper { |
| /// Highlighter changes the text color of the hint, to differentiate between user input |
| /// and the hint itself. |
| 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 {} |