diff --git a/Cargo.toml b/Cargo.toml index e3d08f1..c0277fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,12 +5,13 @@ members = [ "members/libpt-core", "members/libpt-log", "members/libpt-py", + "members/libpt-cli", ] default-members = [".", "members/libpt-core"] [workspace.package] publish = true -version = "0.5.1" +version = "0.6.0-alpha.0" edition = "2021" authors = ["Christoph J. Scherr "] license = "GPL-3.0-or-later" @@ -31,6 +32,7 @@ thiserror = "1.0.56" libpt-core = { version = "0.4.0", path = "members/libpt-core" } libpt-bintols = { version = "0.5.1", path = "members/libpt-bintols" } libpt-log = { version = "0.4.2", path = "members/libpt-log" } +libpt-cli = { version = "0.1.0", path = "members/libpt-cli" } [package] name = "libpt" @@ -52,6 +54,7 @@ core = [] full = ["default", "core", "log", "bintols"] log = ["dep:libpt-log"] bintols = ["dep:libpt-bintols", "log"] +cli = ["dep:libpt-cli", "core", "log"] # py = ["dep:libpt-py"] [lib] @@ -66,3 +69,4 @@ crate-type = [ libpt-core = { workspace = true } libpt-bintols = { workspace = true, optional = true } libpt-log = { workspace = true, optional = true } +libpt-cli = { workspace = true, optional = true } diff --git a/members/libpt-cli/Cargo.toml b/members/libpt-cli/Cargo.toml new file mode 100644 index 0000000..12176b3 --- /dev/null +++ b/members/libpt-cli/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "libpt-cli" +publish.workspace = true +version = "0.1.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +description.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true + +[package.metadata.docs.rs] +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] + +[features] +default = [] +log = ["dep:log"] + +[dependencies] +anyhow.workspace = true +clap = { version = "4.5.7", features = ["derive"] } +comfy-table = "7.1.1" +console = "0.15.8" +dialoguer = { version = "0.11.0", features = ["completion", "history"] } +embed-doc-image = "0.1.4" +exitcode = "1.1.2" +human-panic = "2.0.0" +indicatif = "0.17.8" +libpt-log = { workspace = true, optional = false } +log = { version = "0.4.21", optional = true } +shlex = "1.3.0" +strum = { version = "0.26.3", features = ["derive"] } +thiserror.workspace = true diff --git a/members/libpt-cli/data/media/repl.png b/members/libpt-cli/data/media/repl.png new file mode 100644 index 0000000..7121227 Binary files /dev/null and b/members/libpt-cli/data/media/repl.png differ diff --git a/members/libpt-cli/examples/cli.rs b/members/libpt-cli/examples/cli.rs new file mode 100644 index 0000000..91fdcad --- /dev/null +++ b/members/libpt-cli/examples/cli.rs @@ -0,0 +1,41 @@ +use clap::Parser; +use libpt_cli::args::VerbosityLevel; +use libpt_cli::{clap, printing}; +use libpt_log::{debug, Logger}; + +/// This is the help +/// +/// This is more help +#[derive(Parser, Debug)] +struct Cli { + // already has documentation + #[command(flatten)] + verbosity: VerbosityLevel, + + /// texts to be echoed + #[arg(required = true)] + text: Vec, + + /// try to be more machine readable + #[arg(short, long)] + machine: bool, +} + +fn main() { + let cli = Cli::parse(); + let _logger = Logger::builder() + .max_level(cli.verbosity.level()) + .show_time(false) + .build(); + + debug!("logger initialized with level: {}", cli.verbosity.level()); + + if !cli.machine { + let text = cli.text.join(" "); + printing::blockprint(text, console::Color::Green); + } else { + for text in cli.text { + println!("{text}") + } + } +} diff --git a/members/libpt-cli/examples/repl.rs b/members/libpt-cli/examples/repl.rs new file mode 100644 index 0000000..28caf93 --- /dev/null +++ b/members/libpt-cli/examples/repl.rs @@ -0,0 +1,85 @@ +use console::style; +use libpt_cli::repl::{DefaultRepl, Repl}; +use libpt_cli::{clap, printing, strum}; +use libpt_log::{debug, Level, Logger}; + +use clap::Subcommand; +use strum::EnumIter; + +// this is where you define what data/commands/arguments the REPL accepts +#[derive(Subcommand, Debug, EnumIter, Clone)] +enum ReplCommand { + /// wait for LEN seconds + Wait { + /// wait so long + len: u64, + }, + /// echo the given texts + Echo { + /// the text you want to print + text: Vec, + /// print with a fancy border and colors + #[arg(short, long)] + fancy: bool, + }, + /// hello world + Hello, + /// leave the repl + Exit, +} + +fn main() -> anyhow::Result<()> { + // You would normally make a proper cli interface with clap before entering the repl. This is + // omitted here for brevity + let _logger = Logger::builder() + .show_time(false) + .max_level(Level::INFO) + .build()?; + + // the compiler can infer that we want to use the ReplCommand enum. + let mut repl = DefaultRepl::::default(); + + debug!("entering the repl"); + loop { + // repl.step() should be at the start of your loop + // It is here that the repl will get the user input, validate it, and so on + match repl.step() { + Ok(c) => c, + Err(e) => { + // if the user requested the help, print in blue, otherwise in red as it's just an + // error + if let libpt_cli::repl::error::Error::Parsing(e) = &e { + if e.kind() == clap::error::ErrorKind::DisplayHelp { + println!("{}", style(e).cyan()); + continue; + } + } + println!("{}", style(e).red().bold()); + continue; + } + }; + + // now we can match our defined commands + // + // only None if the repl has not stepped yet + match repl.command().to_owned().unwrap() { + ReplCommand::Exit => break, + ReplCommand::Wait { len } => { + debug!("len: {len}"); + let spinner = indicatif::ProgressBar::new_spinner(); + spinner.enable_steady_tick(std::time::Duration::from_millis(100)); + std::thread::sleep(std::time::Duration::from_secs(len)); + spinner.finish(); + } + ReplCommand::Hello => println!("Hello!"), + ReplCommand::Echo { text, fancy } => { + if !fancy { + println!("{}", text.join(" ")) + } else { + printing::blockprint(&text.join(" "), console::Color::Cyan) + } + } + } + } + Ok(()) +} diff --git a/members/libpt-cli/src/args.rs b/members/libpt-cli/src/args.rs new file mode 100644 index 0000000..34dd0fa --- /dev/null +++ b/members/libpt-cli/src/args.rs @@ -0,0 +1,280 @@ +//! Utilities for parsing options and arguments on the start of a CLI application + +use clap::Parser; +use libpt_log::Level; +#[cfg(feature = "log")] +use log; + +/// Custom help template for displaying command-line usage information +/// +/// This template modifies the default template provided by Clap to include additional information +/// and customize the layout of the help message. +/// +/// Differences from the default template: +/// - Includes the application version and author information at the end +/// +/// Apply like this: +/// ``` +/// # use libpt_cli::args::HELP_TEMPLATE; +/// use clap::Parser; +/// #[derive(Parser, Debug, Clone, PartialEq, Eq, Hash)] +/// #[command(help_template = HELP_TEMPLATE)] +/// pub struct MyArgs { +/// /// show more details +/// #[arg(short, long)] +/// pub verbose: bool, +/// } +/// ``` +/// +/// ## Example +/// +/// Don't forget to set `authors` in your `Cargo.toml`! +/// +/// ```bash +/// $ cargo run -- -h +/// about: short +/// +/// Usage: aaa [OPTIONS] +/// +/// Options: +/// -v, --verbose show more details +/// -h, --help Print help (see more with '--help') +/// -V, --version Print version +/// +/// aaa: 0.1.0 +/// Author: Christoph J. Scherr +/// +/// ``` +pub const HELP_TEMPLATE: &str = r"{about-section} +{usage-heading} {usage} + +{all-args}{tab} + +{name}: {version} +Author: {author-with-newline} +"; + +/// Transform -v and -q flags to some kind of loglevel +/// +/// # Example +/// +/// Include this into your [clap] derive struct like this: +/// +/// ``` +/// use libpt_cli::args::VerbosityLevel; +/// use clap::Parser; +/// +/// #[derive(Parser, Debug)] +/// pub struct Opts { +/// #[command(flatten)] +/// pub verbose: VerbosityLevel, +/// #[arg(short, long)] +/// pub mynum: usize, +/// } +/// +/// ``` +/// +/// Get the loglevel like this: +/// +/// ```no_run +/// # use libpt_cli::args::VerbosityLevel; +/// use libpt_log::Level; +/// # use clap::Parser; +/// +/// # #[derive(Parser, Debug)] +/// # pub struct Opts { +/// # #[command(flatten)] +/// # pub verbose: VerbosityLevel, +/// # } +/// +/// fn main() { +/// let opts = Opts::parse(); +/// +/// // Level might be None if the user wants no output at all. +/// // for the 'tracing' level: +/// let level: Level = opts.verbose.level(); +/// } +/// ``` +#[derive(Parser, Clone, PartialEq, Eq, Hash)] +pub struct VerbosityLevel { + /// make the output more verbose + #[arg( + long, + short = 'v', + action = clap::ArgAction::Count, + global = true, + // help = L::verbose_help(), + // long_help = L::verbose_long_help(), + )] + verbose: i8, + + /// make the output less verbose + /// + /// ( -qqq for completely quiet) + #[arg( + long, + short = 'q', + action = clap::ArgAction::Count, + global = true, + conflicts_with = "verbose", + )] + quiet: i8, +} + +impl VerbosityLevel { + /// true only if no verbose and no quiet was set (user is using defaults) + #[inline] + #[must_use] + #[allow(clippy::missing_const_for_fn)] // the values of self can change + pub fn changed(&self) -> bool { + self.verbose != 0 || self.quiet != 0 + } + #[inline] + #[must_use] + fn value(&self) -> i8 { + Self::level_value(Level::INFO) + .saturating_sub((self.quiet).min(10)) + .saturating_add((self.verbose).min(10)) + } + + /// get the [Level] for that [`VerbosityLevel`] + /// + /// # Examples + /// + /// ``` + /// use libpt_log::Level; // reexport: tracing + /// use libpt_cli::args::VerbosityLevel; + /// + /// let verbosity_level = VerbosityLevel::INFO; + /// assert_eq!(verbosity_level.level(), Level::INFO); + /// ``` + #[inline] + #[must_use] + pub fn level(&self) -> Level { + let v = self.value(); + match v { + 0 => Level::ERROR, + 1 => Level::WARN, + 2 => Level::INFO, + 3 => Level::DEBUG, + 4 => Level::TRACE, + _ => { + if v > 4 { + Level::TRACE + } else { + /* v < 0 */ + Level::ERROR + } + } + } + } + + /// get the [`log::Level`] for that `VerbosityLevel` + /// + /// This is the method for the [log] crate, which I use less often. + /// + /// [None] means that absolutely no output is wanted (completely quiet) + #[inline] + #[must_use] + #[cfg(feature = "log")] + pub fn level_for_log_crate(&self) -> log::Level { + match self.level() { + Level::TRACE => log::Level::Trace, + Level::DEBUG => log::Level::Debug, + Level::INFO => log::Level::Info, + Level::WARN => log::Level::Warn, + Level::ERROR => log::Level::Error, + } + } + + #[inline] + #[must_use] + const fn level_value(level: Level) -> i8 { + match level { + Level::TRACE => 4, + Level::DEBUG => 3, + Level::INFO => 2, + Level::WARN => 1, + Level::ERROR => 0, + } + } + + /// # Examples + /// + /// ``` + /// use libpt_log::Level; // reexport: tracing + /// use libpt_cli::args::VerbosityLevel; + /// + /// let verbosity_level = VerbosityLevel::TRACE; + /// assert_eq!(verbosity_level.level(), Level::TRACE); + /// ``` + pub const TRACE: Self = Self { + verbose: 2, + quiet: 0, + }; + /// # Examples + /// + /// ``` + /// use libpt_log::Level; // reexport: tracing + /// use libpt_cli::args::VerbosityLevel; + /// + /// let verbosity_level = VerbosityLevel::DEBUG; + /// assert_eq!(verbosity_level.level(), Level::DEBUG); + /// ``` + pub const DEBUG: Self = Self { + verbose: 1, + quiet: 0, + }; + /// # Examples + /// + /// ``` + /// use libpt_log::Level; // reexport: tracing + /// use libpt_cli::args::VerbosityLevel; + /// + /// let verbosity_level = VerbosityLevel::INFO; + /// assert_eq!(verbosity_level.level(), Level::INFO); + /// ``` + pub const INFO: Self = Self { + verbose: 0, + quiet: 0, + }; + /// # Examples + /// + /// ``` + /// use libpt_log::Level; // reexport: tracing + /// use libpt_cli::args::VerbosityLevel; + /// + /// let verbosity_level = VerbosityLevel::WARN; + /// assert_eq!(verbosity_level.level(), Level::WARN); + /// ``` + pub const WARN: Self = Self { + verbose: 0, + quiet: 1, + }; + /// # Examples + /// + /// ``` + /// use libpt_log::Level; // reexport: tracing + /// use libpt_cli::args::VerbosityLevel; + /// + /// let verbosity_level = VerbosityLevel::ERROR; + /// assert_eq!(verbosity_level.level(), Level::ERROR); + /// ``` + pub const ERROR: Self = Self { + verbose: 0, + quiet: 2, + }; +} + +impl std::fmt::Debug for VerbosityLevel { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.level()) + } +} + +impl Default for VerbosityLevel { + fn default() -> Self { + Self::INFO + } +} diff --git a/members/libpt-cli/src/lib.rs b/members/libpt-cli/src/lib.rs new file mode 100644 index 0000000..7ffceab --- /dev/null +++ b/members/libpt-cli/src/lib.rs @@ -0,0 +1,14 @@ +#![warn(clippy::pedantic, clippy::style, clippy::nursery)] +pub mod args; +pub mod printing; +pub mod repl; + +pub use clap; +pub use comfy_table; +pub use console; +pub use dialoguer; +pub use exitcode; +pub use human_panic; +pub use indicatif; +pub use shlex; +pub use strum; diff --git a/members/libpt-cli/src/printing.rs b/members/libpt-cli/src/printing.rs new file mode 100644 index 0000000..6cf3c01 --- /dev/null +++ b/members/libpt-cli/src/printing.rs @@ -0,0 +1,145 @@ +//! Utilities for formatting, bordering, aligning and printing text content +//! +//! This module provides functions for formatting content with borders and colors, printing them to the console. +//! The functions in this module are designed to simplify the process of creating visually appealing +//! output for CLI applications. +//! +//! Note that most of the utilities in this module are focused on communication with humans, not +//! with machines. Consider evaluating [`std::io::IsTerminal`] before using colorful, dynamic and bordered +//! printing. If you are talking to a machine, it might be useful to not add extra space, add a +//! newline per output or even output JSON. An example that does this well is `ls`: +//! +//! ```bash +//! $ ls +//! Cargo.lock Cargo.toml data LICENSE members README.md scripts src target +//! ``` +//! +//! ```bash +//! $ ls | cat +//! Cargo.lock +//! Cargo.toml +//! data +//! LICENSE +//! members +//! README.md +//! scripts +//! src +//! target +//! ``` +//! +//! See the [CLI Rustbook](https://rust-cli.github.io/book/in-depth/machine-communication.html) for +//! more information on the topic. + +use comfy_table::presets; +use comfy_table::{CellAlignment, ContentArrangement, Table}; +use console::{style, Color}; + +/// Prints content with a simple border around it +/// +/// This function is a convenience wrapper around [blockfmt] and [println]. It automatically +/// formats the content with a border using the specified color and then prints it to the console. +/// +/// # Example +/// +/// ``` +/// use libpt_cli::console::Color; +/// use libpt_cli::printing::blockprint; +/// # fn main() { +/// blockprint("Hello world!", Color::Blue); +/// # } +/// ``` +#[inline] +#[allow(clippy::needless_pass_by_value)] // we just take an impl, using a &impl is much less ergonomic +pub fn blockprint(content: impl ToString, color: Color) { + println!("{}", blockfmt(content, color)); +} + +/// Formats content with a simple border around it +/// +/// This function is a convenience wrapper around [`blockfmt_advanced`] with preset values for +/// border style, content arrangement, and cell alignment. It automatically formats the content +/// with a border as large as possible and centers the content. The resulting cell is colored in +/// the specified color. +/// +/// # Example +/// +/// ``` +/// use libpt_cli::console::Color; +/// use libpt_cli::printing::blockfmt; +/// # fn main() { +/// let formatted_content = blockfmt("Hello world!", Color::Blue); +/// println!("{}", formatted_content); +/// # } +/// ``` +#[inline] +#[allow(clippy::needless_pass_by_value)] // we just take an impl, using a &impl is much less ergonomic +pub fn blockfmt(content: impl ToString, color: Color) -> String { + blockfmt_advanced( + content, + Some(color), + presets::UTF8_BORDERS_ONLY, + ContentArrangement::DynamicFullWidth, + CellAlignment::Center, + ) +} + +/// Formats content with a border around it +/// +/// Unless you are looking for something specific, use [blockfmt] or [blockprint]. +/// +/// The border can be created using box-drawing characters, and the content is formatted +/// within the border. The function allows customization of the border's color, preset, +/// content arrangement, and cell alignment. +/// +/// # Example +/// ``` +/// use libpt_cli::comfy_table::{presets, CellAlignment, ContentArrangement}; +/// use libpt_cli::console::Color; +/// use libpt_cli::printing::blockfmt_advanced; +/// # fn main() { +/// println!( +/// "{}", +/// blockfmt_advanced( +/// "Hello world!", +/// Some(Color::Blue), +/// presets::UTF8_FULL, +/// ContentArrangement::DynamicFullWidth, +/// CellAlignment::Center +/// ) +/// ); +/// # } +/// ``` +/// ```text +/// ┌────────────────────────────────────────────────────────────────────────────────────────┐ +/// │ Hello world! │ +/// └────────────────────────────────────────────────────────────────────────────────────────┘ +/// ``` +/// +/// # Parameters +/// +/// - `content`: The content to be formatted within the border +/// - `color`: The color of the border and text +/// - `preset`: The preset style for the border +/// - `arrangement`: The arrangement of the the border (e.g., stretch to sides, wrap around ) +/// - `alignment`: The alignment of the content within the cells (e.g., left, center, right) +#[allow(clippy::missing_panics_doc)] // we add a row then unwrap it, no panic should be possible +#[allow(clippy::needless_pass_by_value)] // we just take an impl, using a &impl is much less ergonomic +pub fn blockfmt_advanced( + content: impl ToString, + color: Option, + preset: &str, + arrangement: ContentArrangement, + alignment: CellAlignment, +) -> String { + let mut table = Table::new(); + table + .load_preset(preset) + .set_content_arrangement(arrangement) + .add_row(vec![content.to_string()]); + table.column_mut(0).unwrap().set_cell_alignment(alignment); + + match color { + Some(c) => format!("{}", style(table).fg(c)), + None => table.to_string(), + } +} diff --git a/members/libpt-cli/src/repl/default.rs b/members/libpt-cli/src/repl/default.rs new file mode 100644 index 0000000..ad22db9 --- /dev/null +++ b/members/libpt-cli/src/repl/default.rs @@ -0,0 +1,217 @@ +//! This module implements a default repl that fullfills the [Repl] trait +//! +//! You can implement your own [Repl] if you want. + +use std::fmt::Debug; + +use super::Repl; + +use embed_doc_image::embed_doc_image; + +/// [clap] help template with only usage and commands/options +pub const REPL_HELP_TEMPLATE: &str = r"{usage-heading} {usage} + +{all-args}{tab} +"; + +use clap::{Parser, Subcommand}; +use dialoguer::{BasicHistory, Completion}; +use libpt_log::trace; + +#[allow(clippy::needless_doctest_main)] // It makes the example look better +/// Default implementation for a REPL +/// +/// Note that you need to define the commands by yourself with a Subcommands enum. +/// +/// # Example +/// +/// ```no_run +/// use libpt_cli::repl::{DefaultRepl, Repl}; +/// use libpt_cli::clap::Subcommand; +/// use libpt_cli::strum::EnumIter; +/// +/// #[derive(Subcommand, Debug, EnumIter, Clone)] +/// enum ReplCommand { +/// /// hello world +/// Hello, +/// /// leave the repl +/// Exit, +/// } +/// +/// fn main() { +/// let mut repl = DefaultRepl::::default(); +/// loop { +/// repl.step().unwrap(); +/// match repl.command().to_owned().unwrap() { +/// ReplCommand::Hello => println!("Hello"), +/// ReplCommand::Exit => break, +/// _ => (), +/// } +/// } +/// } +/// ``` +/// **Screenshot** +/// +/// ![Screenshot of an example program with a REPL][repl_screenshot] +#[embed_doc_image("repl_screenshot", "data/media/repl.png")] +#[derive(Parser)] +#[command(multicall = true, help_template = REPL_HELP_TEMPLATE)] +#[allow(clippy::module_name_repetitions)] // we can't just name it `Default`, that's part of std +pub struct DefaultRepl +where + C: Debug + Subcommand + strum::IntoEnumIterator, +{ + /// the command you want to execute, along with its arguments + #[command(subcommand)] + command: Option, + + // the following fields are not to be parsed from a command, but used for the internal workings + // of the repl + #[clap(skip)] + buf: String, + #[clap(skip)] + buf_preparsed: Vec, + #[clap(skip)] + completion: DefaultReplCompletion, + #[clap(skip)] + history: BasicHistory, +} + +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)] +struct DefaultReplCompletion +where + C: Debug + Subcommand + strum::IntoEnumIterator, +{ + commands: std::marker::PhantomData, +} + +impl Repl for DefaultRepl +where + C: Debug + Subcommand + strum::IntoEnumIterator, +{ + fn new() -> Self { + Self { + command: None, + buf_preparsed: Vec::new(), + buf: String::new(), + history: BasicHistory::new(), + completion: DefaultReplCompletion::new(), + } + } + fn command(&self) -> &Option { + &self.command + } + fn step(&mut self) -> Result<(), super::error::Error> { + self.buf.clear(); + + // NOTE: display::Input requires some kind of lifetime that would be a bother to store in + // our struct. It's documentation also uses it in place, so it should be fine to do it like + // this. + // + // NOTE: It would be nice if we could use the Validator mechanism of dialoguer, but + // unfortunately we can only process our input after we've preparsed it and we need an + // actual output. If we could set a status after the Input is over that would be amazing, + // but that is currently not supported by dialoguer. + // Therefore, every prompt will show as success regardless. + self.buf = dialoguer::Input::with_theme(&dialoguer::theme::ColorfulTheme::default()) + .completion_with(&self.completion) + .history_with(&mut self.history) + .interact_text()?; + + self.buf_preparsed = Vec::new(); + self.buf_preparsed + .extend(shlex::split(&self.buf).unwrap_or_default()); + + trace!("read input: {:?}", self.buf_preparsed); + trace!("repl after step: {:#?}", self); + + // HACK: find a way to not allocate a new struct for this + let cmds = Self::try_parse_from(&self.buf_preparsed)?; + self.command = cmds.command; + Ok(()) + } +} + +impl Default for DefaultRepl +where + C: Debug + Subcommand + strum::IntoEnumIterator, +{ + fn default() -> Self { + Self::new() + } +} + +impl Debug for DefaultRepl +where + C: Debug + Subcommand + strum::IntoEnumIterator, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DefaultRepl") + .field("command", &self.command) + .field("buf", &self.buf) + .field("buf_preparsed", &self.buf_preparsed) + .field("completion", &self.completion) + .field("history", &"(no debug)") + .finish() + } +} + +impl DefaultReplCompletion +where + C: Debug + Subcommand + strum::IntoEnumIterator, +{ + /// Make a new [`DefaultReplCompletion`] for the type `C` + pub const fn new() -> Self { + Self { + commands: std::marker::PhantomData::, + } + } + fn commands() -> Vec { + let mut buf = Vec::new(); + // every crate has the help command, but it is not part of the enum + buf.push("help".to_string()); + for c in C::iter() { + // HACK: this is a horrible way to do this + // I just need the names of the commands + buf.push( + format!("{c:?}") + .split_whitespace() + .map(str::to_lowercase) + .next() + .unwrap() + .to_string(), + ); + } + trace!("commands: {buf:?}"); + buf + } +} + +impl Default for DefaultReplCompletion +where + C: Debug + Subcommand + strum::IntoEnumIterator, +{ + fn default() -> Self { + Self::new() + } +} + +impl Completion for DefaultReplCompletion +where + C: Debug + Subcommand + strum::IntoEnumIterator, +{ + /// Simple completion implementation based on substring + fn get(&self, input: &str) -> Option { + let matches = Self::commands() + .into_iter() + .filter(|option| option.starts_with(input)) + .collect::>(); + + trace!("\nmatches: {matches:#?}"); + if matches.len() == 1 { + Some(matches[0].to_string()) + } else { + None + } + } +} diff --git a/members/libpt-cli/src/repl/error.rs b/members/libpt-cli/src/repl/error.rs new file mode 100644 index 0000000..3cb9546 --- /dev/null +++ b/members/libpt-cli/src/repl/error.rs @@ -0,0 +1,13 @@ +//! Errors for the Repl module + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Parsing(#[from] clap::Error), + #[error(transparent)] + Input(#[from] dialoguer::Error), + #[error(transparent)] + Other(#[from] anyhow::Error), +} diff --git a/members/libpt-cli/src/repl/mod.rs b/members/libpt-cli/src/repl/mod.rs new file mode 100644 index 0000000..f4e9cae --- /dev/null +++ b/members/libpt-cli/src/repl/mod.rs @@ -0,0 +1,48 @@ +//! Create easy and well defined REPLs +//! +//! A REPL is a [Read-Eval-Print-Loop](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop). +//! Well known examples for REPLs are shells (like bash). +//! +//! This module offers a convenient way to create a well-defined REPL without a lot of complicated +//! code and with a visually pleasing aesthetic. An example REPL implementation can be found in the +//! examples. +//! +//! The basic idea is that the user defines the commands with an enum and uses [claps](clap) +//! `#[derive(Subcommand)]`. A loop is then used to read from the stdin into a buffer, that buffer +//! is put to [clap] for parsing, similar to how [clap] would parse commandline arguments. + +use std::fmt::Debug; + +pub mod error; +use error::Error; +mod default; +pub use default::*; + +use clap::{Parser, Subcommand}; + +/// Common Trait for repl objects +/// +/// Unless you want to implement custom features (not just commands), just use [`DefaultRepl`]. +pub trait Repl: Parser + Debug +where + C: Debug + Subcommand + strum::IntoEnumIterator, +{ + /// create a new repl + fn new() -> Self; + /// get the command that was parsed from user input + /// + /// Will only be [None] if the repl has not had [step](Repl::step) executed yet. + fn command(&self) -> &Option; + /// advance the repl to the next iteration of the main loop + /// + /// This should be used at the start of your loop. + /// + /// Note that the help menu is an Error: [`clap::error::ErrorKind::DisplayHelp`] + /// + /// # Errors + /// + /// * [`Error::Input`] – [dialoguer] User Input had some kind of I/O Error + /// * [`Error::Parsing`] – [clap] could not parse the user input, or user requested help + /// * [`Error::Other`] – Any other error with [anyhow], [`DefaultRepl`] does not use this but custom implementations might + fn step(&mut self) -> Result<(), Error>; +} diff --git a/members/libpt-core/src/lib.rs b/members/libpt-core/src/lib.rs index 3245b8c..848cd5c 100644 --- a/members/libpt-core/src/lib.rs +++ b/members/libpt-core/src/lib.rs @@ -8,5 +8,3 @@ /// macros to make things faster in your code pub mod macros; -/// some general use printing to stdout tools -pub mod printing; diff --git a/members/libpt-core/src/printing.rs b/members/libpt-core/src/printing.rs deleted file mode 100644 index 808de0b..0000000 --- a/members/libpt-core/src/printing.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! # tools that make printing stuff better - -/// Quickly get a one line visual divider -pub fn divider() -> String { - format!("{:=^80}", "=") -} - -/// Quickly print a one line visual divider -pub fn print_divider() { - println!("{:=^80}", "=") -} diff --git a/members/libpt-log/Cargo.toml b/members/libpt-log/Cargo.toml index 1a73ddd..e1645ab 100644 --- a/members/libpt-log/Cargo.toml +++ b/members/libpt-log/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "libpt-log" publish.workspace = true -version = "0.4.2" +version = "0.4.3" edition.workspace = true authors.workspace = true license.workspace = true diff --git a/members/libpt-log/src/lib.rs b/members/libpt-log/src/lib.rs index e794b8b..47493dc 100644 --- a/members/libpt-log/src/lib.rs +++ b/members/libpt-log/src/lib.rs @@ -23,6 +23,7 @@ use std::{ pub mod error; use error::*; +pub use tracing; pub use tracing::{debug, error, info, trace, warn, Level}; use tracing_appender::{self, non_blocking::NonBlocking}; use tracing_subscriber::fmt::{format::FmtSpan, time}; diff --git a/members/libpt-py/Cargo.toml b/members/libpt-py/Cargo.toml index 0422edc..8be932e 100644 --- a/members/libpt-py/Cargo.toml +++ b/members/libpt-py/Cargo.toml @@ -19,7 +19,7 @@ name = "libpt" crate-type = ["cdylib", "rlib"] [dependencies] -libpt = { version = "0.5.0", path = "../.." } +libpt = { version = "0.5.0"} pyo3 = { version = "0.19.0", features = ["full"] } anyhow.workspace = true diff --git a/members/libpt-py/src/lib.rs b/members/libpt-py/src/lib.rs index 36463b0..2d648a1 100644 --- a/members/libpt-py/src/lib.rs +++ b/members/libpt-py/src/lib.rs @@ -1,4 +1,4 @@ -//! Python bindings for [`libpt`](libpt) +//! Python bindings for [`libpt`] #[cfg(feature = "bintols")] mod bintols; diff --git a/src/lib.rs b/src/lib.rs index 02b30f7..307f723 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,12 @@ //! //! `pt` is a project consisting of multiple smaller crates, all bundled together in this //! "main crate". Most crates will only show up if you activate their feature. +#![warn(clippy::pedantic, clippy::style, clippy::nursery)] #[cfg_attr(docsrs, doc(cfg(feature = "full")))] #[cfg(feature = "bintols")] pub use libpt_bintols as bintols; +#[cfg(feature = "cli")] +pub use libpt_cli as cli; #[cfg(feature = "core")] pub use libpt_core as core; #[cfg(feature = "log")]