From 2ab07552e171cf7cad893cd9a5354611163e4c2a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Dec 2022 07:49:41 +0000 Subject: [PATCH 1/7] build(deps): update gitoxide crates --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be5e3d039..9dc167c22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1030,9 +1030,9 @@ dependencies = [ [[package]] name = "git-features" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "510591428bb22671eb60f56430975718af88fdae55a1489d403005f74c0d3c25" +checksum = "0f98e6ede7b790dfba16bf3c62861ae75c3719485d675b522cf7d7e748a4011c" dependencies = [ "crc32fast", "crossbeam-channel", @@ -1146,9 +1146,9 @@ dependencies = [ [[package]] name = "git-odb" -version = "0.38.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30a069e4c30d8aeabe41235f9a1595b60186a3cdfae73a7f3c89054e3e0d0ad" +checksum = "55333419bbb25aa6d39e29155f747ad8e1777fe385f70f447be9d680824d23dd" dependencies = [ "arc-swap", "git-features", @@ -1256,9 +1256,9 @@ dependencies = [ [[package]] name = "git-repository" -version = "0.30.1" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dbc1d1d0c346c66f18ea3c1393e8441f0729f759ec91965606b9f9ae6f2545c" +checksum = "1925a65a9fea6587e969a7a85cb239c8e1e438cf6dc520406df1b4c9d0e83bdc" dependencies = [ "git-actor", "git-attributes", @@ -1352,9 +1352,9 @@ dependencies = [ [[package]] name = "git-url" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc3a878147b2cc4bb3011ef7a290ecf4d6ab11c36e50fedd99ec52702dea98c" +checksum = "8651924c9692a778f09141ca44d1bf2dada229fe9b240f1ff1bdecd9621a1a93" dependencies = [ "bstr", "git-features", diff --git a/Cargo.toml b/Cargo.toml index b5dd11599..ec9a4120e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,9 +48,9 @@ clap_complete = "4.0.7" dirs-next = "2.0.0" dunce = "1.0.3" gethostname = "0.4.1" -git-features = { version = "0.25.0", optional = true } +git-features = { version = "0.25.1", optional = true } # default feature restriction addresses https://github.com/starship/starship/issues/4251 -git-repository = { version = "0.30.1", default-features = false, features = ["max-performance-safe"] } +git-repository = { version = "0.30.2", default-features = false, features = ["max-performance-safe"] } indexmap = { version = "1.9.2", features = ["serde"] } log = { version = "0.4.17", features = ["std"] } # nofity-rust is optional (on by default) because the crate doesn't currently build for darwin with nix From ddb17f8851fe8135f2b699897898b385d0906463 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Dec 2022 18:36:15 +0000 Subject: [PATCH 2/7] build(deps): update rust crate serde to 1.0.152 --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9dc167c22..25afbbc08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2619,18 +2619,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.151" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.151" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ec9a4120e..871cbd8f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,7 @@ rayon = "1.6.1" regex = "1.7.0" rust-ini = "0.18.0" semver = "1.0.16" -serde = { version = "1.0.151", features = ["derive"] } +serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.91" sha1 = "0.10.5" shadow-rs = { version = "0.19.0", default-features = false } From d4e664eda5ed06dfa91ae4e97d420afda38c30d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Dec 2022 03:59:10 +0000 Subject: [PATCH 3/7] build(deps): update rust crate toml_edit to 0.16.1 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 25afbbc08..94e6c32c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3104,9 +3104,9 @@ checksum = "808b51e57d0ef8f71115d8f3a01e7d3750d01c79cac4b3eda910f4389fdf92fd" [[package]] name = "toml_edit" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcd65b83c7473af53e3fd994eb2888dfddfeb28cac9a82825ec5803c233c882c" +checksum = "5c040d7eb2b695a2a39048f9d8e7ee865ef1c57cd9c44ba9b4a4d389095f7e6a" dependencies = [ "indexmap", "itertools", diff --git a/Cargo.toml b/Cargo.toml index 871cbd8f4..b72498df4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,7 @@ strsim = "0.10.0" systemstat = "=0.2.2" terminal_size = "0.2.3" toml = { version = "0.5.10", features = ["preserve_order"] } -toml_edit = "0.16.0" +toml_edit = "0.16.1" unicode-segmentation = "1.10.0" unicode-width = "0.1.10" urlencoding = "2.1.2" From 9093891acbe2c86b1615c37386dadbb0cc632199 Mon Sep 17 00:00:00 2001 From: Scott Palmer Date: Tue, 27 Dec 2022 07:40:56 -0500 Subject: [PATCH 4/7] fix(package): Improve regex for extracting gradle version from gradle.properties (#4760) fix: Improve regex for extracting gradle package version from gradle.properties (#4759) --- src/modules/package.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/modules/package.rs b/src/modules/package.rs index 7a2e88a97..d741ad1f1 100644 --- a/src/modules/package.rs +++ b/src/modules/package.rs @@ -105,7 +105,7 @@ fn get_gradle_version(context: &Context, config: &PackageConfig) -> Option.*)").unwrap(); + let re = Regex::new(r"(?m)^\s*version\s*=\s*(?P.*)").unwrap(); let caps = re.captures(&contents)?; format_version(&caps["version"], config.version_format) }).or_else(|| { @@ -979,6 +979,18 @@ java { expect_output(&project_dir, Some("v1.2.3"), None); project_dir.close() } + #[test] + fn test_extract_grade_version_from_properties_with_comment_and_whitespace() -> io::Result<()> { + let config_name = "gradle.properties"; + let config_content = " + # or use -Pversion=0.0.1 + version = 1.2.3 + "; + let project_dir = create_project_dir()?; + fill_config(&project_dir, config_name, Some(config_content))?; + expect_output(&project_dir, Some("v1.2.3"), None); + project_dir.close() + } #[test] fn test_extract_mix_version() -> io::Result<()> { From 19fdf9bba59f6ae5a756b81d221a9dc3185208f5 Mon Sep 17 00:00:00 2001 From: Julian Antonielli Date: Tue, 27 Dec 2022 13:59:40 +0000 Subject: [PATCH 5/7] feat(nix): support new `nix shell` command (#4724) * Support `nix shell` * Remove unnecessary `Debug` implementation * Add test to detect false positive * Improve detection of `/nix/store` in $PATH * Add docs about unknown state * Gate under `heuristic` flag * Regenerate config schema --- .github/config-schema.json | 12 +++- docs/config/README.md | 19 ++++--- src/configs/nix_shell.rs | 4 ++ src/modules/nix_shell.rs | 114 ++++++++++++++++++++++++++++++++++--- 4 files changed, 131 insertions(+), 18 deletions(-) mode change 100644 => 100755 src/configs/nix_shell.rs diff --git a/.github/config-schema.json b/.github/config-schema.json index e7fb0af12..72595f6b8 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -989,10 +989,12 @@ "default": { "disabled": false, "format": "via [$symbol$state( \\($name\\))]($style) ", + "heuristic": false, "impure_msg": "impure", "pure_msg": "pure", "style": "bold blue", - "symbol": "❄️ " + "symbol": "❄️ ", + "unknown_msg": "" }, "allOf": [ { @@ -4066,9 +4068,17 @@ "default": "pure", "type": "string" }, + "unknown_msg": { + "default": "", + "type": "string" + }, "disabled": { "default": false, "type": "boolean" + }, + "heuristic": { + "default": false, + "type": "boolean" } }, "additionalProperties": false diff --git a/docs/config/README.md b/docs/config/README.md index 904504b24..1a875bfaf 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -2718,14 +2718,16 @@ The module will be shown when inside a nix-shell environment. ### Options -| Option | Default | Description | -| ------------ | -------------------------------------------- | ----------------------------------------------------- | -| `format` | `'via [$symbol$state( \($name\))]($style) '` | The format for the module. | -| `symbol` | `'❄️ '` | A format string representing the symbol of nix-shell. | -| `style` | `'bold blue'` | The style for the module. | -| `impure_msg` | `'impure'` | A format string shown when the shell is impure. | -| `pure_msg` | `'pure'` | A format string shown when the shell is pure. | -| `disabled` | `false` | Disables the `nix_shell` module. | +| Option | Default | Description | +| ------------- | -------------------------------------------- | --------------------------------------------------------------------- | +| `format` | `'via [$symbol$state( \($name\))]($style) '` | The format for the module. | +| `symbol` | `'❄️ '` | A format string representing the symbol of nix-shell. | +| `style` | `'bold blue'` | The style for the module. | +| `impure_msg` | `'impure'` | A format string shown when the shell is impure. | +| `pure_msg` | `'pure'` | A format string shown when the shell is pure. | +| `unknown_msg` | `''` | A format string shown when it is unknown if the shell is pure/impure. | +| `disabled` | `false` | Disables the `nix_shell` module. | +| `heuristic` | `false` | Attempts to detect new `nix shell`-style shells with a heuristic. | ### Variables @@ -2747,6 +2749,7 @@ The module will be shown when inside a nix-shell environment. disabled = true impure_msg = '[impure shell](bold red)' pure_msg = '[pure shell](bold green)' +unknown_msg = '[unknown shell](bold yellow)' format = 'via [☃️ $state( \($name\))](bold blue) ' ``` diff --git a/src/configs/nix_shell.rs b/src/configs/nix_shell.rs old mode 100644 new mode 100755 index 0a676aeb7..be787a8fa --- a/src/configs/nix_shell.rs +++ b/src/configs/nix_shell.rs @@ -13,7 +13,9 @@ pub struct NixShellConfig<'a> { pub style: &'a str, pub impure_msg: &'a str, pub pure_msg: &'a str, + pub unknown_msg: &'a str, pub disabled: bool, + pub heuristic: bool, } /* The trailing double spaces in `symbol` are needed to work around issues with @@ -27,7 +29,9 @@ impl<'a> Default for NixShellConfig<'a> { style: "bold blue", impure_msg: "impure", pure_msg: "pure", + unknown_msg: "", disabled: false, + heuristic: false, } } } diff --git a/src/modules/nix_shell.rs b/src/modules/nix_shell.rs index c3cba422a..f89744018 100644 --- a/src/modules/nix_shell.rs +++ b/src/modules/nix_shell.rs @@ -3,32 +3,70 @@ use super::{Context, Module, ModuleConfig}; use crate::configs::nix_shell::NixShellConfig; use crate::formatter::StringFormatter; +enum NixShellType { + Pure, + Impure, + /// We're in a Nix shell, but we don't know which type. + /// This can only happen in a `nix shell` shell (not a `nix-shell` one). + Unknown, +} + +impl NixShellType { + fn detect_shell_type(use_heuristic: bool, context: &Context) -> Option { + use NixShellType::*; + + let shell_type = context.get_env("IN_NIX_SHELL"); + match shell_type.as_deref() { + Some("pure") => return Some(Pure), + Some("impure") => return Some(Impure), + _ => {} + }; + + if use_heuristic { + Self::in_new_nix_shell(context).map(|_| Unknown) + } else { + None + } + } + + // Hack to detect if we're in a `nix shell` (in constrast to a `nix-shell`). + // A better way to do this will be enabled by https://github.com/NixOS/nix/issues/6677. + fn in_new_nix_shell(context: &Context) -> Option<()> { + let path = context.get_env("PATH")?; + + std::env::split_paths(&path) + .any(|path| path.starts_with("/nix/store")) + .then_some(()) + } +} + /// Creates a module showing if inside a nix-shell /// /// The module will use the `$IN_NIX_SHELL` and `$name` environment variable to /// determine if it's inside a nix-shell and the name of it. /// /// The following options are availables: -/// - `impure_msg` (string) // change the impure msg -/// - `pure_msg` (string) // change the pure msg +/// - `impure_msg` (string) // change the impure msg +/// - `pure_msg` (string) // change the pure msg +/// - `unknown_msg` (string) // change the unknown message /// /// Will display the following: /// - pure (name) // $name == "name" in a pure nix-shell /// - impure (name) // $name == "name" in an impure nix-shell /// - pure // $name == "" in a pure nix-shell /// - impure // $name == "" in an impure nix-shell +/// - unknown (name) // $name == "name" in an unknown nix-shell +/// - unknown // $name == "" in an unknown nix-shell pub fn module<'a>(context: &'a Context) -> Option> { let mut module = context.new_module("nix_shell"); let config: NixShellConfig = NixShellConfig::try_load(module.config); let shell_name = context.get_env("name"); - let shell_type = context.get_env("IN_NIX_SHELL")?; - let shell_type_format = match shell_type.as_ref() { - "impure" => config.impure_msg, - "pure" => config.pure_msg, - _ => { - return None; - } + let shell_type = NixShellType::detect_shell_type(config.heuristic, context)?; + let shell_type_format = match shell_type { + NixShellType::Pure => config.pure_msg, + NixShellType::Impure => config.impure_msg, + NixShellType::Unknown => config.unknown_msg, }; let parsed = StringFormatter::new(config.format).and_then(|formatter| { @@ -130,4 +168,62 @@ mod tests { assert_eq!(expected, actual); } + + #[test] + fn new_nix_shell() { + let actual = ModuleRenderer::new("nix_shell") + .env( + "PATH", + "/nix/store/v7qvqv81jp0cajvrxr9x072jgqc01yhi-nix-info/bin:/Users/user/.cargo/bin", + ) + .config(toml::toml! { + [nix_shell] + heuristic = true + }) + .collect(); + let expected = Some(format!("via {} ", Color::Blue.bold().paint("❄️ "))); + + assert_eq!(expected, actual); + } + + #[test] + fn no_new_nix_shell() { + let actual = ModuleRenderer::new("nix_shell") + .env("PATH", "/Users/user/.cargo/bin") + .config(toml::toml! { + [nix_shell] + heuristic = true + }) + .collect(); + let expected = None; + + assert_eq!(expected, actual); + } + + #[test] + fn no_new_nix_shell_with_nix_store_subdirectory() { + let actual = ModuleRenderer::new("nix_shell") + .env("PATH", "/Users/user/some/nix/store/subdirectory") + .config(toml::toml! { + [nix_shell] + heuristic = true + }) + .collect(); + let expected = None; + + assert_eq!(expected, actual); + } + + #[test] + fn no_new_nix_shell_when_heuristic_is_disabled() { + let actual = ModuleRenderer::new("nix_shell") + .env( + "PATH", + "/nix/store/v7qvqv81jp0cajvrxr9x072jgqc01yhi-nix-info/bin:/Users/user/.cargo/bin", + ) + .collect(); + let expected = None; + + assert_eq!(expected, actual); + } } From f183a4e4c294f6bf7c4f5a604816682cf8864555 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Dec 2022 02:30:43 +0000 Subject: [PATCH 6/7] build(deps): update rust crate toml_edit to 0.16.2 --- Cargo.lock | 9 ++++----- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94e6c32c7..e16b56188 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1839,12 +1839,11 @@ dependencies = [ [[package]] name = "nom8" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75d908f0297c3526d34e478d438b07eefe3d7b0416494d7ffccb17f1c7f7262c" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" dependencies = [ "memchr", - "minimal-lexical", ] [[package]] @@ -3104,9 +3103,9 @@ checksum = "808b51e57d0ef8f71115d8f3a01e7d3750d01c79cac4b3eda910f4389fdf92fd" [[package]] name = "toml_edit" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c040d7eb2b695a2a39048f9d8e7ee865ef1c57cd9c44ba9b4a4d389095f7e6a" +checksum = "dd30deba9a1cd7153c22aecf93e86df639e7b81c622b0af8d9255e989991a7b7" dependencies = [ "indexmap", "itertools", diff --git a/Cargo.toml b/Cargo.toml index b72498df4..72ff899d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,7 @@ strsim = "0.10.0" systemstat = "=0.2.2" terminal_size = "0.2.3" toml = { version = "0.5.10", features = ["preserve_order"] } -toml_edit = "0.16.1" +toml_edit = "0.16.2" unicode-segmentation = "1.10.0" unicode-width = "0.1.10" urlencoding = "2.1.2" From 5d4cb6ff8f6bd1915aa2c16162950b270f1759b1 Mon Sep 17 00:00:00 2001 From: David Knaack Date: Wed, 28 Dec 2022 21:00:49 +0100 Subject: [PATCH 7/7] feat(env_var): Add support for env_var.VAR in format (#4497) Co-Authored-By: Segev Finer <24731903+segevfiner@users.noreply.github.com> Co-authored-by: Segev Finer <24731903+segevfiner@users.noreply.github.com> --- .github/config-schema.json | 4 + docs/config/README.md | 23 ++-- src/configs/env_var.rs | 2 + src/context.rs | 9 -- src/module.rs | 1 - src/modules/custom.rs | 121 ++++++++++++++------- src/modules/env_var.rs | 217 +++++++++++++++++++++++++------------ src/modules/mod.rs | 8 +- src/print.rs | 215 +++++++++++++++++++++++++++++------- 9 files changed, 430 insertions(+), 170 deletions(-) diff --git a/.github/config-schema.json b/.github/config-schema.json index 72595f6b8..7924c0cea 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -2834,6 +2834,10 @@ "disabled": { "default": false, "type": "boolean" + }, + "description": { + "default": "", + "type": "string" } }, "additionalProperties": false diff --git a/docs/config/README.md b/docs/config/README.md index 1a875bfaf..f8b27bfd1 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -1380,6 +1380,14 @@ The module will be shown only if any of the following conditions are met: ::: tip +The order in which env_var modules are shown can be individually set by including +`${env_var.foo}` in the top level `format` (as it includes a dot, you need to use `${...}`). +By default, the `env_var` module will simply show all env_var modules in the order they were defined. + +::: + +::: tip + Multiple environmental variables can be displayed by using a `.`. (see example) If the `variable` configuration option is not set, the module will display value of variable under the name of text after the `.` character. @@ -1396,13 +1404,14 @@ default = 'unknown user' ### Options -| Option | Default | Description | -| ---------- | ------------------------------ | ---------------------------------------------------------------------------- | -| `symbol` | `''` | The symbol used before displaying the variable value. | -| `variable` | | The environment variable to be displayed. | -| `default` | | The default value to be displayed when the selected variable is not defined. | -| `format` | `'with [$env_value]($style) '` | The format for the module. | -| `disabled` | `false` | Disables the `env_var` module. | +| Option | Default | Description | +| ------------- | ------------------------------ | ---------------------------------------------------------------------------- | +| `symbol` | `""` | The symbol used before displaying the variable value. | +| `variable` | | The environment variable to be displayed. | +| `default` | | The default value to be displayed when the selected variable is not defined. | +| `format` | `"with [$env_value]($style) "` | The format for the module. | +| `description` | `""` | The description of the module that is shown when running `starship explain`. | +| `disabled` | `false` | Disables the `env_var` module. | ### Variables diff --git a/src/configs/env_var.rs b/src/configs/env_var.rs index d9a8db9a3..0ef6bae59 100644 --- a/src/configs/env_var.rs +++ b/src/configs/env_var.rs @@ -16,6 +16,7 @@ pub struct EnvVarConfig<'a> { pub default: Option<&'a str>, pub format: &'a str, pub disabled: bool, + pub description: &'a str, } impl<'a> Default for EnvVarConfig<'a> { @@ -27,6 +28,7 @@ impl<'a> Default for EnvVarConfig<'a> { default: None, format: "with [$env_value]($style) ", disabled: false, + description: "", } } } diff --git a/src/context.rs b/src/context.rs index 91341238c..702837647 100644 --- a/src/context.rs +++ b/src/context.rs @@ -238,15 +238,6 @@ impl<'a> Context<'a> { disabled == Some(true) } - /// Return whether the specified custom module has a `disabled` option set to true. - /// If it doesn't exist, `None` is returned. - pub fn is_custom_module_disabled_in_config(&self, name: &str) -> Option { - let config = self.config.get_custom_module_config(name)?; - let disabled = Some(config).and_then(|table| table.as_table()?.get("disabled")?.as_bool()); - - Some(disabled == Some(true)) - } - // returns a new ScanDir struct with reference to current dir_files of context // see ScanDir for methods pub fn try_begin_scan(&'a self) -> Option> { diff --git a/src/module.rs b/src/module.rs index 77508f147..17a345b6b 100644 --- a/src/module.rs +++ b/src/module.rs @@ -31,7 +31,6 @@ pub const ALL_MODULES: &[&str] = &[ "dotnet", "elixir", "elm", - "env_var", "erlang", "fennel", "fill", diff --git a/src/modules/custom.rs b/src/modules/custom.rs index d963e69ed..e224958a4 100644 --- a/src/modules/custom.rs +++ b/src/modules/custom.rs @@ -1,9 +1,9 @@ use std::env; +use std::fmt::{self, Debug}; use std::io::Write; use std::path::Path; use std::process::{Command, Stdio}; use std::time::Duration; -use std::time::Instant; use process_control::{ChildExt, Control, Output}; @@ -22,11 +22,11 @@ use crate::{ /// /// Finally, the content of the module itself is also set by a command. pub fn module<'a>(name: &str, context: &'a Context) -> Option> { - let start: Instant = Instant::now(); - let toml_config = context.config.get_custom_module_config(name).expect( - "modules::custom::module should only be called after ensuring that the module exists", - ); + let toml_config = get_config(name, context)?; let config = CustomConfig::load(toml_config); + if config.disabled { + return None; + } if let Some(os) = config.os { if os != env::consts::OS && !(os == "unix" && cfg!(unix)) { @@ -34,7 +34,8 @@ pub fn module<'a>(name: &str, context: &'a Context) -> Option> { } } - let mut module = Module::new(name, config.description, Some(toml_config)); + // Note: Forward config if `Module` ends up needing `config` + let mut module = Module::new(&format!("custom.{name}"), config.description, None); let mut is_match = context .try_begin_scan()? @@ -48,48 +49,74 @@ pub fn module<'a>(name: &str, context: &'a Context) -> Option> { Either::First(b) => b, Either::Second(s) => exec_when(s, &config, context), }; + + if !is_match { + return None; + } } - if is_match { - let parsed = StringFormatter::new(config.format).and_then(|formatter| { - formatter - .map_meta(|var, _| match var { - "symbol" => Some(config.symbol), - _ => None, - }) - .map_style(|variable| match variable { - "style" => Some(Ok(config.style)), - _ => None, - }) - .map_no_escaping(|variable| match variable { - "output" => { - let output = exec_command(config.command, context, &config)?; - let trimmed = output.trim(); + let parsed = StringFormatter::new(config.format).and_then(|formatter| { + formatter + .map_meta(|var, _| match var { + "symbol" => Some(config.symbol), + _ => None, + }) + .map_style(|variable| match variable { + "style" => Some(Ok(config.style)), + _ => None, + }) + .map_no_escaping(|variable| match variable { + "output" => { + let output = exec_command(config.command, context, &config)?; + let trimmed = output.trim(); - if trimmed.is_empty() { - None - } else { - Some(Ok(trimmed.to_string())) - } + if trimmed.is_empty() { + None + } else { + Some(Ok(trimmed.to_string())) } - _ => None, - }) - .parse(None, Some(context)) - }); + } + _ => None, + }) + .parse(None, Some(context)) + }); - match parsed { - Ok(segments) => module.set_segments(segments), - Err(error) => { - log::warn!("Error in module `custom.{}`:\n{}", name, error); - } - }; - } - let elapsed = start.elapsed(); - log::trace!("Took {:?} to compute custom module {:?}", elapsed, name); - module.duration = elapsed; + match parsed { + Ok(segments) => module.set_segments(segments), + Err(error) => { + log::warn!("Error in module `custom.{}`:\n{}", name, error); + } + }; Some(module) } +/// Gets the TOML config for the custom module, handling the case where the module is not defined +fn get_config<'a>(module_name: &str, context: &'a Context<'a>) -> Option<&'a toml::Value> { + struct DebugCustomModules<'tmp>(&'tmp toml::value::Table); + + impl Debug for DebugCustomModules<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_list().entries(self.0.keys()).finish() + } + } + + let config = context.config.get_custom_module_config(module_name); + + if config.is_some() { + return config; + } else if let Some(modules) = context.config.get_custom_modules() { + log::debug!( + "top level format contains custom module {module_name:?}, but no configuration was provided. Configuration for the following modules were provided: {:?}", + DebugCustomModules(modules), + ); + } else { + log::debug!( + "top level format contains custom module {module_name:?}, but no configuration was provided.", + ); + }; + None +} + /// Return the invoking shell, using `shell` and fallbacking in order to `STARSHIP_SHELL` and "sh"/"cmd" fn get_shell<'a, 'b>( shell_args: &'b [&'a str], @@ -680,4 +707,18 @@ mod tests { dir.close() } + + #[test] + fn disabled() { + let actual = ModuleRenderer::new("custom.test") + .config(toml::toml! { + [custom.test] + disabled = true + when = true + format = "test" + }) + .collect(); + let expected = None; + assert_eq!(expected, actual); + } } diff --git a/src/modules/env_var.rs b/src/modules/env_var.rs index 1dbcca134..2ba09f30a 100644 --- a/src/modules/env_var.rs +++ b/src/modules/env_var.rs @@ -1,38 +1,9 @@ use super::{Context, Module}; +use std::borrow::Cow; use crate::config::ModuleConfig; use crate::configs::env_var::EnvVarConfig; use crate::formatter::StringFormatter; -use crate::segment::Segment; - -/// Creates `env_var_module` displayer which displays all configured environmental variables -pub fn module<'a>(context: &'a Context) -> Option> { - let config_table = context.config.get_env_var_modules()?; - let mut env_modules = config_table - .iter() - .filter(|(_, config)| config.is_table()) - .filter_map(|(variable, _)| env_var_module(vec!["env_var", variable], context)) - .collect::>(); - // Old configuration is present in starship configuration - if config_table.iter().any(|(_, config)| !config.is_table()) { - if let Some(fallback_env_var_module) = env_var_module(vec!["env_var"], context) { - env_modules.push(fallback_env_var_module); - } - } - Some(env_var_displayer(env_modules, context)) -} - -/// A utility module to display multiple `env_variable` modules -fn env_var_displayer<'a>(modules: Vec, context: &'a Context) -> Module<'a> { - let mut module = context.new_module("env_var_displayer"); - - let module_segments = modules - .into_iter() - .flat_map(|module| module.segments) - .collect::>(); - module.set_segments(module_segments); - module -} /// Creates a module with the value of the chosen environment variable /// @@ -40,20 +11,36 @@ fn env_var_displayer<'a>(modules: Vec, context: &'a Context) -> Module<' /// - `env_var.disabled` is absent or false /// - `env_var.variable` is defined /// - a variable named as the value of `env_var.variable` is defined -fn env_var_module<'a>(module_config_path: Vec<&str>, context: &'a Context) -> Option> { - let mut module = context.new_module(&module_config_path.join(".")); - let config_value = context.config.get_config(&module_config_path); - let config = EnvVarConfig::load(config_value.expect( - "modules::env_var::module should only be called after ensuring that the module exists", - )); +pub fn module<'a>(name: Option<&str>, context: &'a Context) -> Option> { + let toml_config = match name { + Some(name) => context + .config + .get_config(&["env_var", name]) + .map(Cow::Borrowed), + None => context + .config + .get_module_config("env_var") + .and_then(filter_config) + .map(Cow::Owned) + .map(Some)?, + }; + let mod_name = match name { + Some(name) => format!("env_var.{}", name), + None => "env_var".to_owned(), + }; + + let config = EnvVarConfig::try_load(toml_config.as_deref()); + // Note: Forward config if `Module` ends up needing `config` + let mut module = Module::new(&mod_name, config.description, None); if config.disabled { return None; }; - let variable_name = get_variable_name(module_config_path, &config); + let variable_name = config.variable.or(name)?; - let env_value = get_env_value(context, variable_name?, config.default)?; + let env_value = context.get_env(variable_name); + let env_value = env_value.as_deref().or(config.default)?; let parsed = StringFormatter::new(config.format).and_then(|formatter| { formatter .map_meta(|var, _| match var { @@ -65,7 +52,7 @@ fn env_var_module<'a>(module_config_path: Vec<&str>, context: &'a Context) -> Op _ => None, }) .map(|variable| match variable { - "env_value" => Some(Ok(&env_value)), + "env_value" => Some(Ok(env_value)), _ => None, }) .parse(None, Some(context)) @@ -82,24 +69,22 @@ fn env_var_module<'a>(module_config_path: Vec<&str>, context: &'a Context) -> Op Some(module) } -fn get_variable_name<'a>( - module_config_path: Vec<&'a str>, - config: &'a EnvVarConfig, -) -> Option<&'a str> { - match config.variable { - Some(v) => Some(v), - None => { - let last_element = module_config_path.last()?; - Some(*last_element) - } - } -} - -fn get_env_value(context: &Context, name: &str, default: Option<&str>) -> Option { - match context.get_env(name) { - Some(value) => Some(value), - None => default.map(std::borrow::ToOwned::to_owned), - } +/// Filter `config` to only includes non-table values +/// This filters the top-level table to only include its specific configuation +fn filter_config(config: &toml::Value) -> Option { + let o = config + .as_table() + .map(|table| { + table + .iter() + .filter(|(_key, val)| !val.is_table()) + .map(|(key, val)| (key.to_owned(), val.to_owned())) + .collect::() + }) + .filter(|table| !table.is_empty()) + .map(toml::Value::Table); + log::trace!("Filtered top-level env_var config: {o:?}"); + o } #[cfg(test)] @@ -133,7 +118,7 @@ mod test { #[test] fn defined_variable() { - let actual = ModuleRenderer::new("env_var") + let actual = ModuleRenderer::new("env_var.TEST_VAR") .config(toml::toml! { [env_var.TEST_VAR] }) @@ -146,7 +131,7 @@ mod test { #[test] fn undefined_variable() { - let actual = ModuleRenderer::new("env_var") + let actual = ModuleRenderer::new("env_var.TEST_VAR") .config(toml::toml! { [env_var.TEST_VAR] }) @@ -158,7 +143,7 @@ mod test { #[test] fn default_has_no_effect() { - let actual = ModuleRenderer::new("env_var") + let actual = ModuleRenderer::new("env_var.TEST_VAR") .config(toml::toml! { [env_var.TEST_VAR] default = "N/A" @@ -172,7 +157,7 @@ mod test { #[test] fn default_takes_effect() { - let actual = ModuleRenderer::new("env_var") + let actual = ModuleRenderer::new("env_var.UNDEFINED_TEST_VAR") .config(toml::toml! { [env_var.UNDEFINED_TEST_VAR] default = "N/A" @@ -185,7 +170,7 @@ mod test { #[test] fn symbol() { - let actual = ModuleRenderer::new("env_var") + let actual = ModuleRenderer::new("env_var.TEST_VAR") .config(toml::toml! { [env_var.TEST_VAR] format = "with [■ $env_value](black bold dimmed) " @@ -202,7 +187,7 @@ mod test { #[test] fn prefix() { - let actual = ModuleRenderer::new("env_var") + let actual = ModuleRenderer::new("env_var.TEST_VAR") .config(toml::toml! { [env_var.TEST_VAR] format = "with [_$env_value](black bold dimmed) " @@ -219,7 +204,7 @@ mod test { #[test] fn suffix() { - let actual = ModuleRenderer::new("env_var") + let actual = ModuleRenderer::new("env_var.TEST_VAR") .config(toml::toml! { [env_var.TEST_VAR] format = "with [${env_value}_](black bold dimmed) " @@ -236,7 +221,7 @@ mod test { #[test] fn display_few() { - let actual = ModuleRenderer::new("env_var") + let actual1 = ModuleRenderer::new("env_var.TEST_VAR") .config(toml::toml! { [env_var.TEST_VAR] [env_var.TEST_VAR2] @@ -244,11 +229,103 @@ mod test { .env("TEST_VAR", TEST_VAR_VALUE) .env("TEST_VAR2", TEST_VAR_VALUE) .collect(); - let expected = Some(format!( - "with {} with {} ", - style().paint(TEST_VAR_VALUE), - style().paint(TEST_VAR_VALUE) - )); + let actual2 = ModuleRenderer::new("env_var.TEST_VAR2") + .config(toml::toml! { + [env_var.TEST_VAR] + [env_var.TEST_VAR2] + }) + .env("TEST_VAR", TEST_VAR_VALUE) + .env("TEST_VAR2", TEST_VAR_VALUE) + .collect(); + let expected = Some(format!("with {} ", style().paint(TEST_VAR_VALUE))); + + assert_eq!(expected, actual1); + assert_eq!(expected, actual2); + } + + #[test] + fn mixed() { + let cfg = toml::toml! { + [env_var] + variable = "TEST_VAR_OUTER" + format = "$env_value" + [env_var.TEST_VAR_INNER] + format = "$env_value" + }; + let actual_inner = ModuleRenderer::new("env_var.TEST_VAR_INNER") + .config(cfg.clone()) + .env("TEST_VAR_OUTER", "outer") + .env("TEST_VAR_INNER", "inner") + .collect(); + + assert_eq!( + actual_inner.as_deref(), + Some("inner"), + "inner module should be rendered" + ); + + let actual_outer = ModuleRenderer::new("env_var") + .config(cfg) + .env("TEST_VAR_OUTER", "outer") + .env("TEST_VAR_INNER", "inner") + .collect(); + + assert_eq!( + actual_outer.as_deref(), + Some("outer"), + "outer module should be rendered" + ); + } + + #[test] + fn no_config() { + let actual = ModuleRenderer::new("env_var.TEST_VAR") + .env("TEST_VAR", TEST_VAR_VALUE) + .collect(); + let expected = Some(format!("with {} ", style().paint(TEST_VAR_VALUE))); + + assert_eq!(expected, actual); + } + + #[test] + fn disabled_child() { + let actual = ModuleRenderer::new("env_var.TEST_VAR") + .config(toml::toml! { + [env_var.TEST_VAR] + disabled = true + }) + .env("TEST_VAR", TEST_VAR_VALUE) + .collect(); + let expected = None; + + assert_eq!(expected, actual); + } + + #[test] + fn disabled_root() { + let actual = ModuleRenderer::new("env_var") + .config(toml::toml! { + [env_var] + disabled = true + }) + .env("TEST_VAR", TEST_VAR_VALUE) + .collect(); + let expected = None; + + assert_eq!(expected, actual); + } + + #[test] + fn variable_override() { + let actual = ModuleRenderer::new("env_var.TEST_VAR") + .config(toml::toml! { + [env_var.TEST_VAR] + variable = "TEST_VAR2" + }) + .env("TEST_VAR", "implicit name") + .env("TEST_VAR2", "explicit name") + .collect(); + let expected = Some(format!("with {} ", style().paint("explicit name"))); assert_eq!(expected, actual); } diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 8c8c5b6bb..64442937f 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -122,8 +122,8 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "elixir" => elixir::module(context), "elm" => elm::module(context), "erlang" => erlang::module(context), + "env_var" => env_var::module(None, context), "fennel" => fennel::module(context), - "env_var" => env_var::module(context), "fill" => fill::module(context), "gcloud" => gcloud::module(context), "git_branch" => git_branch::module(context), @@ -183,8 +183,9 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "vagrant" => vagrant::module(context), "vcsh" => vcsh::module(context), "zig" => zig::module(context), - // Added for tests, avoid potential side effects in production code. - #[cfg(test)] + env if env.starts_with("env_var.") => { + env_var::module(env.strip_prefix("env_var."), context) + } custom if custom.starts_with("custom.") => { // SAFETY: We just checked that the module starts with "custom." custom::module(custom.strip_prefix("custom.").unwrap(), context) @@ -233,7 +234,6 @@ pub fn description(module: &str) -> &'static str { "dotnet" => "The relevant version of the .NET Core SDK for the current directory", "elixir" => "The currently installed versions of Elixir and OTP", "elm" => "The currently installed version of Elm", - "env_var" => "Displays the current value of a selected environment variable", "erlang" => "Current OTP version", "fennel" => "The currently installed version of Fennel", "fill" => "Fills the remaining space on the line with a pad string", diff --git a/src/print.rs b/src/print.rs index 6a1bdc73b..5702cf4e0 100644 --- a/src/print.rs +++ b/src/print.rs @@ -2,7 +2,7 @@ use clap::{builder::PossibleValue, ValueEnum}; use nu_ansi_term::AnsiStrings; use rayon::prelude::*; use std::collections::BTreeSet; -use std::fmt::{self, Debug, Write as FmtWrite}; +use std::fmt::{Debug, Write as FmtWrite}; use std::io::{self, Write}; use std::time::Duration; use terminal_size::terminal_size; @@ -313,14 +313,6 @@ fn handle_module<'a>( context: &'a Context, module_list: &BTreeSet, ) -> Vec> { - struct DebugCustomModules<'tmp>(&'tmp toml::value::Table); - - impl Debug for DebugCustomModules<'_> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_list().entries(self.0.keys()).finish() - } - } - let mut modules: Vec = Vec::new(); if ALL_MODULES.contains(&module) { @@ -328,34 +320,29 @@ fn handle_module<'a>( if !context.is_module_disabled_in_config(module) { modules.extend(modules::handle(module, context)); } - } else if module == "custom" { - // Write out all custom modules, except for those that are explicitly set - if let Some(custom_modules) = context.config.get_custom_modules() { - let custom_modules = custom_modules.iter().filter_map(|(custom_module, config)| { - if should_add_implicit_custom_module(custom_module, config, module_list) { - modules::custom::module(custom_module, context) - } else { - None - } - }); - modules.extend(custom_modules); + } else if module.starts_with("custom.") || module.starts_with("env_var.") { + // custom. and env_var. are special cases and handle disabled modules themselves + modules.extend(modules::handle(module, context)); + } else if matches!(module, "custom" | "env_var") { + // env var is a spacial case and may contain a top-level module definition + if module == "env_var" { + modules.extend(modules::handle(module, context)); } - } else if let Some(module) = module.strip_prefix("custom.") { - // Write out a custom module if it isn't disabled (and it exists...) - match context.is_custom_module_disabled_in_config(module) { - Some(true) => (), // Module is disabled, we don't add it to the prompt - Some(false) => modules.extend(modules::custom::module(module, context)), - None => match context.config.get_custom_modules() { - Some(modules) => log::debug!( - "top level format contains custom module \"{}\", but no configuration was provided. Configuration for the following modules were provided: {:?}", - module, - DebugCustomModules(modules), - ), - None => log::debug!( - "top level format contains custom module \"{}\", but no configuration was provided.", - module, - ), - }, + + // Write out all custom modules, except for those that are explicitly set + for (child, config) in context + .config + .get_config(&[module]) + .and_then(|config| config.as_table().map(|t| t.iter())) + .into_iter() + .flatten() + { + // Some env var keys may be part of a top-level module definition + if module == "env_var" && !config.is_table() { + continue; + } else if should_add_implicit_module(module, child, config, module_list) { + modules.extend(modules::handle(&format!("{module}.{child}"), context)); + } } } else { log::debug!( @@ -368,12 +355,13 @@ fn handle_module<'a>( modules } -fn should_add_implicit_custom_module( - custom_module: &str, +fn should_add_implicit_module( + parent_module: &str, + child_module: &str, config: &toml::Value, module_list: &BTreeSet, ) -> bool { - let explicit_module_name = format!("custom.{custom_module}"); + let explicit_module_name = format!("{parent_module}.{child_module}"); let is_explicitly_specified = module_list.contains(&explicit_module_name); if is_explicitly_specified { @@ -539,4 +527,153 @@ mod test { fn print_schema_does_not_panic() { print_schema(); } + + #[test] + fn custom_expands() -> std::io::Result<()> { + let dir = tempfile::tempdir()?; + let mut context = default_context(); + context.current_dir = dir.path().to_path_buf(); + context.config = StarshipConfig { + config: Some(toml::toml! { + format="$custom" + [custom.a] + when=true + format="a" + [custom.b] + when=true + format="b" + }), + }; + context.root_config.format = "$custom".to_string(); + + let expected = String::from("\nab"); + let actual = get_prompt(context); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn env_expands() { + let mut context = default_context(); + context.config = StarshipConfig { + config: Some(toml::toml! { + format="$env_var" + [env_var] + format="$env_value" + variable = "a" + [env_var.b] + format="$env_value" + [env_var.c] + format="$env_value" + }), + }; + context.root_config.format = "$env_var".to_string(); + context.env.insert("a", "a".to_string()); + context.env.insert("b", "b".to_string()); + context.env.insert("c", "c".to_string()); + + let expected = String::from("\nabc"); + let actual = get_prompt(context); + assert_eq!(expected, actual); + } + + #[test] + fn custom_mixed() -> std::io::Result<()> { + let dir = tempfile::tempdir()?; + let mut context = default_context(); + context.current_dir = dir.path().to_path_buf(); + context.config = StarshipConfig { + config: Some(toml::toml! { + format="${custom.c}$custom${custom.b}" + [custom.a] + when=true + format="a" + [custom.b] + when=true + format="b" + [custom.c] + when=true + format="c" + }), + }; + context.root_config.format = "${custom.c}$custom${custom.b}".to_string(); + + let expected = String::from("\ncab"); + let actual = get_prompt(context); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn env_mixed() { + let mut context = default_context(); + context.config = StarshipConfig { + config: Some(toml::toml! { + format="${env_var.c}$env_var${env_var.b}" + [env_var] + format="$env_value" + variable = "d" + [env_var.a] + format="$env_value" + [env_var.b] + format="$env_value" + [env_var.c] + format="$env_value" + }), + }; + context.root_config.format = "${env_var.c}$env_var${env_var.b}".to_string(); + context.env.insert("a", "a".to_string()); + context.env.insert("b", "b".to_string()); + context.env.insert("c", "c".to_string()); + context.env.insert("d", "d".to_string()); + + let expected = String::from("\ncdab"); + let actual = get_prompt(context); + assert_eq!(expected, actual); + } + + #[test] + fn custom_subset() -> std::io::Result<()> { + let dir = tempfile::tempdir()?; + let mut context = default_context(); + context.current_dir = dir.path().to_path_buf(); + context.config = StarshipConfig { + config: Some(toml::toml! { + format="${custom.b}" + [custom.a] + when=true + format="a" + [custom.b] + when=true + format="b" + }), + }; + context.root_config.format = "${custom.b}".to_string(); + + let expected = String::from("\nb"); + let actual = get_prompt(context); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn custom_missing() -> std::io::Result<()> { + let dir = tempfile::tempdir()?; + let mut context = default_context(); + context.current_dir = dir.path().to_path_buf(); + context.config = StarshipConfig { + config: Some(toml::toml! { + format="${custom.b}" + [custom.a] + when=true + format="a" + }), + }; + context.root_config.format = "${custom.b}".to_string(); + + let expected = String::from("\n"); + let actual = get_prompt(context); + assert_eq!(expected, actual); + dir.close() + } }