Change to crates.io reedline-repl-rs
This commit is contained in:
parent
e823f863c9
commit
88d7d79bb3
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -608,6 +608,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "reedline-repl-rs"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a82ee3209046ff272bd79fe1311ed82823102f741f3130be9dc82ef77d492b3"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"crossterm",
|
||||
|
@ -16,7 +16,7 @@ inventory = "0.3.1"
|
||||
itertools = "0.10.5"
|
||||
nullable-result = { version = "0.7.0", features=["try_trait"] }
|
||||
parse_int = "0.6.0"
|
||||
reedline-repl-rs = { path = "reedline-repl-rs" }
|
||||
reedline-repl-rs = "1.0.2"
|
||||
serde = { version = "1.0.144", features = ["derive"] }
|
||||
thiserror = "1.0.37"
|
||||
toml = "0.5.9"
|
||||
|
2
reedline-repl-rs/.gitignore
vendored
2
reedline-repl-rs/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
/target
|
||||
Cargo.lock
|
@ -1,41 +0,0 @@
|
||||
[package]
|
||||
name = "reedline-repl-rs"
|
||||
version = "1.0.2"
|
||||
authors = ["Artur Hallmann <arturh@arturh.de>", "Jack Lund <jackl@geekheads.net>"]
|
||||
description = "Library to generate a fancy REPL for your application based on reedline and clap"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/arturh85/reedline-repl-rs"
|
||||
homepage = "https://github.com/arturh85/reedline-repl-rs"
|
||||
readme = "README.md"
|
||||
keywords = ["repl", "interpreter", "clap"]
|
||||
categories = ["command-line-interface"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
reedline = "0.6.0"
|
||||
nu-ansi-term = { version = "0.45.1" }
|
||||
crossterm = { version = "0.23.2" }
|
||||
yansi = "0.5.1"
|
||||
regex = "1"
|
||||
clap = "3"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] } # only for async example
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi-util = "0.1.5"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
async = []
|
||||
macro = ["clap/cargo"]
|
||||
|
||||
[[example]]
|
||||
name = "async"
|
||||
required-features = ["async"]
|
||||
|
||||
[[example]]
|
||||
name = "macro"
|
||||
required-features = ["macro"]
|
@ -1,28 +0,0 @@
|
||||
//! Example using Repl with a custom error type.
|
||||
use reedline_repl_rs::clap::{Arg, ArgMatches, Command};
|
||||
use reedline_repl_rs::{Repl, Result};
|
||||
|
||||
/// Write "Hello" with given name
|
||||
async fn hello<T>(args: ArgMatches, _context: &mut T) -> Result<Option<String>> {
|
||||
Ok(Some(format!("Hello, {}", args.value_of("who").unwrap())))
|
||||
}
|
||||
|
||||
/// Called after successful command execution, updates prompt with returned Option
|
||||
async fn update_prompt<T>(_context: &mut T) -> Result<Option<String>> {
|
||||
Ok(Some("updated".to_string()))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let mut repl = Repl::new(())
|
||||
.with_name("MyApp")
|
||||
.with_version("v0.1.0")
|
||||
.with_command_async(
|
||||
Command::new("hello")
|
||||
.arg(Arg::new("who").required(true))
|
||||
.about("Greetings!"),
|
||||
|args, context| Box::pin(hello(args, context)),
|
||||
)
|
||||
.with_on_after_command_async(|context| Box::pin(update_prompt(context)));
|
||||
repl.run_async().await
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
//! Example using Repl with a custom error type.
|
||||
|
||||
use reedline_repl_rs::clap::{ArgMatches, Command};
|
||||
use reedline_repl_rs::Repl;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum CustomError {
|
||||
ReplError(reedline_repl_rs::Error),
|
||||
StringError(String),
|
||||
}
|
||||
|
||||
impl From<reedline_repl_rs::Error> for CustomError {
|
||||
fn from(e: reedline_repl_rs::Error) -> Self {
|
||||
CustomError::ReplError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CustomError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
CustomError::ReplError(e) => write!(f, "REPL Error: {}", e),
|
||||
CustomError::StringError(s) => write!(f, "String Error: {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CustomError {}
|
||||
|
||||
/// Do nothing, unsuccesfully
|
||||
fn hello<T>(_args: ArgMatches, _context: &mut T) -> Result<Option<String>, CustomError> {
|
||||
Err(CustomError::StringError("Returning an error".to_string()))
|
||||
}
|
||||
|
||||
fn main() -> Result<(), reedline_repl_rs::Error> {
|
||||
let mut repl = Repl::new(())
|
||||
.with_name("MyApp")
|
||||
.with_version("v0.1.0")
|
||||
.with_description("My very cool app")
|
||||
.with_command(
|
||||
Command::new("hello").about("Do nothing, unsuccessfully"),
|
||||
hello,
|
||||
);
|
||||
repl.run()
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
//! Example with custom Keybinding
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use reedline::{EditCommand, ReedlineEvent};
|
||||
use reedline_repl_rs::clap::{Arg, ArgMatches, Command};
|
||||
use reedline_repl_rs::{Repl, Result};
|
||||
|
||||
/// Write "Hello" with given name
|
||||
fn hello<T>(args: ArgMatches, _context: &mut T) -> Result<Option<String>> {
|
||||
Ok(Some(format!("Hello, {}", args.value_of("who").unwrap())))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut repl = Repl::new(())
|
||||
.with_name("MyApp")
|
||||
.with_version("v0.1.0")
|
||||
.with_description("My very cool app")
|
||||
.with_banner("Welcome to MyApp")
|
||||
.with_command(
|
||||
Command::new("hello")
|
||||
.arg(Arg::new("who").required(true))
|
||||
.about("Greetings!"),
|
||||
hello,
|
||||
)
|
||||
// greet friend with CTRG+g
|
||||
.with_keybinding(
|
||||
KeyModifiers::CONTROL,
|
||||
KeyCode::Char('g'),
|
||||
ReedlineEvent::ExecuteHostCommand("hello Friend".to_string()),
|
||||
)
|
||||
// show help with CTRL+h
|
||||
.with_keybinding(
|
||||
KeyModifiers::CONTROL,
|
||||
KeyCode::Char('h'),
|
||||
ReedlineEvent::ExecuteHostCommand("help".to_string()),
|
||||
)
|
||||
// uppercase current word with CTRL+u
|
||||
.with_keybinding(
|
||||
KeyModifiers::CONTROL,
|
||||
KeyCode::Char('u'),
|
||||
ReedlineEvent::Edit(vec![EditCommand::UppercaseWord]),
|
||||
)
|
||||
// uppercase current word with CTRL+l
|
||||
.with_keybinding(
|
||||
KeyModifiers::CONTROL,
|
||||
KeyCode::Char('l'),
|
||||
ReedlineEvent::Edit(vec![EditCommand::LowercaseWord]),
|
||||
);
|
||||
|
||||
println!("Keybindings:");
|
||||
let keybindings = repl.get_keybindings();
|
||||
for search_modifier in [
|
||||
KeyModifiers::NONE,
|
||||
KeyModifiers::CONTROL,
|
||||
KeyModifiers::SHIFT,
|
||||
KeyModifiers::ALT,
|
||||
] {
|
||||
for ((modifier, key_code), reedline_event) in &keybindings {
|
||||
if *modifier == search_modifier {
|
||||
println!("{:?} + {:?} => {:?}", modifier, key_code, reedline_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
repl.run()
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
//! Minimal example
|
||||
use reedline_repl_rs::clap::{Arg, ArgMatches, Command};
|
||||
use reedline_repl_rs::{Repl, Result};
|
||||
|
||||
/// Write "Hello" with given name
|
||||
fn hello<T>(args: ArgMatches, _context: &mut T) -> Result<Option<String>> {
|
||||
Ok(Some(format!("Hello, {}", args.value_of("who").unwrap())))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut repl = Repl::new(())
|
||||
.with_name("MyApp")
|
||||
.with_version("v0.1.0")
|
||||
.with_description("My very cool app")
|
||||
.with_banner("Welcome to MyApp")
|
||||
.with_command(
|
||||
Command::new("hello")
|
||||
.arg(Arg::new("who").required(true))
|
||||
.about("Greetings!"),
|
||||
hello,
|
||||
);
|
||||
repl.run()
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//! Example using initialize_repl macro
|
||||
use reedline_repl_rs::clap::{Arg, ArgMatches, Command};
|
||||
use reedline_repl_rs::{initialize_repl, Repl, Result};
|
||||
|
||||
/// Write "Hello" with given name
|
||||
fn hello<T>(args: ArgMatches, _context: &mut T) -> Result<Option<String>> {
|
||||
Ok(Some(format!("Hello, {}", args.value_of("who").unwrap())))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut repl = initialize_repl!(()).with_command(
|
||||
Command::new("hello")
|
||||
.arg(Arg::new("who").required(true))
|
||||
.about("Greetings!"),
|
||||
hello,
|
||||
);
|
||||
repl.run()
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
//! Example using Repl without Context (or, more precisely, a Context of ())
|
||||
use reedline_repl_rs::clap::{Arg, ArgMatches, Command};
|
||||
use reedline_repl_rs::{Repl, Result};
|
||||
|
||||
/// Add two numbers. Have to make this generic to be able to pass a Context of type ()
|
||||
fn add<T>(args: ArgMatches, _context: &mut T) -> Result<Option<String>> {
|
||||
let first: i32 = args.value_of("first").unwrap().parse()?;
|
||||
let second: i32 = args.value_of("second").unwrap().parse()?;
|
||||
|
||||
Ok(Some((first + second).to_string()))
|
||||
}
|
||||
|
||||
/// Write "Hello"
|
||||
fn hello<T>(args: ArgMatches, _context: &mut T) -> Result<Option<String>> {
|
||||
Ok(Some(format!("Hello, {}", args.value_of("who").unwrap())))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut repl = Repl::new(())
|
||||
.with_name("MyApp")
|
||||
.with_version("v0.1.0")
|
||||
.with_description("My very cool app")
|
||||
.with_command(
|
||||
Command::new("add")
|
||||
.arg(Arg::new("first").required(true))
|
||||
.arg(Arg::new("second").required(true))
|
||||
.about("Add two numbers together"),
|
||||
add,
|
||||
)
|
||||
.with_command(
|
||||
Command::new("hello")
|
||||
.arg(Arg::new("who").required(true))
|
||||
.about("Greetings!"),
|
||||
hello,
|
||||
);
|
||||
repl.run()
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
//! Example using Repl with Context
|
||||
use reedline_repl_rs::clap::{Arg, ArgMatches, Command};
|
||||
use reedline_repl_rs::{Repl, Result};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Context {
|
||||
list: VecDeque<String>,
|
||||
}
|
||||
|
||||
/// Append name to list
|
||||
fn append(args: ArgMatches, context: &mut Context) -> Result<Option<String>> {
|
||||
let name: String = args.value_of("name").unwrap().to_string();
|
||||
context.list.push_back(name);
|
||||
let list: Vec<String> = context.list.clone().into();
|
||||
|
||||
Ok(Some(list.join(", ")))
|
||||
}
|
||||
|
||||
/// Prepend name to list
|
||||
fn prepend(args: ArgMatches, context: &mut Context) -> Result<Option<String>> {
|
||||
let name: String = args.value_of("name").unwrap().to_string();
|
||||
context.list.push_front(name);
|
||||
let list: Vec<String> = context.list.clone().into();
|
||||
|
||||
Ok(Some(list.join(", ")))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut repl = Repl::new(Context::default())
|
||||
.with_name("MyList")
|
||||
.with_version("v0.1.0")
|
||||
.with_description("My very cool List")
|
||||
.with_command(
|
||||
Command::new("append")
|
||||
.arg(Arg::new("name").required(true))
|
||||
.about("Append name to end of list"),
|
||||
append,
|
||||
)
|
||||
.with_command(
|
||||
Command::new("prepend")
|
||||
.arg(Arg::new("name").required(true))
|
||||
.about("Prepend name to front of list"),
|
||||
prepend,
|
||||
)
|
||||
.with_on_after_command(|context| Ok(Some(format!("MyList [{}]", context.list.len()))));
|
||||
repl.run()
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["major","minor", "patch", "pin", "digest"],
|
||||
"automerge": true
|
||||
}
|
||||
]
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
#[cfg(feature = "async")]
|
||||
use crate::AsyncCallback;
|
||||
use crate::Callback;
|
||||
use clap::Command;
|
||||
use std::fmt;
|
||||
|
||||
/// Struct to define a command in the REPL
|
||||
|
||||
pub(crate) struct ReplCommand<Context, E> {
|
||||
pub(crate) name: String,
|
||||
pub(crate) command: Command<'static>,
|
||||
pub(crate) callback: Option<Callback<Context, E>>,
|
||||
#[cfg(feature = "async")]
|
||||
pub(crate) async_callback: Option<AsyncCallback<Context, E>>,
|
||||
}
|
||||
|
||||
impl<Context, E> fmt::Debug for ReplCommand<Context, E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Command").field("name", &self.name).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Context, E> PartialEq for ReplCommand<Context, E> {
|
||||
fn eq(&self, other: &ReplCommand<Context, E>) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl<Context, E> ReplCommand<Context, E> {
|
||||
/// Create a new command with the given name and callback function
|
||||
pub fn new(name: &str, command: Command<'static>, callback: Callback<Context, E>) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
command,
|
||||
callback: Some(callback),
|
||||
#[cfg(feature = "async")]
|
||||
async_callback: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new async command with the given name and callback function
|
||||
#[cfg(feature = "async")]
|
||||
pub fn new_async(
|
||||
name: &str,
|
||||
command: Command<'static>,
|
||||
callback: AsyncCallback<Context, E>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
command,
|
||||
callback: None,
|
||||
async_callback: Some(callback),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
use crate::command::ReplCommand;
|
||||
use clap::Command;
|
||||
use reedline::{Completer, Span, Suggestion};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) struct ReplCompleter {
|
||||
commands: HashMap<String, Command<'static>>,
|
||||
}
|
||||
|
||||
impl Completer for ReplCompleter {
|
||||
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||
let mut completions = vec![];
|
||||
completions.extend(if line.contains(' ') {
|
||||
let mut words = line[0..pos].split(' ');
|
||||
let first_word = words.next().unwrap();
|
||||
let mut words_rev = words.rev();
|
||||
if let Some(command) = self.commands.get(first_word) {
|
||||
let last_word = words_rev.next().unwrap();
|
||||
let last_word_start_pos = line.len() - last_word.len();
|
||||
let span = Span::new(last_word_start_pos, pos);
|
||||
self.parameter_values_starting_with(command, words_rev.count(), last_word, span)
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
let span = Span::new(0, pos);
|
||||
self.commands_starting_with(line, span)
|
||||
});
|
||||
completions.dedup();
|
||||
completions
|
||||
}
|
||||
}
|
||||
|
||||
impl ReplCompleter {
|
||||
pub fn new<Context, E>(repl_commands: &HashMap<String, ReplCommand<Context, E>>) -> Self {
|
||||
let mut commands = HashMap::new();
|
||||
for (name, repl_command) in repl_commands.iter() {
|
||||
commands.insert(name.clone(), repl_command.command.clone());
|
||||
}
|
||||
ReplCompleter { commands }
|
||||
}
|
||||
|
||||
fn build_suggestion(&self, value: &str, help: Option<&str>, span: Span) -> Suggestion {
|
||||
Suggestion {
|
||||
value: value.to_string(),
|
||||
description: help.map(|n| n.to_string()),
|
||||
extra: None,
|
||||
span,
|
||||
append_whitespace: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn parameter_values_starting_with(
|
||||
&self,
|
||||
command: &Command<'static>,
|
||||
_parameter_idx: usize,
|
||||
search: &str,
|
||||
span: Span,
|
||||
) -> Vec<Suggestion> {
|
||||
let mut completions = vec![];
|
||||
for arg in command.get_arguments() {
|
||||
// skips --help and --version
|
||||
if arg.is_global_set() {
|
||||
continue;
|
||||
}
|
||||
if let Some(possible_values) = arg.get_possible_values() {
|
||||
completions.extend(
|
||||
possible_values
|
||||
.iter()
|
||||
.filter(|value| value.get_name().starts_with(search))
|
||||
.map(|value| {
|
||||
self.build_suggestion(value.get_name(), value.get_help(), span)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(long) = arg.get_long() {
|
||||
let value = "--".to_string() + long;
|
||||
if value.starts_with(search) {
|
||||
completions.push(self.build_suggestion(&value, arg.get_help(), span));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(short) = arg.get_short() {
|
||||
let value = "-".to_string() + &short.to_string();
|
||||
if value.starts_with(search) {
|
||||
completions.push(self.build_suggestion(&value, arg.get_help(), span));
|
||||
}
|
||||
}
|
||||
}
|
||||
completions
|
||||
}
|
||||
|
||||
fn commands_starting_with(&self, search: &str, span: Span) -> Vec<Suggestion> {
|
||||
let mut result: Vec<Suggestion> = self
|
||||
.commands
|
||||
.iter()
|
||||
.filter(|(key, _)| key.starts_with(search))
|
||||
.map(|(_, command)| {
|
||||
self.build_suggestion(command.get_name(), command.get_about(), span)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if "help".starts_with(search) {
|
||||
result.push(self.build_suggestion("help", Some("show help"), span));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
use std::convert::From;
|
||||
use std::fmt;
|
||||
use std::num;
|
||||
|
||||
/// Result type
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Error type
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
/// Parameter is required when it shouldn't be
|
||||
IllegalRequiredError(String),
|
||||
|
||||
/// Parameter is defaulted when it's also required
|
||||
IllegalDefaultError(String),
|
||||
|
||||
/// A required argument is missing
|
||||
MissingRequiredArgument(String, String),
|
||||
|
||||
/// Too many arguments were provided
|
||||
TooManyArguments(String, usize),
|
||||
|
||||
/// Error parsing a bool value
|
||||
ParseBoolError(std::str::ParseBoolError),
|
||||
|
||||
/// Error parsing an int value
|
||||
ParseIntError(num::ParseIntError),
|
||||
|
||||
/// Error parsing a float value
|
||||
ParseFloatError(num::ParseFloatError),
|
||||
|
||||
/// Command not found
|
||||
UnknownCommand(String),
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> std::result::Result<(), fmt::Error> {
|
||||
match self {
|
||||
Error::IllegalDefaultError(parameter) => {
|
||||
write!(f, "Error: Parameter '{}' cannot have a default", parameter)
|
||||
}
|
||||
Error::IllegalRequiredError(parameter) => {
|
||||
write!(f, "Error: Parameter '{}' cannot be required", parameter)
|
||||
}
|
||||
Error::MissingRequiredArgument(command, parameter) => write!(
|
||||
f,
|
||||
"Error: Missing required argument '{}' for command '{}'",
|
||||
parameter, command
|
||||
),
|
||||
Error::TooManyArguments(command, nargs) => write!(
|
||||
f,
|
||||
"Error: Command '{}' can have no more than {} arguments",
|
||||
command, nargs,
|
||||
),
|
||||
Error::ParseBoolError(error) => write!(f, "Error: {}", error,),
|
||||
Error::ParseFloatError(error) => write!(f, "Error: {}", error,),
|
||||
Error::ParseIntError(error) => write!(f, "Error: {}", error,),
|
||||
Error::UnknownCommand(command) => write!(f, "Error: Unknown command '{}'", command),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<num::ParseIntError> for Error {
|
||||
fn from(error: num::ParseIntError) -> Self {
|
||||
Error::ParseIntError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<num::ParseFloatError> for Error {
|
||||
fn from(error: num::ParseFloatError) -> Self {
|
||||
Error::ParseFloatError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::str::ParseBoolError> for Error {
|
||||
fn from(error: std::str::ParseBoolError) -> Self {
|
||||
Error::ParseBoolError(error)
|
||||
}
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
//! reedline-repl-rs - [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) library
|
||||
//! for Rust
|
||||
//!
|
||||
//! # Example
|
||||
//! ```rust,no_run
|
||||
#![doc = include_str!("../examples/hello_world.rs")]
|
||||
//! ```
|
||||
//!
|
||||
//! reedline-repl-rs uses the [builder](https://en.wikipedia.org/wiki/Builder_pattern) pattern extensively.
|
||||
//! What these lines are doing is:
|
||||
//! - creating a repl with an empty Context (see below)
|
||||
//! - with a name of "MyApp", the given version, and the given description
|
||||
//! - and adding a "hello" command which calls out to the `hello` callback function defined above
|
||||
//! - the `hello` command has a single parameter, "who", which is required, and has the given help
|
||||
//! message
|
||||
//!
|
||||
//! The `hello` function takes a reference to [ArgMatches](https://docs.rs/clap/latest/clap/struct.ArgMatches.html),
|
||||
//! and an (unused) `Context`, which is used to hold state if you
|
||||
//! need to - the initial context is passed in to the call to
|
||||
//! [Repl::new](struct.Repl.html#method.new), in our case, `()`.
|
||||
//! Because we're not using a Context, we need to include a generic type in our `hello` function,
|
||||
//! because there's no way to pass an argument of type `()` otherwise.
|
||||
//!
|
||||
//! All command function callbacks return a `Result<Option<String>>`. This has the following
|
||||
//! effect:
|
||||
//! - If the return is `Ok(Some(String))`, it prints the string to stdout
|
||||
//! - If the return is `Ok(None)`, it prints nothing
|
||||
//! - If the return is an error, it prints the error message to stderr
|
||||
//!
|
||||
//! # Context
|
||||
//!
|
||||
//! The `Context` type is used to keep state between REPL calls. Here's an example:
|
||||
//! ```rust,no_run
|
||||
#![doc = include_str!("../examples/with_context.rs")]
|
||||
//! ```
|
||||
//! A few things to note:
|
||||
//! - you pass in the initial value for your Context struct to the call to
|
||||
//! [Repl::new()](struct.Repl.html#method.new)
|
||||
//! - the context is passed to your command callback functions as a mutable reference
|
||||
//! - the prompt can be changed after each executed commmand using with_on_after_command as shown
|
||||
//!
|
||||
//! # Async Support
|
||||
//!
|
||||
//! The `async` feature allows you to write async REPL code:
|
||||
//! ```rust,no_run
|
||||
#![doc = include_str!("../examples/async.rs")]
|
||||
//! ```
|
||||
//! A few things to note:
|
||||
//! - The ugly Pin::Box workaround is required because of unstable rust async Fn's
|
||||
//!
|
||||
//! # Keybindings
|
||||
//!
|
||||
//! Per default Emacs-style keybindings are used
|
||||
//! ```rust,no_run
|
||||
#![doc = include_str!("../examples/custom_keybinding.rs")]
|
||||
//! ```
|
||||
//! A few things to note:
|
||||
//! - The ugly Pin::Box workaround is required because of unstable rust async Fn's
|
||||
//!
|
||||
//! # Help
|
||||
//! reedline-repl-rs automatically builds help commands for your REPL using clap [print_help](https://docs.rs/clap/latest/clap/struct.App.html#method.print_help):
|
||||
//!
|
||||
//! ```bash
|
||||
//! % myapp
|
||||
//! MyApp> 〉help
|
||||
//! MyApp v0.1.0: My very cool app
|
||||
//!
|
||||
//! COMMANDS:
|
||||
//! append Append name to end of list
|
||||
//! help Print this message or the help of the given subcommand(s)
|
||||
//! prepend Prepend name to front of list
|
||||
//!
|
||||
//! MyApp> 〉help append
|
||||
//! append
|
||||
//! Append name to end of list
|
||||
//!
|
||||
//! USAGE:
|
||||
//! append <name>
|
||||
//!
|
||||
//! ARGS:
|
||||
//! <name>
|
||||
//!
|
||||
//! OPTIONS:
|
||||
//! -h, --help Print help information
|
||||
//! MyApp> 〉
|
||||
//! ```
|
||||
//!
|
||||
//! # Errors
|
||||
//!
|
||||
//! Your command functions don't need to return `reedline_repl_rs::Error`; you can return any error from
|
||||
//! them. Your error will need to implement `std::fmt::Display`, so the Repl can print the error,
|
||||
//! and you'll need to implement `std::convert::From` for `reedline_repl_rs::Error` to your error type.
|
||||
//! This makes error handling in your command functions easier, since you can just allow whatever
|
||||
//! errors your functions emit bubble up.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
#![doc = include_str!("../examples/custom_error.rs")]
|
||||
//! ```
|
||||
|
||||
mod command;
|
||||
mod completer;
|
||||
mod error;
|
||||
mod prompt;
|
||||
mod repl;
|
||||
|
||||
pub use clap;
|
||||
use clap::ArgMatches;
|
||||
pub use crossterm;
|
||||
pub use error::{Error, Result};
|
||||
pub use nu_ansi_term;
|
||||
pub use reedline;
|
||||
#[doc(inline)]
|
||||
pub use repl::Repl;
|
||||
#[cfg(feature = "async")]
|
||||
use std::{future::Future, pin::Pin};
|
||||
pub use yansi;
|
||||
use yansi::Paint;
|
||||
|
||||
/// Command callback function signature
|
||||
pub type Callback<Context, Error> =
|
||||
fn(ArgMatches, &mut Context) -> std::result::Result<Option<String>, Error>;
|
||||
|
||||
/// Async Command callback function signature
|
||||
#[cfg(feature = "async")]
|
||||
pub type AsyncCallback<Context, Error> =
|
||||
fn(
|
||||
ArgMatches,
|
||||
&'_ mut Context,
|
||||
) -> Pin<Box<dyn Future<Output = std::result::Result<Option<String>, Error>> + '_>>;
|
||||
|
||||
/// AfterCommand callback function signature
|
||||
pub type AfterCommandCallback<Context, Error> =
|
||||
fn(&mut Context) -> std::result::Result<Option<String>, Error>;
|
||||
|
||||
/// Async AfterCommand callback function signature
|
||||
#[cfg(feature = "async")]
|
||||
pub type AsyncAfterCommandCallback<Context, Error> =
|
||||
fn(
|
||||
&'_ mut Context,
|
||||
) -> Pin<Box<dyn Future<Output = std::result::Result<Option<String>, Error>> + '_>>;
|
||||
|
||||
/// Utility to format prompt strings as green and bold. Use yansi directly instead for custom colors.
|
||||
pub fn paint_green_bold(input: &str) -> String {
|
||||
Box::new(Paint::green(input).bold()).to_string()
|
||||
}
|
||||
|
||||
/// Utility to format prompt strings as yellow and bold. Use yansi directly instead for custom colors.
|
||||
pub fn paint_yellow_bold(input: &str) -> String {
|
||||
Box::new(Paint::yellow(input).bold()).to_string()
|
||||
}
|
||||
|
||||
/// Initialize the name, version and description of the Repl from your
|
||||
/// crate name, version and description
|
||||
#[macro_export]
|
||||
#[cfg(feature = "macro")]
|
||||
macro_rules! initialize_repl {
|
||||
($context: expr) => {{
|
||||
let repl = Repl::new($context)
|
||||
.with_name(clap::crate_name!())
|
||||
.with_version(clap::crate_version!())
|
||||
.with_description(clap::crate_description!());
|
||||
|
||||
repl
|
||||
}};
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
use reedline::{DefaultPrompt, Prompt, PromptEditMode, PromptHistorySearch};
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ReplPrompt {
|
||||
default: DefaultPrompt,
|
||||
prefix: String,
|
||||
}
|
||||
|
||||
impl Prompt for ReplPrompt {
|
||||
/// Use prefix as render prompt
|
||||
fn render_prompt_left(&self) -> Cow<str> {
|
||||
{
|
||||
Cow::Borrowed(&self.prefix)
|
||||
}
|
||||
}
|
||||
|
||||
// call default impl
|
||||
fn render_prompt_right(&self) -> Cow<str> {
|
||||
// self.default.render_prompt_right()
|
||||
Cow::Borrowed("")
|
||||
}
|
||||
fn render_prompt_indicator(&self, edit_mode: PromptEditMode) -> Cow<str> {
|
||||
self.default.render_prompt_indicator(edit_mode)
|
||||
}
|
||||
fn render_prompt_multiline_indicator(&self) -> Cow<str> {
|
||||
self.default.render_prompt_multiline_indicator()
|
||||
}
|
||||
fn render_prompt_history_search_indicator(
|
||||
&self,
|
||||
history_search: PromptHistorySearch,
|
||||
) -> Cow<str> {
|
||||
self.default
|
||||
.render_prompt_history_search_indicator(history_search)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReplPrompt {
|
||||
fn default() -> Self {
|
||||
ReplPrompt::new("repl")
|
||||
}
|
||||
}
|
||||
|
||||
impl ReplPrompt {
|
||||
/// Constructor for the default prompt, which takes the amount of spaces required between the left and right-hand sides of the prompt
|
||||
pub fn new(left_prompt: &str) -> ReplPrompt {
|
||||
ReplPrompt {
|
||||
prefix: left_prompt.to_string(),
|
||||
default: DefaultPrompt::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn update_prefix(&mut self, prefix: &str) {
|
||||
self.prefix = prefix.to_string();
|
||||
}
|
||||
}
|
@ -1,615 +0,0 @@
|
||||
use crate::command::ReplCommand;
|
||||
use crate::completer::ReplCompleter;
|
||||
use crate::error::*;
|
||||
use crate::prompt::ReplPrompt;
|
||||
use crate::{paint_green_bold, paint_yellow_bold, AfterCommandCallback, Callback};
|
||||
#[cfg(feature = "async")]
|
||||
use crate::{AsyncAfterCommandCallback, AsyncCallback};
|
||||
use clap::Command;
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use nu_ansi_term::{Color, Style};
|
||||
use reedline::{
|
||||
default_emacs_keybindings, ColumnarMenu, DefaultHinter, DefaultValidator, Emacs,
|
||||
ExampleHighlighter, FileBackedHistory, Keybindings, Reedline, ReedlineEvent, ReedlineMenu,
|
||||
Signal,
|
||||
};
|
||||
use std::boxed::Box;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::path::PathBuf;
|
||||
|
||||
type ErrorHandler<Context, E> = fn(error: E, repl: &Repl<Context, E>) -> Result<()>;
|
||||
|
||||
fn default_error_handler<Context, E: Display>(error: E, _repl: &Repl<Context, E>) -> Result<()> {
|
||||
eprintln!("{}", error);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main REPL struct
|
||||
pub struct Repl<Context, E: Display> {
|
||||
name: String,
|
||||
banner: Option<String>,
|
||||
version: String,
|
||||
description: String,
|
||||
prompt: ReplPrompt,
|
||||
after_command_callback: Option<AfterCommandCallback<Context, E>>,
|
||||
#[cfg(feature = "async")]
|
||||
after_command_callback_async: Option<AsyncAfterCommandCallback<Context, E>>,
|
||||
commands: HashMap<String, ReplCommand<Context, E>>,
|
||||
history: Option<PathBuf>,
|
||||
history_capacity: Option<usize>,
|
||||
context: Context,
|
||||
keybindings: Keybindings,
|
||||
hinter_style: Style,
|
||||
hinter_enabled: bool,
|
||||
quick_completions: bool,
|
||||
partial_completions: bool,
|
||||
stop_on_ctrl_c: bool,
|
||||
stop_on_ctrl_d: bool,
|
||||
error_handler: ErrorHandler<Context, E>,
|
||||
}
|
||||
|
||||
impl<Context, E> Repl<Context, E>
|
||||
where
|
||||
E: Display + From<Error> + std::fmt::Debug,
|
||||
{
|
||||
/// Create a new Repl with the given context's initial value.
|
||||
pub fn new(context: Context) -> Self {
|
||||
let name = String::from("repl");
|
||||
let style = Style::new().italic().fg(Color::LightGray);
|
||||
let mut keybindings = default_emacs_keybindings();
|
||||
keybindings.add_binding(
|
||||
KeyModifiers::NONE,
|
||||
KeyCode::Tab,
|
||||
ReedlineEvent::Menu("completion_menu".to_string()),
|
||||
);
|
||||
let prompt = ReplPrompt::new(&paint_green_bold(&format!("{}> ", name)));
|
||||
|
||||
Self {
|
||||
name,
|
||||
banner: None,
|
||||
version: String::new(),
|
||||
description: String::new(),
|
||||
commands: HashMap::new(),
|
||||
history: None,
|
||||
history_capacity: None,
|
||||
after_command_callback: None,
|
||||
#[cfg(feature = "async")]
|
||||
after_command_callback_async: None,
|
||||
quick_completions: true,
|
||||
partial_completions: false,
|
||||
hinter_enabled: true,
|
||||
hinter_style: style,
|
||||
prompt,
|
||||
context,
|
||||
keybindings,
|
||||
stop_on_ctrl_c: false,
|
||||
stop_on_ctrl_d: true,
|
||||
error_handler: default_error_handler,
|
||||
}
|
||||
}
|
||||
|
||||
/// Give your Repl a name. This is used in the help summary for the Repl.
|
||||
pub fn with_name(mut self, name: &str) -> Self {
|
||||
self.name = name.to_string();
|
||||
self.with_formatted_prompt(name)
|
||||
}
|
||||
|
||||
/// Give your Repl a banner. This is printed at the start of running the Repl.
|
||||
pub fn with_banner(mut self, banner: &str) -> Self {
|
||||
self.banner = Some(banner.to_string());
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Give your Repl a version. This is used in the help summary for the Repl.
|
||||
pub fn with_version(mut self, version: &str) -> Self {
|
||||
self.version = version.to_string();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Give your Repl a description. This is used in the help summary for the Repl.
|
||||
pub fn with_description(mut self, description: &str) -> Self {
|
||||
self.description = description.to_string();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Give your REPL a callback which is called after every command and may update the prompt
|
||||
pub fn with_on_after_command(mut self, callback: AfterCommandCallback<Context, E>) -> Self {
|
||||
self.after_command_callback = Some(callback);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Give your REPL a callback which is called after every command and may update the prompt
|
||||
#[cfg(feature = "async")]
|
||||
pub fn with_on_after_command_async(
|
||||
mut self,
|
||||
callback: AsyncAfterCommandCallback<Context, E>,
|
||||
) -> Self {
|
||||
self.after_command_callback_async = Some(callback);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Give your Repl a file based history saved at history_path
|
||||
pub fn with_history(mut self, history_path: PathBuf, capacity: usize) -> Self {
|
||||
self.history = Some(history_path);
|
||||
self.history_capacity = Some(capacity);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Give your Repl a custom prompt. The default prompt is the Repl name, followed by
|
||||
/// a `>`, all in green and bold, followed by a space:
|
||||
///
|
||||
/// &Paint::green(format!("{}> ", name)).bold().to_string()
|
||||
pub fn with_prompt(mut self, prompt: &str) -> Self {
|
||||
self.prompt.update_prefix(prompt);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Give your Repl a custom prompt while applying green/bold formatting automatically
|
||||
///
|
||||
/// &Paint::green(format!("{}> ", name)).bold().to_string()
|
||||
pub fn with_formatted_prompt(mut self, prompt: &str) -> Self {
|
||||
self.prompt.update_prefix(prompt);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Pass in a custom error handler. This is really only for testing - the default
|
||||
/// error handler simply prints the error to stderr and then returns
|
||||
pub fn with_error_handler(mut self, handler: ErrorHandler<Context, E>) -> Self {
|
||||
self.error_handler = handler;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Turn on/off if REPL run is stopped on CTRG+C (Default: false)
|
||||
pub fn with_stop_on_ctrl_c(mut self, stop_on_ctrl_c: bool) -> Self {
|
||||
self.stop_on_ctrl_c = stop_on_ctrl_c;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Turn on/off if REPL run is stopped on CTRG+D (Default: true)
|
||||
pub fn with_stop_on_ctrl_d(mut self, stop_on_ctrl_d: bool) -> Self {
|
||||
self.stop_on_ctrl_d = stop_on_ctrl_d;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Turn on quick completions. These completions will auto-select if the completer
|
||||
/// ever narrows down to a single entry.
|
||||
pub fn with_quick_completions(mut self, quick_completions: bool) -> Self {
|
||||
self.quick_completions = quick_completions;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Turn on partial completions. These completions will fill the buffer with the
|
||||
/// smallest common string from all the options
|
||||
pub fn with_partial_completions(mut self, partial_completions: bool) -> Self {
|
||||
self.partial_completions = partial_completions;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style for reedline's fish-style history autosuggestions
|
||||
///
|
||||
/// Default: `nu_ansi_term::Style::new().italic().fg(nu_ansi_term::Color::LightGray)`
|
||||
///
|
||||
pub fn with_hinter_style(mut self, style: Style) -> Self {
|
||||
self.hinter_style = style;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Disables reedline's fish-style history autosuggestions
|
||||
pub fn with_hinter_disabled(mut self) -> Self {
|
||||
self.hinter_enabled = false;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a reedline keybinding
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If `comamnd` is an empty [`ReedlineEvent::UntilFound`]
|
||||
pub fn with_keybinding(
|
||||
mut self,
|
||||
modifier: KeyModifiers,
|
||||
key_code: KeyCode,
|
||||
command: ReedlineEvent,
|
||||
) -> Self {
|
||||
self.keybindings.add_binding(modifier, key_code, command);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Find a keybinding based on the modifier and keycode
|
||||
pub fn find_keybinding(
|
||||
&self,
|
||||
modifier: KeyModifiers,
|
||||
key_code: KeyCode,
|
||||
) -> Option<ReedlineEvent> {
|
||||
self.keybindings.find_binding(modifier, key_code)
|
||||
}
|
||||
|
||||
/// Get assigned keybindings
|
||||
pub fn get_keybindings(&self) -> HashMap<(KeyModifiers, KeyCode), ReedlineEvent> {
|
||||
// keybindings.get_keybindings() cannot be returned directly because KeyCombination is not visible
|
||||
self.keybindings
|
||||
.get_keybindings()
|
||||
.iter()
|
||||
.map(|(key, value)| ((key.modifier, key.key_code), value.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Remove a keybinding
|
||||
///
|
||||
/// Returns `Some(ReedlineEvent)` if the keycombination was previously bound to a particular [`ReedlineEvent`]
|
||||
pub fn without_keybinding(mut self, modifier: KeyModifiers, key_code: KeyCode) -> Self {
|
||||
self.keybindings.remove_binding(modifier, key_code);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a command to your REPL
|
||||
pub fn with_command(
|
||||
mut self,
|
||||
command: Command<'static>,
|
||||
callback: Callback<Context, E>,
|
||||
) -> Self {
|
||||
let name = command.get_name().to_string();
|
||||
self.commands
|
||||
.insert(name.clone(), ReplCommand::new(&name, command, callback));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a command to your REPL
|
||||
#[cfg(feature = "async")]
|
||||
pub fn with_command_async(
|
||||
mut self,
|
||||
command: Command<'static>,
|
||||
callback: AsyncCallback<Context, E>,
|
||||
) -> Self {
|
||||
let name = command.get_name().to_string();
|
||||
self.commands.insert(
|
||||
name.clone(),
|
||||
ReplCommand::new_async(&name, command, callback),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
fn show_help(&self, args: &[&str]) -> Result<()> {
|
||||
if args.is_empty() {
|
||||
let mut app = Command::new("app");
|
||||
|
||||
for (_, com) in self.commands.iter() {
|
||||
app = app.subcommand(com.command.clone());
|
||||
}
|
||||
let mut help_bytes: Vec<u8> = Vec::new();
|
||||
app.write_help(&mut help_bytes)
|
||||
.expect("failed to print help");
|
||||
let mut help_string =
|
||||
String::from_utf8(help_bytes).expect("Help message was invalid UTF8");
|
||||
let marker = "SUBCOMMANDS:";
|
||||
if let Some(marker_pos) = help_string.find(marker) {
|
||||
help_string = paint_yellow_bold("COMMANDS:")
|
||||
+ &help_string[(marker_pos + marker.len())..help_string.len()];
|
||||
}
|
||||
let header = format!(
|
||||
"{} {}\n{}\n",
|
||||
paint_green_bold(&self.name),
|
||||
self.version,
|
||||
self.description
|
||||
);
|
||||
println!("{}", header);
|
||||
println!("{}", help_string);
|
||||
} else if let Some((_, subcommand)) = self
|
||||
.commands
|
||||
.iter()
|
||||
.find(|(name, _)| name.as_str() == args[0])
|
||||
{
|
||||
subcommand
|
||||
.command
|
||||
.clone()
|
||||
.print_help()
|
||||
.expect("failed to print help");
|
||||
println!();
|
||||
} else {
|
||||
eprintln!("Help not found for command '{}'", args[0]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_command(&mut self, command: &str, args: &[&str]) -> core::result::Result<(), E> {
|
||||
match self.commands.get(command) {
|
||||
Some(definition) => {
|
||||
let mut argv: Vec<&str> = vec![command];
|
||||
argv.extend(args);
|
||||
match definition.command.clone().try_get_matches_from_mut(argv) {
|
||||
Ok(matches) => match (definition
|
||||
.callback
|
||||
.expect("Must be filled for sync commands"))(
|
||||
matches, &mut self.context
|
||||
) {
|
||||
Ok(Some(value)) => println!("{}", value),
|
||||
Ok(None) => (),
|
||||
Err(error) => return Err(error),
|
||||
},
|
||||
Err(err) => {
|
||||
err.print().expect("failed to print");
|
||||
}
|
||||
};
|
||||
self.execute_after_command_callback()?;
|
||||
}
|
||||
None => {
|
||||
if command == "help" {
|
||||
self.show_help(args)?;
|
||||
} else {
|
||||
return Err(Error::UnknownCommand(command.to_string()).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn execute_after_command_callback(&mut self) -> core::result::Result<(), E> {
|
||||
if let Some(callback) = self.after_command_callback {
|
||||
match callback(&mut self.context) {
|
||||
Ok(Some(new_prompt)) => {
|
||||
self.prompt.update_prefix(&new_prompt);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) => {
|
||||
eprintln!("failed to execute after_command_callback {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
async fn execute_after_command_callback_async(&mut self) -> core::result::Result<(), E> {
|
||||
self.execute_after_command_callback()?;
|
||||
if let Some(callback) = self.after_command_callback_async {
|
||||
match callback(&mut self.context).await {
|
||||
Ok(new_prompt) => {
|
||||
if let Some(new_prompt) = new_prompt {
|
||||
self.prompt.update_prefix(&new_prompt);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("failed to execute after_command_callback {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
async fn handle_command_async(
|
||||
&mut self,
|
||||
command: &str,
|
||||
args: &[&str],
|
||||
) -> core::result::Result<(), E> {
|
||||
match self.commands.get(command) {
|
||||
Some(definition) => {
|
||||
let mut argv: Vec<&str> = vec![command];
|
||||
argv.extend(args);
|
||||
match definition.command.clone().try_get_matches_from_mut(argv) {
|
||||
Ok(matches) => match if let Some(async_callback) = definition.async_callback {
|
||||
async_callback(matches, &mut self.context).await
|
||||
} else {
|
||||
definition
|
||||
.callback
|
||||
.expect("Either async or sync callback must be set")(
|
||||
matches,
|
||||
&mut self.context,
|
||||
)
|
||||
} {
|
||||
Ok(Some(value)) => println!("{}", value),
|
||||
Ok(None) => (),
|
||||
Err(error) => return Err(error),
|
||||
},
|
||||
Err(err) => {
|
||||
err.print().expect("failed to print");
|
||||
}
|
||||
};
|
||||
self.execute_after_command_callback_async().await?;
|
||||
}
|
||||
None => {
|
||||
if command == "help" {
|
||||
self.show_help(args)?;
|
||||
} else {
|
||||
return Err(Error::UnknownCommand(command.to_string()).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_line(&self, line: &str) -> (String, Vec<String>) {
|
||||
let r = regex::Regex::new(r#"("[^"\n]+"|[\S]+)"#).unwrap();
|
||||
let mut args = r
|
||||
.captures_iter(line)
|
||||
.map(|a| a[0].to_string().replace('\"', ""))
|
||||
.collect::<Vec<String>>();
|
||||
let command: String = args.drain(..1).collect();
|
||||
(command, args)
|
||||
}
|
||||
|
||||
pub fn process_line(&mut self, line: String) -> core::result::Result<(), E> {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let (command, args) = self.parse_line(trimmed);
|
||||
let args = args.iter().fold(vec![], |mut state, a| {
|
||||
state.push(a.as_str());
|
||||
state
|
||||
});
|
||||
self.handle_command(&command, &args)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
async fn process_line_async(&mut self, line: String) -> core::result::Result<(), E> {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let (command, args) = self.parse_line(trimmed);
|
||||
let args = args.iter().fold(vec![], |mut state, a| {
|
||||
state.push(a.as_str());
|
||||
state
|
||||
});
|
||||
self.handle_command_async(&command, &args).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_line_editor(&mut self) -> Result<Reedline> {
|
||||
let mut valid_commands: Vec<String> = self
|
||||
.commands
|
||||
.iter()
|
||||
.map(|(_, command)| command.name.clone())
|
||||
.collect();
|
||||
valid_commands.push("help".to_string());
|
||||
let completer = Box::new(ReplCompleter::new(&self.commands));
|
||||
let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu"));
|
||||
let validator = Box::new(DefaultValidator);
|
||||
let mut line_editor = Reedline::create()
|
||||
.with_edit_mode(Box::new(Emacs::new(self.keybindings.clone())))
|
||||
.with_completer(completer)
|
||||
.with_menu(ReedlineMenu::EngineCompleter(completion_menu))
|
||||
.with_highlighter(Box::new(ExampleHighlighter::new(valid_commands.clone())))
|
||||
.with_validator(validator)
|
||||
.with_partial_completions(self.partial_completions)
|
||||
.with_quick_completions(self.quick_completions);
|
||||
|
||||
if self.hinter_enabled {
|
||||
line_editor = line_editor.with_hinter(Box::new(
|
||||
DefaultHinter::default().with_style(self.hinter_style),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(history_path) = &self.history {
|
||||
let capacity = self.history_capacity.unwrap();
|
||||
let history =
|
||||
FileBackedHistory::with_file(capacity, history_path.to_path_buf()).unwrap();
|
||||
line_editor = line_editor.with_history(Box::new(history));
|
||||
}
|
||||
|
||||
Ok(line_editor)
|
||||
}
|
||||
|
||||
/// Execute REPL
|
||||
pub fn run(&mut self) -> Result<()> {
|
||||
enable_virtual_terminal_processing();
|
||||
if let Some(banner) = &self.banner {
|
||||
println!("{}", banner);
|
||||
}
|
||||
let mut line_editor = self.build_line_editor()?;
|
||||
|
||||
loop {
|
||||
let sig = line_editor
|
||||
.read_line(&self.prompt)
|
||||
.expect("failed to read_line");
|
||||
match sig {
|
||||
Signal::Success(line) => {
|
||||
if let Err(err) = self.process_line(line) {
|
||||
(self.error_handler)(err, self)?;
|
||||
}
|
||||
}
|
||||
Signal::CtrlC => {
|
||||
if self.stop_on_ctrl_c {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Signal::CtrlD => {
|
||||
if self.stop_on_ctrl_d {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
disable_virtual_terminal_processing();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute REPL
|
||||
#[cfg(feature = "async")]
|
||||
pub async fn run_async(&mut self) -> Result<()> {
|
||||
enable_virtual_terminal_processing();
|
||||
if let Some(banner) = &self.banner {
|
||||
println!("{}", banner);
|
||||
}
|
||||
let mut line_editor = self.build_line_editor()?;
|
||||
|
||||
loop {
|
||||
let sig = line_editor
|
||||
.read_line(&self.prompt)
|
||||
.expect("failed to read_line");
|
||||
match sig {
|
||||
Signal::Success(line) => {
|
||||
if let Err(err) = self.process_line_async(line).await {
|
||||
(self.error_handler)(err, self)?;
|
||||
}
|
||||
}
|
||||
Signal::CtrlC => {
|
||||
if self.stop_on_ctrl_c {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Signal::CtrlD => {
|
||||
if self.stop_on_ctrl_d {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
disable_virtual_terminal_processing();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn enable_virtual_terminal_processing() {
|
||||
use winapi_util::console::Console;
|
||||
if let Ok(mut term) = Console::stdout() {
|
||||
let _guard = term.set_virtual_terminal_processing(true);
|
||||
}
|
||||
if let Ok(mut term) = Console::stderr() {
|
||||
let _guard = term.set_virtual_terminal_processing(true);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn disable_virtual_terminal_processing() {
|
||||
use winapi_util::console::Console;
|
||||
if let Ok(mut term) = Console::stdout() {
|
||||
let _guard = term.set_virtual_terminal_processing(false);
|
||||
}
|
||||
if let Ok(mut term) = Console::stderr() {
|
||||
let _guard = term.set_virtual_terminal_processing(false);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn enable_virtual_terminal_processing() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn disable_virtual_terminal_processing() {
|
||||
// no-op
|
||||
}
|
Loading…
Reference in New Issue
Block a user