diff --git a/Cargo.lock b/Cargo.lock index 9d594aa1..02ee184f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1428,8 +1428,11 @@ dependencies = [ "starship_module_config_derive 0.1.1", "sysinfo 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "urlencoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1508,6 +1511,16 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "term_size" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "termcolor" version = "1.0.5" @@ -2180,6 +2193,7 @@ dependencies = [ "checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" "checksum sysinfo 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)" = "02067c0c215cbc50176ad0c8718183c5e6bfd3d97c6913c26abeae3bfa8ed2ae" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +"checksum term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9e5b9a66db815dcfd2da92db471106457082577c3c278d4138ab3e3b4e189327" "checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" diff --git a/Cargo.toml b/Cargo.toml index 67297059..cb298766 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,9 @@ open = "1.3.2" # OpenSSL causes problems when building a MUSL release. Opting to use native SSL implementation # see: https://github.com/richfelker/musl-cross-make/issues/65#issuecomment-509790889 reqwest = { version = "0.9.24", default-features = false, features = ["rustls-tls"] } +unicode-width = "0.1.7" +textwrap = "0.11.0" +term_size = "0.3.1" [dev-dependencies] tempfile = "3.1.0" diff --git a/src/context.rs b/src/context.rs index 97923dba..ead8e1e7 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,6 +1,7 @@ use crate::config::StarshipConfig; use crate::module::Module; +use crate::modules; use clap::ArgMatches; use git2::{Repository, RepositoryState}; use once_cell::sync::OnceCell; @@ -91,8 +92,9 @@ impl<'a> Context<'a> { /// Create a new module pub fn new_module(&self, name: &str) -> Module { let config = self.config.get_module_config(name); + let desc = modules::description(name); - Module::new(name, config) + Module::new(name, desc, config) } /// Check if `disabled` option of the module is true in configuration file. diff --git a/src/main.rs b/src/main.rs index 9fb1e0fe..9b917a34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -122,6 +122,9 @@ fn main() { .about("Prints time in milliseconds") .settings(&[AppSettings::Hidden]), ) + .subcommand( + SubCommand::with_name("explain").about("Explains the currently showing modules"), + ) .get_matches(); match matches.subcommand() { @@ -157,6 +160,7 @@ fn main() { None => println!("{}", -1), } } + ("explain", Some(sub_m)) => print::explain(sub_m.clone()), _ => {} } } diff --git a/src/module.rs b/src/module.rs index 0643631c..3de985d6 100644 --- a/src/module.rs +++ b/src/module.rs @@ -50,6 +50,9 @@ pub struct Module<'a> { /// The module's name, to be used in configuration and logging. _name: String, + /// The module's description + description: String, + /// The styling to be inherited by all segments contained within this module. style: Style, @@ -65,10 +68,11 @@ pub struct Module<'a> { impl<'a> Module<'a> { /// Creates a module with no segments. - pub fn new(name: &str, config: Option<&'a toml::Value>) -> Module<'a> { + pub fn new(name: &str, desc: &str, config: Option<&'a toml::Value>) -> Module<'a> { Module { config, _name: name.to_string(), + description: desc.to_string(), style: Style::default(), prefix: Affix::default_prefix(name), segments: Vec::new(), @@ -91,11 +95,20 @@ impl<'a> Module<'a> { &self._name } + /// Get module's description + pub fn get_description(&self) -> &String { + &self.description + } + /// Whether a module has non-empty segments pub fn is_empty(&self) -> bool { self.segments.iter().all(|segment| segment.is_empty()) } + pub fn get_segments(&self) -> Vec<&str> { + self.segments.iter().map(Segment::get_value).collect() + } + /// Get the module's prefix pub fn get_prefix(&mut self) -> &mut Affix { &mut self.prefix @@ -120,7 +133,10 @@ impl<'a> Module<'a> { /// Returns a vector of colored ANSIString elements to be later used with /// `ANSIStrings()` to optimize ANSI codes pub fn ansi_strings(&self) -> Vec { - let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default(); + self.ansi_strings_for_prompt(true) + } + + pub fn ansi_strings_for_prompt(&self, is_prompt: bool) -> Vec { let mut ansi_strings = self .segments .iter() @@ -130,11 +146,14 @@ impl<'a> Module<'a> { ansi_strings.insert(0, self.prefix.ansi_string()); ansi_strings.push(self.suffix.ansi_string()); - ansi_strings = match shell.as_str() { - "bash" => ansi_strings_modified(ansi_strings, shell), - "zsh" => ansi_strings_modified(ansi_strings, shell), - _ => ansi_strings, - }; + if is_prompt { + let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default(); + ansi_strings = match shell.as_str() { + "bash" => ansi_strings_modified(ansi_strings, shell), + "zsh" => ansi_strings_modified(ansi_strings, shell), + _ => ansi_strings, + }; + } ansi_strings } @@ -261,9 +280,11 @@ mod tests { #[test] fn test_module_is_empty_with_no_segments() { let name = "unit_test"; + let desc = "This is a unit test"; let module = Module { config: None, _name: name.to_string(), + description: desc.to_string(), style: Style::default(), prefix: Affix::default_prefix(name), segments: Vec::new(), @@ -276,9 +297,11 @@ mod tests { #[test] fn test_module_is_empty_with_all_empty_segments() { let name = "unit_test"; + let desc = "This is a unit test"; let module = Module { config: None, _name: name.to_string(), + description: desc.to_string(), style: Style::default(), prefix: Affix::default_prefix(name), segments: vec![Segment::new("test_segment")], diff --git a/src/modules/mod.rs b/src/modules/mod.rs index a95ea4fd..57928576 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -78,3 +78,41 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { } } } + +pub fn description(module: &str) -> &'static str { + match module { + "aws" => "The current AWS region and profile", + "battery" => "The current charge of the device's battery and its current charging status", + "character" => { + "A character (usually an arrow) beside where the text is entered in your terminal" + } + "cmd_duration" => "How long the last command took to execute", + "conda" => "The current conda environment, if $CONDA_DEFAULT_ENV is set", + "directory" => "The current working directory", + "dotnet" => "The relevant version of the .NET Core SDK for the current directory", + "env_var" => "Displays the current value of a selected environment variable", + "git_branch" => "The active branch of the repo in your current directory", + "git_commit" => "The active branch of the repo in your current directory", + "git_state" => "The current git operation, and it's progress", + "git_status" => "Symbol representing the state of the repo", + "golang" => "The currently installed version of Golang", + "hg_branch" => "The active branch of the repo in your current directory", + "hostname" => "The system hostname", + "java" => "The currently installed version of Java", + "jobs" => "The current number of jobs running", + "kubernetes" => "The current Kubernetes context name and, if set, the namespace", + "line_break" => "Separates the prompt into two lines", + "memory_usage" => "Current system memory and swap usage", + "nix_shell" => "The nix-shell environment", + "nodejs" => "The currently installed version of NodeJS", + "package" => "The package version of the current directory's project", + "php" => "The currently installed version of PHP", + "python" => "The currently installed version of Python", + "ruby" => "The currently installed version of Ruby", + "rust" => "The currently installed version of Rust", + "terraform" => "The currently selected terraform workspace and version", + "time" => "The current local time", + "username" => "The active user's username", + _ => "", + } +} diff --git a/src/print.rs b/src/print.rs index 70826827..ee3b3666 100644 --- a/src/print.rs +++ b/src/print.rs @@ -2,6 +2,7 @@ use clap::ArgMatches; use rayon::prelude::*; use std::fmt::Write as FmtWrite; use std::io::{self, Write}; +use unicode_width::UnicodeWidthChar; use crate::context::Context; use crate::module::Module; @@ -26,27 +27,7 @@ pub fn get_prompt(context: Context) -> String { buf.push_str("\x1b[J"); - let mut prompt_order: Vec<&str> = Vec::new(); - - // Write out a custom prompt order - for module in config.prompt_order { - if ALL_MODULES.contains(&module) { - prompt_order.push(module); - } else { - log::debug!( - "Expected prompt_order to contain value from {:?}. Instead received {}", - ALL_MODULES, - module, - ); - } - } - - let modules = &prompt_order - .par_iter() - .filter(|module| !context.is_module_disabled_in_config(module)) - .map(|module| modules::handle(module, &context)) // Compute modules - .flatten() - .collect::>(); // Remove segments set to `None` + let modules = compute_modules(&context); let mut print_without_prefix = true; let printable = modules.iter(); @@ -76,3 +57,99 @@ pub fn module(module_name: &str, args: ArgMatches) { print!("{}", module); } + +pub fn explain(args: ArgMatches) { + let context = Context::new(args); + + struct ModuleInfo { + value: String, + value_len: usize, + desc: String, + } + + let dont_print = vec!["line_break", "character"]; + + let modules = compute_modules(&context) + .into_iter() + .filter(|module| !dont_print.contains(&module.get_name().as_str())) + .map(|module| { + let ansi_strings = module.ansi_strings_for_prompt(false); + let value = module.get_segments().join(""); + ModuleInfo { + value: ansi_term::ANSIStrings(&ansi_strings[1..ansi_strings.len() - 1]).to_string(), + value_len: value.chars().count() + count_wide_chars(&value), + desc: module.get_description().to_owned(), + } + }) + .collect::>(); + + let mut max_ansi_module_width = 0; + let mut max_module_width = 0; + + for info in &modules { + max_ansi_module_width = std::cmp::max( + max_ansi_module_width, + info.value.chars().count() + count_wide_chars(&info.value), + ); + max_module_width = std::cmp::max(max_module_width, info.value_len); + } + + let desc_width = term_size::dimensions() + .map(|(w, _)| w) + .map(|width| width - std::cmp::min(width, max_ansi_module_width)); + + println!("\n Here's a breakdown of your prompt:"); + for info in modules { + let wide_chars = count_wide_chars(&info.value); + + if let Some(desc_width) = desc_width { + let wrapped = textwrap::fill(&info.desc, desc_width); + let mut lines = wrapped.split('\n'); + println!( + " {:width$} - {}", + info.value, + lines.next().unwrap(), + width = max_ansi_module_width - wide_chars + ); + + for line in lines { + println!("{}{}", " ".repeat(max_module_width + 6), line.trim()); + } + } else { + println!( + " {:width$} - {}", + info.value, + info.desc, + width = max_ansi_module_width - wide_chars + ); + }; + } +} + +fn compute_modules<'a>(context: &'a Context) -> Vec> { + let mut prompt_order: Vec<&str> = Vec::new(); + + // Write out a custom prompt order + for module in context.config.get_root_config().prompt_order { + if ALL_MODULES.contains(&module) { + prompt_order.push(module); + } else { + log::debug!( + "Expected prompt_order to contain value from {:?}. Instead received {}", + ALL_MODULES, + module, + ); + } + } + + prompt_order + .par_iter() + .filter(|module| !context.is_module_disabled_in_config(module)) + .map(|module| modules::handle(module, &context)) // Compute modules + .flatten() // Remove segments set to `None` + .collect::>>() +} + +fn count_wide_chars(value: &str) -> usize { + value.chars().filter(|c| c.width().unwrap_or(0) > 1).count() +} diff --git a/src/segment.rs b/src/segment.rs index 8c0140ac..d01034bb 100644 --- a/src/segment.rs +++ b/src/segment.rs @@ -45,6 +45,11 @@ impl Segment { self } + /// Gets the value of the segment. + pub fn get_value(&self) -> &str { + &self.value + } + // Returns the ANSIString of the segment value, not including its prefix and suffix pub fn ansi_string(&self) -> ANSIString { match self.style {