feat: Add the `starship explain` command (#699)
This adds the explain argument to Starship, which explains what the printed modules in the prompt are.
This commit is contained in:
parent
6bafe4cd66
commit
8e5fa60fc8
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ANSIString> {
|
||||
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<ANSIString> {
|
||||
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")],
|
||||
|
|
|
@ -78,3 +78,41 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
_ => "<no description>",
|
||||
}
|
||||
}
|
||||
|
|
119
src/print.rs
119
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::<Vec<Module>>(); // 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::<Vec<ModuleInfo>>();
|
||||
|
||||
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<Module<'a>> {
|
||||
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::<Vec<Module<'a>>>()
|
||||
}
|
||||
|
||||
fn count_wide_chars(value: &str) -> usize {
|
||||
value.chars().filter(|c| c.width().unwrap_or(0) > 1).count()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue