Change to crates.io reedline-repl-rs

This commit is contained in:
pjht 2022-11-12 16:00:45 -06:00
parent e823f863c9
commit 88d7d79bb3
18 changed files with 3 additions and 1401 deletions

2
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -1,2 +0,0 @@
/target
Cargo.lock

View File

@ -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"]

View File

@ -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
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -1,11 +0,0 @@
{
"extends": [
"config:base"
],
"packageRules": [
{
"matchUpdateTypes": ["major","minor", "patch", "pin", "digest"],
"automerge": true
}
]
}

View File

@ -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),
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}};
}

View File

@ -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();
}
}

View File

@ -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
}