diff --git a/Cargo.lock b/Cargo.lock index 92051e66..f1f91f08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,17 +218,42 @@ dependencies = [ [[package]] name = "clap" -version = "2.34.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "d17bf219fcd37199b9a29e00ba65dfb8cd5b2688b7297ec14ff829c40ac50ca9" dependencies = [ - "ansi_term", "atty", "bitflags", - "strsim 0.8.0", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", "textwrap", - "unicode-width", - "vec_map", + "unicase", +] + +[[package]] +name = "clap_complete" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d123fbea4c5d9799cffd44051e2125c880efd23b3b7c529baf3ea5508c8736" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b9752c030a14235a0bd5ef3ad60a1dcac8468c30921327fc8af36b20c790b9" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1191,6 +1216,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + [[package]] name = "parking" version = "2.0.0" @@ -1344,6 +1378,30 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -1719,6 +1777,7 @@ dependencies = [ "byte-unit", "chrono", "clap", + "clap_complete", "directories-next", "gethostname", "git2", @@ -1748,7 +1807,7 @@ dependencies = [ "shell-words", "starship-battery", "starship_module_config_derive", - "strsim 0.10.0", + "strsim", "sys-info", "tempfile", "terminal_size", @@ -1795,12 +1854,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.10.0" @@ -1863,6 +1916,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.1.17" @@ -1875,9 +1937,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.11.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" dependencies = [ "unicode-width", ] @@ -1968,6 +2030,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.6" @@ -2041,12 +2112,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.3" @@ -2129,6 +2194,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index c9a46ff1..e7d25282 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,8 @@ battery = ["starship-battery"] tls-vendored = ["native-tls/vendored"] [dependencies] -clap = "2.34.0" +clap = { version = "3.0.0", features = ["derive", "cargo", "unicode"] } +clap_complete = "3.0.0" ansi_term = "0.12.1" directories-next = "2.0.0" git2 = { version = "0.13.25", default-features = false } diff --git a/src/configure.rs b/src/configure.rs index 7a6c6fb9..e3de2ada 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -73,7 +73,7 @@ fn handle_update_configuration(doc: &mut Document, name: &str, value: &str) -> R Ok(()) } -pub fn print_configuration(use_default: bool, paths: &[&str]) { +pub fn print_configuration(use_default: bool, paths: &[String]) { let config = if use_default { // Get default config let default_config = crate::configs::FullConfig::default(); @@ -94,7 +94,7 @@ pub fn print_configuration(use_default: bool, paths: &[&str]) { if paths.is_empty() || paths .iter() - .any(|&path| path == "format" || path == "right_format") + .any(|path| path == "format" || path == "right_format") { println!( "# $all is shorthand for {}", @@ -128,7 +128,7 @@ pub fn print_configuration(use_default: bool, paths: &[&str]) { println!("{}", string_config); } -fn extract_toml_paths(mut config: toml::Value, paths: &[&str]) -> toml::Value { +fn extract_toml_paths(mut config: toml::Value, paths: &[String]) -> toml::Value { // Extract all the requested sections into a new configuration. let mut subset = toml::value::Table::new(); let config = if let Some(config) = config.as_table_mut() { @@ -138,7 +138,7 @@ fn extract_toml_paths(mut config: toml::Value, paths: &[&str]) -> toml::Value { return toml::Value::Table(subset); }; - 'paths: for &path in paths { + 'paths: for path in paths { let path_segments: Vec<_> = path.split('.').collect(); let (&end, parents) = path_segments.split_last().unwrap_or((&"", &[])); @@ -414,9 +414,9 @@ mod tests { let actual_config = extract_toml_paths( config, &[ - "extract_root", - "extract_section", - "extract_subsection.extracted", + "extract_root".to_owned(), + "extract_section".to_owned(), + "extract_subsection.extracted".to_owned(), ], ); diff --git a/src/context.rs b/src/context.rs index 6c24c06d..800cf3e6 100644 --- a/src/context.rs +++ b/src/context.rs @@ -5,14 +5,17 @@ use crate::utils::{create_command, exec_timeout, CommandOutput}; use crate::modules; use crate::utils::{self, home_dir}; -use clap::ArgMatches; +use clap::Parser; use git2::{ErrorCode::UnbornBranch, Repository, RepositoryState}; use once_cell::sync::OnceCell; -use std::collections::{HashMap, HashSet}; +#[cfg(test)] +use std::collections::HashMap; +use std::collections::HashSet; use std::env; use std::ffi::{OsStr, OsString}; use std::fmt::Debug; use std::fs; +use std::num::ParseIntError; use std::path::{Path, PathBuf}; use std::string::String; use std::time::{Duration, Instant}; @@ -37,7 +40,7 @@ pub struct Context<'a> { dir_contents: OnceCell, /// Properties to provide to modules. - pub properties: HashMap<&'a str, String>, + pub properties: Properties, /// Pipestatus of processes in pipe pub pipestatus: Option>, @@ -48,11 +51,8 @@ pub struct Context<'a> { /// The shell the user is assumed to be running pub shell: Shell, - /// Construct the right prompt instead of the left prompt - pub right: bool, - - /// Construct the continuation prompt instead of the normal prompt - pub continuation: bool, + /// Which prompt to print (main, right, ...) + pub target: Target, /// Width of terminal, or zero if width cannot be detected. pub width: usize, @@ -76,53 +76,46 @@ impl<'a> Context<'a> { /// Identify the current working directory and create an instance of Context /// for it. "logical-path" is used when a shell allows the "current working directory" /// to be something other than a file system path (like powershell provider specific paths). - pub fn new(arguments: ArgMatches) -> Context { + pub fn new(arguments: Properties, target: Target) -> Context<'a> { let shell = Context::get_shell(); // Retrieve the "current directory". // If the path argument is not set fall back to the OS current directory. let path = arguments - .value_of("path") - .map(PathBuf::from) + .path + .clone() .or_else(|| env::current_dir().ok()) .or_else(|| env::var("PWD").map(PathBuf::from).ok()) - .or_else(|| arguments.value_of("logical_path").map(PathBuf::from)) + .or_else(|| arguments.logical_path.clone()) .unwrap_or_default(); // Retrieve the "logical directory". // If the path argument is not set fall back to the PWD env variable set by many shells // or to the other path. let logical_path = arguments - .value_of("logical_path") - .map(PathBuf::from) + .logical_path + .clone() .or_else(|| env::var("PWD").map(PathBuf::from).ok()) .unwrap_or_else(|| path.clone()); - Context::new_with_shell_and_path(arguments, shell, path, logical_path) + Context::new_with_shell_and_path(arguments, shell, target, path, logical_path) } /// Create a new instance of Context for the provided directory pub fn new_with_shell_and_path( - arguments: ArgMatches, + properties: Properties, shell: Shell, + target: Target, path: PathBuf, logical_path: PathBuf, - ) -> Context { + ) -> Context<'a> { let config = StarshipConfig::initialize(); - // Unwrap the clap arguments into a simple hashtable - let properties: HashMap<&str, std::string::String> = arguments - .args - .iter() - .filter(|(_, v)| !v.vals.is_empty()) - .map(|(a, b)| (*a, b.vals.first().cloned().unwrap().into_string().unwrap())) - .collect(); - - let pipestatus = arguments - .values_of("pipestatus") + let pipestatus = properties + .pipestatus + .as_deref() .map(Context::get_and_flatten_pipestatus) .flatten(); - log::trace!("Received completed pipestatus of {:?}", pipestatus); // Canonicalize the current path to resolve symlinks, etc. @@ -136,14 +129,7 @@ impl<'a> Context<'a> { .as_ref() .map_or_else(StarshipRootConfig::default, StarshipRootConfig::load); - let right = arguments.is_present("right"); - let continuation = arguments.is_present("continuation"); - - let width = arguments - .value_of("terminal_width") - .and_then(|w| w.parse().ok()) - .or_else(|| terminal_size().map(|(w, _)| w.0 as usize)) - .unwrap_or(80); + let width = properties.terminal_width; Context { config, @@ -154,8 +140,7 @@ impl<'a> Context<'a> { dir_contents: OnceCell::new(), repo: OnceCell::new(), shell, - right, - continuation, + target, width, #[cfg(test)] env: HashMap::new(), @@ -212,12 +197,13 @@ impl<'a> Context<'a> { } /// Reads and appropriately flattens multiple args for pipestatus - pub fn get_and_flatten_pipestatus(args: clap::Values) -> Option> { + // TODO: Replace with value_delimiter = ' ' clap option? + pub fn get_and_flatten_pipestatus(args: &[String]) -> Option> { // Due to shell differences, we can potentially receive individual or space // separated inputs, e.g. "0","1","2","0" is the same as "0 1 2 0" and // "0 1", "2 0". We need to accept all these formats and return a Vec let parsed_vals = args - .into_iter() + .iter() .map(|x| x.split_ascii_whitespace()) .flatten() .map(|x| x.to_string()) @@ -310,8 +296,12 @@ impl<'a> Context<'a> { } } + // TODO: This should be used directly by clap parse pub fn get_cmd_duration(&self) -> Option { - self.properties.get("cmd_duration")?.parse::().ok() + self.properties + .cmd_duration + .as_deref() + .and_then(|cd| cd.parse::().ok()) } /// Execute a command and return the output on stdout and stderr if successful @@ -574,6 +564,67 @@ pub enum Shell { Unknown, } +fn default_width() -> usize { + terminal_size().map_or(80, |(w, _)| w.0 as usize) +} + +/// Which kind of prompt target to print (main prompt, rprompt, ...) +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Target { + Main, + Right, + Continuation, +} + +/// Properties as passed on from the shell as arguments +#[derive(Parser, Debug)] +pub struct Properties { + /// The status code of the previously run command + #[clap(short = 's', long = "status")] + pub status_code: Option, + /// Bash and Zsh support returning codes for each process in a pipeline. + #[clap(long)] + pipestatus: Option>, + /// The width of the current interactive terminal. + #[clap(short = 'w', long, default_value_t=default_width())] + terminal_width: usize, + /// The path that the prompt should render for. + #[clap(short, long)] + path: Option, + /// The logical path that the prompt should render for. + /// This path should be a virtual/logical representation of the PATH argument. + #[clap(short = 'P', long)] + logical_path: Option, + /// The execution duration of the last command, in milliseconds + #[clap(short = 'd', long)] + pub cmd_duration: Option, + /// The keymap of fish/zsh + #[clap(short = 'k', long, default_value = "viins")] + pub keymap: String, + /// The number of currently running jobs + #[clap(short, long, default_value_t, parse(try_from_str=parse_jobs))] + pub jobs: i64, +} + +impl Default for Properties { + fn default() -> Self { + Properties { + status_code: None, + pipestatus: None, + terminal_width: default_width(), + path: None, + logical_path: None, + cmd_duration: None, + keymap: "viins".to_string(), + jobs: 0, + } + } +} + +fn parse_jobs(jobs: &str) -> Result { + jobs.trim().parse::() +} + #[cfg(test)] mod tests { use super::*; @@ -660,8 +711,9 @@ mod tests { // Mock navigation into the symlink path let test_path = path_symlink.join("yyy"); let context = Context::new_with_shell_and_path( - ArgMatches::default(), + Default::default(), Shell::Unknown, + Target::Main, test_path.clone(), test_path.clone(), ); @@ -685,8 +737,9 @@ mod tests { // Mock navigation to a directory which does not exist on disk let test_path = Path::new("/path_which_does_not_exist").to_path_buf(); let context = Context::new_with_shell_and_path( - ArgMatches::default(), + Default::default(), Shell::Unknown, + Target::Main, test_path.clone(), test_path.clone(), ); @@ -705,8 +758,9 @@ mod tests { // Mock navigation to a directory which does not exist on disk let test_path = Path::new("~/path_which_does_not_exist").to_path_buf(); let context = Context::new_with_shell_and_path( - ArgMatches::default(), + Default::default(), Shell::Unknown, + Target::Main, test_path.clone(), test_path.clone(), ); diff --git a/src/main.rs b/src/main.rs index 028e9838..c41926d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,297 +4,165 @@ use clap::crate_authors; use std::io; use std::time::SystemTime; -use clap::{App, AppSettings, Arg, Shell, SubCommand}; +use clap::{AppSettings, IntoApp, Parser, Subcommand}; +use clap_complete::{generate, Shell as CompletionShell}; use rand::distributions::Alphanumeric; use rand::Rng; +use starship::context::{Properties, Target}; use starship::module::ALL_MODULES; use starship::*; +fn long_version() -> &'static str { + let ver = Box::new(crate::shadow::clap_version()); + Box::leak(ver).as_str() +} + +#[derive(Parser, Debug)] +#[clap( + author=crate_authors!(), + version=shadow::PKG_VERSION, + long_version=long_version(), + about="The cross-shell prompt for astronauts. ☄🌌️" +)] +#[clap(setting(AppSettings::SubcommandRequiredElseHelp))] +struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Create a pre-populated GitHub issue with information about your configuration + BugReport, + /// Generate starship shell completions for your shell to stdout + Completions { + #[clap(arg_enum)] + shell: CompletionShell, + }, + /// Edit the starship configuration + Config { + /// Configuration key to edit + #[clap(requires = "value")] + name: Option, + /// Value to place into that key + value: Option, + }, + /// Explains the currently showing modules + Explain(Properties), + /// Prints the shell function used to execute starship + Init { + shell: String, + #[clap(long)] + print_full_init: bool, + }, + /// Prints a specific prompt module + Module { + /// The name of the module to be printed + #[clap(required = true, required_unless_present = "list")] + name: Option, + /// List out all supported modules + #[clap(short, long)] + list: bool, + #[clap(flatten)] + properties: Properties, + }, + /// Prints the computed starship configuration + PrintConfig { + /// Print the default instead of the computed config + #[clap(short, long)] + default: bool, + /// Configuration keys to print + name: Vec, + }, + /// Prints the full starship prompt + Prompt { + /// Print the right prompt (instead of the standard left prompt) + #[clap(long)] + right: bool, + /// Print the continuation prompt (instead of the standard left prompt) + #[clap(long, conflicts_with = "right")] + continuation: bool, + #[clap(flatten)] + properties: Properties, + }, + /// Generate random session key + Session, + /// Prints time in milliseconds + #[clap(setting=AppSettings::Hidden)] + Time, + /// Prints timings of all active modules + Timings(Properties), + /// Toggle a given starship module + Toggle { + /// The name of the module to be toggled + name: String, + /// The key of the config to be toggled + #[clap(default_value = "disabled")] + value: String, + }, +} + fn main() { // Configure the current terminal on windows to support ANSI escape sequences. #[cfg(windows)] let _ = ansi_term::enable_ansi_support(); logger::init(); - let status_code_arg = Arg::with_name("status_code") - .short("s") - .long("status") - .value_name("STATUS_CODE") - .help("The status code of the previously run command") - .takes_value(true); + let args = Cli::parse(); + log::trace!("Parsed arguments: {:#?}", args); - let pipestatus_arg = Arg::with_name("pipestatus") - .long("pipestatus") - .value_name("PIPESTATUS") - .help("Status codes from a command pipeline") - .long_help("Bash and Zsh supports returning codes for each process in a pipeline.") - .multiple(true); - - let terminal_width_arg = Arg::with_name("terminal_width") - .short("w") - .long("terminal-width") - .value_name("TERMINAL_WIDTH") - .help("The width of the current interactive terminal.") - .takes_value(true); - - let path_arg = Arg::with_name("path") - .short("p") - .long("path") - .value_name("PATH") - .help("The path that the prompt should render for.") - .takes_value(true); - - let logical_path_arg = Arg::with_name("logical_path") - .short("P") - .long("logical-path") - .value_name("LOGICAL_PATH") - .help(concat!( - "The logical path that the prompt should render for. ", - "This path should be a virtual/logical representation of the PATH argument." - )) - .takes_value(true); - - let shell_arg = Arg::with_name("shell") - .value_name("SHELL") - .help( - "The name of the currently running shell\nCurrently supported options: bash, zsh, fish, powershell, ion, elvish, tcsh, nu, xonsh", - ) - .required(true); - - let cmd_duration_arg = Arg::with_name("cmd_duration") - .short("d") - .long("cmd-duration") - .value_name("CMD_DURATION") - .help("The execution duration of the last command, in milliseconds") - .takes_value(true); - - let keymap_arg = Arg::with_name("keymap") - .short("k") - .long("keymap") - .value_name("KEYMAP") - // fish/zsh only - .help("The keymap of fish/zsh") - .takes_value(true); - - let jobs_arg = Arg::with_name("jobs") - .short("j") - .long("jobs") - .value_name("JOBS") - .help("The number of currently running jobs") - .takes_value(true); - - let init_scripts_arg = Arg::with_name("print_full_init") - .long("print-full-init") - .help("Print the main initialization script (as opposed to the init stub)"); - - let long_version = crate::shadow::clap_version(); - let mut app = App::new("starship") - .about("The cross-shell prompt for astronauts. ☄🌌️") - // pull the version number from Cargo.toml - .version(shadow::PKG_VERSION) - .long_version(long_version.as_str()) - // pull the authors from Cargo.toml - .author(crate_authors!()) - .after_help("https://github.com/starship/starship") - .setting(AppSettings::SubcommandRequiredElseHelp) - .subcommand( - SubCommand::with_name("init") - .about("Prints the shell function used to execute starship") - .arg(&shell_arg) - .arg(&init_scripts_arg), - ) - .subcommand( - SubCommand::with_name("prompt") - .about("Prints the full starship prompt") - .arg( - Arg::with_name("right") - .long("right") - .help("Print the right prompt (instead of the standard left prompt)"), - ) - .arg( - Arg::with_name("continuation") - .long("continuation") - .help("Print the continuation prompt (instead of the standard left prompt)") - .conflicts_with("right"), - ) - .arg(&status_code_arg) - .arg(&pipestatus_arg) - .arg(&terminal_width_arg) - .arg(&path_arg) - .arg(&logical_path_arg) - .arg(&cmd_duration_arg) - .arg(&keymap_arg) - .arg(&jobs_arg), - ) - .subcommand( - SubCommand::with_name("module") - .about("Prints a specific prompt module") - .arg( - Arg::with_name("name") - .help("The name of the module to be printed") - .required(true) - .required_unless("list"), - ) - .arg( - Arg::with_name("list") - .short("l") - .long("list") - .help("List out all supported modules"), - ) - .arg(&status_code_arg) - .arg(&pipestatus_arg) - .arg(&terminal_width_arg) - .arg(&path_arg) - .arg(&logical_path_arg) - .arg(&cmd_duration_arg) - .arg(&keymap_arg) - .arg(&jobs_arg), - ) - .subcommand( - SubCommand::with_name("config") - .alias("configure") - .about("Edit the starship configuration") - .arg( - Arg::with_name("name") - .help("Configuration key to edit") - .required(false) - .requires("value"), - ) - .arg(Arg::with_name("value").help("Value to place into that key")), - ) - .subcommand( - SubCommand::with_name("print-config") - .about("Prints the computed starship configuration") - .arg( - Arg::with_name("default") - .short("d") - .long("default") - .help("Print the default instead of the computed config") - .takes_value(false), - ) - .arg( - Arg::with_name("name") - .help("Configuration keys to print") - .multiple(true) - .required(false), - ), - ) - .subcommand( - SubCommand::with_name("toggle") - .about("Toggle a given starship module") - .arg( - Arg::with_name("name") - .help("The name of the module to be toggled") - .required(true), - ) - .arg( - Arg::with_name("key") - .help("The key of the config to be toggled") - .required(false) - .required_unless("name"), - ), - ) - .subcommand( - SubCommand::with_name("bug-report").about( - "Create a pre-populated GitHub issue with information about your configuration", - ), - ) - .subcommand( - SubCommand::with_name("time") - .about("Prints time in milliseconds") - .settings(&[AppSettings::Hidden]), - ) - .subcommand( - SubCommand::with_name("explain") - .about("Explains the currently showing modules") - .arg(&status_code_arg) - .arg(&pipestatus_arg) - .arg(&terminal_width_arg) - .arg(&path_arg) - .arg(&logical_path_arg) - .arg(&cmd_duration_arg) - .arg(&keymap_arg) - .arg(&jobs_arg), - ) - .subcommand( - SubCommand::with_name("timings") - .about("Prints timings of all active modules") - .arg(&status_code_arg) - .arg(&pipestatus_arg) - .arg(&terminal_width_arg) - .arg(&path_arg) - .arg(&logical_path_arg) - .arg(&cmd_duration_arg) - .arg(&keymap_arg) - .arg(&jobs_arg), - ) - .subcommand( - SubCommand::with_name("completions") - .about("Generate starship shell completions for your shell to stdout") - .arg( - Arg::with_name("shell") - .takes_value(true) - .possible_values(&Shell::variants()) - .help("the shell to generate completions for") - .value_name("SHELL") - .required(true) - .env("STARSHIP_SHELL"), - ), - ) - .subcommand(SubCommand::with_name("session").about("Generate random session key")); - - let matches = app.clone().get_matches(); - - match matches.subcommand() { - ("init", Some(sub_m)) => { - let shell_name = sub_m.value_of("shell").expect("Shell name missing."); - if sub_m.is_present("print_full_init") { - init::init_main(shell_name).expect("can't init_main"); + match args.command { + Commands::Init { + shell, + print_full_init, + } => { + if print_full_init { + init::init_main(&shell).expect("can't init_main"); } else { - init::init_stub(shell_name).expect("can't init_stub"); + init::init_stub(&shell).expect("can't init_stub"); } } - ("prompt", Some(sub_m)) => print::prompt(sub_m.clone()), - ("module", Some(sub_m)) => { - if sub_m.is_present("list") { + Commands::Prompt { + properties, + right, + continuation, + } => { + let target = match (right, continuation) { + (true, _) => Target::Right, + (_, true) => Target::Continuation, + (_, _) => Target::Main, + }; + print::prompt(properties, target) + } + Commands::Module { + name, + list, + properties, + } => { + if list { println!("Supported modules list"); println!("----------------------"); for modules in ALL_MODULES { println!("{}", modules); } } - if let Some(module_name) = sub_m.value_of("name") { - print::module(module_name, sub_m.clone()); + if let Some(module_name) = name { + print::module(&module_name, properties); } } - ("config", Some(sub_m)) => { - if let Some(name) = sub_m.value_of("name") { - if let Some(value) = sub_m.value_of("value") { - configure::update_configuration(name, value) + Commands::Config { name, value } => { + if let Some(name) = name { + if let Some(value) = value { + configure::update_configuration(&name, &value) } } else { configure::edit_configuration() } } - ("print-config", Some(sub_m)) => { - let print_default = sub_m.is_present("default"); - let paths = sub_m - .values_of("name") - .map(|paths| paths.collect::>()) - .unwrap_or_default(); - configure::print_configuration(print_default, &paths) - } - ("toggle", Some(sub_m)) => { - if let Some(name) = sub_m.value_of("name") { - if let Some(value) = sub_m.value_of("key") { - configure::toggle_configuration(name, value) - } else { - configure::toggle_configuration(name, "disabled") - } - } - } - ("bug-report", Some(_)) => bug_report::create(), - ("time", _) => { + Commands::PrintConfig { default, name } => configure::print_configuration(default, &name), + Commands::Toggle { name, value } => configure::toggle_configuration(&name, &value), + Commands::BugReport => bug_report::create(), + Commands::Time => { match SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .ok() @@ -303,18 +171,15 @@ fn main() { None => println!("{}", -1), } } - ("explain", Some(sub_m)) => print::explain(sub_m.clone()), - ("timings", Some(sub_m)) => print::timings(sub_m.clone()), - ("completions", Some(sub_m)) => { - let shell: Shell = sub_m - .value_of("shell") - .expect("Shell name missing.") - .parse() - .expect("Invalid shell"); - - app.gen_completions_to("starship", shell, &mut io::stdout().lock()); - } - ("session", _) => println!( + Commands::Explain(props) => print::explain(props), + Commands::Timings(props) => print::timings(props), + Commands::Completions { shell } => generate( + shell, + &mut Cli::into_app(), + "starship", + &mut io::stdout().lock(), + ), + Commands::Session => println!( "{}", rand::thread_rng() .sample_iter(&Alphanumeric) @@ -322,6 +187,5 @@ fn main() { .map(char::from) .collect::() ), - (command, _) => unreachable!("Invalid subcommand: {}", command), } } diff --git a/src/modules/character.rs b/src/modules/character.rs index bf60f772..e2e7c1b8 100644 --- a/src/modules/character.rs +++ b/src/modules/character.rs @@ -22,9 +22,9 @@ pub fn module<'a>(context: &'a Context) -> Option> { let config: CharacterConfig = CharacterConfig::try_load(module.config); let props = &context.properties; - let exit_code = props.get("status_code").map_or("0", String::as_str); - let keymap = props.get("keymap").map_or("viins", String::as_str); - let exit_success = exit_code == "0"; + let exit_code = props.status_code; + let keymap = props.keymap.as_str(); + let exit_success = exit_code.unwrap_or_default() == 0; // Match shell "keymap" names to normalized vi modes // NOTE: in vi mode, fish reports normal mode as "default". diff --git a/src/modules/jobs.rs b/src/modules/jobs.rs index a202b292..36a05189 100644 --- a/src/modules/jobs.rs +++ b/src/modules/jobs.rs @@ -33,12 +33,7 @@ pub fn module<'a>(context: &'a Context) -> Option> { } let props = &context.properties; - let num_of_jobs = props - .get("jobs") - .map_or("0", String::as_str) - .trim() - .parse::() - .ok()?; + let num_of_jobs = props.jobs; if num_of_jobs == 0 && config.threshold > 0 diff --git a/src/modules/pulumi.rs b/src/modules/pulumi.rs index 90e42e28..7d07e87b 100644 --- a/src/modules/pulumi.rs +++ b/src/modules/pulumi.rs @@ -168,9 +168,9 @@ mod tests { use std::io; use super::*; + use crate::context::Target; use crate::test::ModuleRenderer; use ansi_term::Color; - use clap::ArgMatches; #[test] fn pulumi_version_release() { @@ -192,7 +192,7 @@ mod tests { #[test] fn get_home_dir() { - let mut context = Context::new(ArgMatches::default()); + let mut context = Context::new(Default::default(), Target::Main); context.env.insert("HOME", "/home/sweet/home".to_string()); assert_eq!( pulumi_home_dir(&context), @@ -204,7 +204,7 @@ mod tests { #[test] fn test_get_pulumi_workspace() { - let mut context = Context::new(ArgMatches::default()); + let mut context = Context::new(Default::default(), Target::Main); context.env.insert("HOME", "/home/sweet/home".to_string()); let name = "foobar"; let project_file = PathBuf::from("/hello/Pulumi.yaml"); diff --git a/src/modules/rust.rs b/src/modules/rust.rs index c28e847b..557c4afb 100644 --- a/src/modules/rust.rs +++ b/src/modules/rust.rs @@ -268,7 +268,7 @@ enum RustupRunRustcVersionOutcome { #[cfg(test)] mod tests { - use crate::context::Shell; + use crate::context::{Shell, Target}; use once_cell::sync::Lazy; use std::io; use std::process::{ExitStatus, Output}; @@ -424,6 +424,7 @@ mod tests { let context = Context::new_with_shell_and_path( Default::default(), Shell::Unknown, + Target::Main, dir.path().into(), dir.path().into(), ); @@ -444,6 +445,7 @@ mod tests { let context = Context::new_with_shell_and_path( Default::default(), Shell::Unknown, + Target::Main, dir.path().into(), dir.path().into(), ); @@ -464,6 +466,7 @@ mod tests { let context = Context::new_with_shell_and_path( Default::default(), Shell::Unknown, + Target::Main, dir.path().into(), dir.path().into(), ); @@ -486,6 +489,7 @@ mod tests { let context = Context::new_with_shell_and_path( Default::default(), Shell::Unknown, + Target::Main, child_dir_path.clone(), child_dir_path, ); @@ -505,6 +509,7 @@ mod tests { let context = Context::new_with_shell_and_path( Default::default(), Shell::Unknown, + Target::Main, dir.path().into(), dir.path().into(), ); @@ -522,6 +527,7 @@ mod tests { let context = Context::new_with_shell_and_path( Default::default(), Shell::Unknown, + Target::Main, dir.path().into(), dir.path().into(), ); @@ -542,6 +548,7 @@ mod tests { let context = Context::new_with_shell_and_path( Default::default(), Shell::Unknown, + Target::Main, dir.path().into(), dir.path().into(), ); @@ -564,6 +571,7 @@ mod tests { let context = Context::new_with_shell_and_path( Default::default(), Shell::Unknown, + Target::Main, child_dir_path.clone(), child_dir_path, ); diff --git a/src/modules/shell.rs b/src/modules/shell.rs index 19b88c9f..fc8c287c 100644 --- a/src/modules/shell.rs +++ b/src/modules/shell.rs @@ -11,7 +11,7 @@ pub fn module<'a>(context: &'a Context) -> Option> { return None; } - let shell = context.shell; + let shell = &context.shell; let parsed = StringFormatter::new(config.format).and_then(|formatter| { formatter diff --git a/src/modules/status.rs b/src/modules/status.rs index b811453d..5d280e20 100644 --- a/src/modules/status.rs +++ b/src/modules/status.rs @@ -28,10 +28,7 @@ pub fn module<'a>(context: &'a Context) -> Option> { return None; }; - let exit_code = context - .properties - .get("status_code") - .map_or("0", String::as_str); + let exit_code = context.properties.status_code.unwrap_or_default(); let pipestatus_status = match &context.pipestatus { None => PipeStatusStatus::Disabled, @@ -47,7 +44,7 @@ pub fn module<'a>(context: &'a Context) -> Option> { }; // Exit code is zero and pipestatus is all zero or disabled/missing - if exit_code == "0" + if exit_code == 0 && (match pipestatus_status { PipeStatusStatus::Pipe(ps) => ps.iter().all(|s| s == "0"), _ => true, @@ -79,7 +76,13 @@ pub fn module<'a>(context: &'a Context) -> Option> { PipeStatusStatus::Pipe(_) => config.pipestatus_format, _ => config.format, }; - let parsed = format_exit_code(exit_code, main_format, Some(&pipestatus), &config, context); + let parsed = format_exit_code( + &exit_code.to_string(), + main_format, + Some(&pipestatus), + &config, + context, + ); module.set_segments(match parsed { Ok(segments) => segments, diff --git a/src/print.rs b/src/print.rs index b7db0a10..79fc1a60 100644 --- a/src/print.rs +++ b/src/print.rs @@ -1,5 +1,4 @@ use ansi_term::ANSIStrings; -use clap::ArgMatches; use rayon::prelude::*; use std::collections::BTreeSet; use std::fmt::{self, Debug, Write as FmtWrite}; @@ -10,7 +9,7 @@ use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthChar; use crate::configs::PROMPT_ORDER; -use crate::context::{Context, Shell}; +use crate::context::{Context, Properties, Shell, Target}; use crate::formatter::{StringFormatter, VariableHolder}; use crate::module::Module; use crate::module::ALL_MODULES; @@ -54,8 +53,8 @@ fn test_grapheme_aware_width() { assert_eq!(11, "normal text".width_graphemes()); } -pub fn prompt(args: ArgMatches) { - let context = Context::new(args); +pub fn prompt(args: Properties, target: Target) { + let context = Context::new(args, target); let stdout = io::stdout(); let mut handle = stdout.lock(); write!(handle, "{}", get_prompt(context)).unwrap(); @@ -114,13 +113,13 @@ pub fn get_prompt(context: Context) -> String { ); let module_strings = root_module.ansi_strings_for_shell(context.shell, Some(context.width)); - if config.add_newline && !context.continuation { + if config.add_newline && context.target != Target::Continuation { // continuation prompts normally do not include newlines, but they can writeln!(buf).unwrap(); } write!(buf, "{}", ANSIStrings(&module_strings)).unwrap(); - if context.right { + if context.target == Target::Right { // right prompts generally do not allow newlines buf = buf.replace('\n', ""); } @@ -135,8 +134,8 @@ pub fn get_prompt(context: Context) -> String { buf } -pub fn module(module_name: &str, args: ArgMatches) { - let context = Context::new(args); +pub fn module(module_name: &str, args: Properties) { + let context = Context::new(args, Target::Main); let module = get_module(module_name, context).unwrap_or_default(); print!("{}", module); } @@ -145,8 +144,8 @@ pub fn get_module(module_name: &str, context: Context) -> Option { modules::handle(module_name, &context).map(|m| m.to_string()) } -pub fn timings(args: ArgMatches) { - let context = Context::new(args); +pub fn timings(args: Properties) { + let context = Context::new(args, Target::Main); struct ModuleTiming { name: String, @@ -191,8 +190,8 @@ pub fn timings(args: ArgMatches) { } } -pub fn explain(args: ArgMatches) { - let context = Context::new(args); +pub fn explain(args: Properties) { + let context = Context::new(args, Target::Main); struct ModuleInfo { value: String, @@ -431,16 +430,14 @@ fn load_formatter_and_modules<'a>(context: &'a Context) -> (StringFormatter<'a>, match (lformatter, rformatter, cformatter) { (Ok(lf), Ok(rf), Ok(cf)) => { let mut modules: BTreeSet = BTreeSet::new(); - if !context.continuation { + if context.target != Target::Continuation { modules.extend(lf.get_variables()); modules.extend(rf.get_variables()); } - if context.continuation { - (cf, modules) - } else if context.right { - (rf, modules) - } else { - (lf, modules) + match context.target { + Target::Main => (lf, modules), + Target::Right => (rf, modules), + Target::Continuation => (cf, modules), } } _ => (StringFormatter::raw(">"), BTreeSet::new()), @@ -464,7 +461,7 @@ mod test { }), }; context.root_config.right_format = "$character".to_string(); - context.right = true; + context.target = Target::Right; let expected = String::from(">>"); // should strip new lines let actual = get_prompt(context); @@ -480,7 +477,7 @@ mod test { }), }; context.root_config.continuation_prompt = "><>".to_string(); - context.continuation = true; + context.target = Target::Continuation; let expected = String::from("><>"); let actual = get_prompt(context); diff --git a/src/test/mod.rs b/src/test/mod.rs index acc4e75c..b0a32e33 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,4 +1,4 @@ -use crate::context::{Context, Shell}; +use crate::context::{Context, Shell, Target}; use crate::logger::StarshipLogger; use crate::{ config::{RootModuleConfig, StarshipConfig}, @@ -34,8 +34,9 @@ static LOGGER: Lazy<()> = Lazy::new(|| { pub fn default_context() -> Context<'static> { let mut context = Context::new_with_shell_and_path( - clap::ArgMatches::default(), + Default::default(), Shell::Unknown, + Target::Main, PathBuf::new(), PathBuf::new(), ); @@ -103,15 +104,13 @@ impl<'a> ModuleRenderer<'a> { self } - pub fn jobs(mut self, jobs: u64) -> Self { - self.context.properties.insert("jobs", jobs.to_string()); + pub fn jobs(mut self, jobs: i64) -> Self { + self.context.properties.jobs = jobs; self } pub fn cmd_duration(mut self, duration: u64) -> Self { - self.context - .properties - .insert("cmd_duration", duration.to_string()); + self.context.properties.cmd_duration = Some(duration.to_string()); self } @@ -119,14 +118,12 @@ impl<'a> ModuleRenderer<'a> { where T: Into, { - self.context.properties.insert("keymap", keymap.into()); + self.context.properties.keymap = keymap.into(); self } pub fn status(mut self, status: i32) -> Self { - self.context - .properties - .insert("status_code", status.to_string()); + self.context.properties.status_code = Some(status); self }