From 355800f8147b1755a5289dc679e2147abd662daf Mon Sep 17 00:00:00 2001 From: Jamie Date: Tue, 11 Oct 2022 09:02:46 -0700 Subject: [PATCH] feat(module): Add a meson devenv indicator (#4389) * feat(module): Add a meson devenv indicator Adds a Meson Developer Environment indicator, if the MESON_DEVENV variable is set. Inside a `meson devenv`, the prompt will include the current Meson project name This also contains a new Truncate utility function, which may be adapted for other modules in the future * docs: Add Meson to presets --- .github/config-schema.json | 47 ++++++++ .../presets/toml/bracketed-segments.toml | 3 + .../presets/toml/nerd-font-symbols.toml | 3 + .../presets/toml/no-runtime-versions.toml | 3 + .../presets/toml/plain-text-symbols.toml | 3 + docs/config/README.md | 40 +++++++ src/configs/meson.rs | 30 +++++ src/configs/mod.rs | 3 + src/configs/starship_root.rs | 1 + src/module.rs | 1 + src/modules/meson.rs | 98 ++++++++++++++++ src/modules/mod.rs | 5 + src/modules/utils/mod.rs | 2 + src/modules/utils/truncate.rs | 107 ++++++++++++++++++ 14 files changed, 346 insertions(+) create mode 100644 src/configs/meson.rs create mode 100644 src/modules/meson.rs create mode 100644 src/modules/utils/truncate.rs diff --git a/.github/config-schema.json b/.github/config-schema.json index f92051a57..f1d97cfb7 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -864,6 +864,21 @@ } ] }, + "meson": { + "default": { + "disabled": false, + "format": "via [$symbol$project]($style) ", + "style": "blue bold", + "symbol": "⬢ ", + "truncation_length": 4294967295, + "truncation_symbol": "…" + }, + "allOf": [ + { + "$ref": "#/definitions/MesonConfig" + } + ] + }, "nim": { "default": { "detect_extensions": [ @@ -3600,6 +3615,38 @@ }, "additionalProperties": false }, + "MesonConfig": { + "type": "object", + "properties": { + "truncation_length": { + "default": 4294967295, + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "truncation_symbol": { + "default": "…", + "type": "string" + }, + "format": { + "default": "via [$symbol$project]($style) ", + "type": "string" + }, + "symbol": { + "default": "⬢ ", + "type": "string" + }, + "style": { + "default": "blue bold", + "type": "string" + }, + "disabled": { + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, "NimConfig": { "type": "object", "properties": { diff --git a/docs/.vuepress/public/presets/toml/bracketed-segments.toml b/docs/.vuepress/public/presets/toml/bracketed-segments.toml index 76a7f382e..51ea664a9 100644 --- a/docs/.vuepress/public/presets/toml/bracketed-segments.toml +++ b/docs/.vuepress/public/presets/toml/bracketed-segments.toml @@ -85,6 +85,9 @@ format = '\[[$symbol($version)]($style)\]' [memory_usage] format = '\[$symbol[$ram( | $swap)]($style)\]' +[meson] +format = '\[[$symbol$project]($style)\]' + [nim] format = '\[[$symbol($version)]($style)\]' diff --git a/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml b/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml index 1c62e0a14..b31685dc0 100644 --- a/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml +++ b/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml @@ -49,6 +49,9 @@ symbol = " " [memory_usage] symbol = " " +[meson] +symbol = "喝 " + [nim] symbol = " " diff --git a/docs/.vuepress/public/presets/toml/no-runtime-versions.toml b/docs/.vuepress/public/presets/toml/no-runtime-versions.toml index 156c4e762..0cea589c8 100644 --- a/docs/.vuepress/public/presets/toml/no-runtime-versions.toml +++ b/docs/.vuepress/public/presets/toml/no-runtime-versions.toml @@ -49,6 +49,9 @@ format = 'via [$symbol]($style)' [lua] format = 'via [$symbol]($style)' +[meson] +format = 'via [$symbol]($style)' + [nim] format = 'via [$symbol]($style)' diff --git a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml index 05b23720e..d152423ca 100644 --- a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml +++ b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml @@ -85,6 +85,9 @@ symbol = "nodejs " [memory_usage] symbol = "memory " +[meson] +symbol = "meson " + [nim] symbol = "nim " diff --git a/docs/config/README.md b/docs/config/README.md index 2f58ee355..36a08d38a 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -278,6 +278,7 @@ $zig\ $buf\ $nix_shell\ $conda\ +$meson\ $spack\ $memory_usage\ $aws\ @@ -2384,6 +2385,45 @@ symbol = " " style = "bold dimmed green" ``` +## Meson + +The `meson` module shows the current Meson developer environment status. + +By default the Meson project name is displayed, if `$MESON_DEVENV` is set. + +### Options + +| Option | Default | Description | +| ------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------- | +| `truncation_length` | `2^32 - 1` | Truncates a project name to `N` graphemes. | +| `truncation_symbol` | `"…"` | The symbol used to indicate a project name was truncated. You can use `""` for no symbol. | +| `format` | `"via [$symbol$project]($style) "` | The format for the module. | +| `symbol` | `"⬢ "` | The symbol used before displaying the project name. | +| `style` | `"blue bold"` | The style for the module. | +| `disabled` | `false` | Disables the `meson` module. | + +### Variables + +| Variable | Example | Description | +| -------- | ---------- | ------------------------------------ | +| project | `starship` | The current Meson project name | +| symbol | `🐏` | Mirrors the value of option `symbol` | +| style\* | | Mirrors the value of option `style` | + +*: This variable can only be used as a part of a style string + +### Example + +```toml +# ~/.config/starship.toml + +[meson] +disabled = false +truncation_symbol = "--" +symbol = " " +style = "bold dimmed green" +``` + ## Mercurial Branch The `hg_branch` module shows the active branch of the repo in your current directory. diff --git a/src/configs/meson.rs b/src/configs/meson.rs new file mode 100644 index 000000000..0325c3f56 --- /dev/null +++ b/src/configs/meson.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Deserialize, Serialize)] +#[cfg_attr( + feature = "config-schema", + derive(schemars::JsonSchema), + schemars(deny_unknown_fields) +)] +#[serde(default)] +pub struct MesonConfig<'a> { + pub truncation_length: u32, + pub truncation_symbol: &'a str, + pub format: &'a str, + pub symbol: &'a str, + pub style: &'a str, + pub disabled: bool, +} + +impl<'a> Default for MesonConfig<'a> { + fn default() -> Self { + MesonConfig { + truncation_length: std::u32::MAX, + truncation_symbol: "…", + format: "via [$symbol$project]($style) ", + symbol: "⬢ ", + style: "blue bold", + disabled: false, + } + } +} diff --git a/src/configs/mod.rs b/src/configs/mod.rs index ec47d5012..e767d0d81 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -46,6 +46,7 @@ pub mod line_break; pub mod localip; pub mod lua; pub mod memory_usage; +pub mod meson; pub mod nim; pub mod nix_shell; pub mod nodejs; @@ -184,6 +185,8 @@ pub struct FullConfig<'a> { #[serde(borrow)] memory_usage: memory_usage::MemoryConfig<'a>, #[serde(borrow)] + meson: meson::MesonConfig<'a>, + #[serde(borrow)] nim: nim::NimConfig<'a>, #[serde(borrow)] nix_shell: nix_shell::NixShellConfig<'a>, diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index aeb03358a..6cc4da77b 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -87,6 +87,7 @@ pub const PROMPT_ORDER: &[&str] = &[ "buf", "nix_shell", "conda", + "meson", "spack", "memory_usage", "aws", diff --git a/src/module.rs b/src/module.rs index c83947e24..c23e9435d 100644 --- a/src/module.rs +++ b/src/module.rs @@ -54,6 +54,7 @@ pub const ALL_MODULES: &[&str] = &[ "localip", "lua", "memory_usage", + "meson", "nim", "nix_shell", "nodejs", diff --git a/src/modules/meson.rs b/src/modules/meson.rs new file mode 100644 index 000000000..afbedcb5a --- /dev/null +++ b/src/modules/meson.rs @@ -0,0 +1,98 @@ +use super::{Context, Module, ModuleConfig}; + +use super::utils::truncate::truncate_text; +use crate::configs::meson::MesonConfig; +use crate::formatter::StringFormatter; + +/// Creates a module with the current Meson dev environment +/// +/// Will display the Meson environment if `$MESON_DEVENV` and `MESON_PROJECT_NAME` are set. +pub fn module<'a>(context: &'a Context) -> Option> { + let meson_env = context.get_env("MESON_DEVENV")?; + let project_env = context.get_env("MESON_PROJECT_NAME")?; + if meson_env != "1" || project_env.trim().is_empty() { + return None; + } + + let mut module = context.new_module("meson"); + let config: MesonConfig = MesonConfig::try_load(module.config); + + let truncated_text = truncate_text( + &project_env, + config.truncation_length as usize, + config.truncation_symbol, + ); + + let parsed = StringFormatter::new(config.format).and_then(|formatter| { + formatter + .map_meta(|variable, _| match variable { + "symbol" => Some(config.symbol), + _ => None, + }) + .map_style(|variable| match variable { + "style" => Some(Ok(config.style)), + _ => None, + }) + .map(|variable| match variable { + "project" => Some(Ok(&truncated_text)), + _ => None, + }) + .parse(None, Some(context)) + }); + + module.set_segments(match parsed { + Ok(segments) => segments, + Err(error) => { + log::warn!("Error in module `meson`:\n{}", error); + return None; + } + }); + + Some(module) +} + +#[cfg(test)] +mod tests { + use crate::test::ModuleRenderer; + use nu_ansi_term::Color; + + #[test] + fn not_in_env() { + let actual = ModuleRenderer::new("meson").collect(); + + let expected = None; + + assert_eq!(expected, actual); + } + + #[test] + fn env_set() { + let actual = ModuleRenderer::new("meson") + .env("MESON_DEVENV", "1") + .env("MESON_PROJECT_NAME", "starship") + .collect(); + + let expected = Some(format!("via {} ", Color::Blue.bold().paint("⬢ starship"))); + + assert_eq!(expected, actual); + } + + #[test] + fn env_invalid_devenv() { + let actual = ModuleRenderer::new("meson") + .env("MESON_DEVENV", "0") + .env("MESON_PROJECT_NAME", "starship") + .collect(); + let expected = None; + assert_eq!(expected, actual); + } + #[test] + fn env_invalid_project_name() { + let actual = ModuleRenderer::new("meson") + .env("MESON_DEVENV", "1") + .env("MESON_PROJECT_NAME", " ") + .collect(); + let expected = None; + assert_eq!(expected, actual); + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index b6575281b..6626e1c77 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -43,6 +43,7 @@ mod line_break; mod localip; mod lua; mod memory_usage; +mod meson; mod nim; mod nix_shell; mod nodejs; @@ -137,6 +138,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "localip" => localip::module(context), "lua" => lua::module(context), "memory_usage" => memory_usage::module(context), + "meson" => meson::module(context), "nim" => nim::module(context), "nix_shell" => nix_shell::module(context), "nodejs" => nodejs::module(context), @@ -242,6 +244,9 @@ pub fn description(module: &str) -> &'static str { "localip" => "The currently assigned ipv4 address", "lua" => "The currently installed version of Lua", "memory_usage" => "Current system memory and swap usage", + "meson" => { + "The current Meson environment, if $MESON_DEVENV and $MESON_PROJECT_NAME are set" + } "nim" => "The currently installed version of Nim", "nix_shell" => "The nix-shell environment", "nodejs" => "The currently installed version of NodeJS", diff --git a/src/modules/utils/mod.rs b/src/modules/utils/mod.rs index 1d509c14b..209da8ed6 100644 --- a/src/modules/utils/mod.rs +++ b/src/modules/utils/mod.rs @@ -7,3 +7,5 @@ pub mod directory_win; pub mod directory_nix; pub mod path; + +pub mod truncate; diff --git a/src/modules/utils/truncate.rs b/src/modules/utils/truncate.rs new file mode 100644 index 000000000..9ec920cb6 --- /dev/null +++ b/src/modules/utils/truncate.rs @@ -0,0 +1,107 @@ +use unicode_segmentation::UnicodeSegmentation; + +/// Truncate a string to only have a set number of characters +/// +/// Will truncate a string to only show the last `length` character in the string. +/// If a length of `0` is provided, the string will not be truncated and the original +/// will be returned. +pub fn truncate_text(text: &str, length: usize, truncation_symbol: &str) -> String { + if length == 0 { + return String::from(text); + } + + let truncated_graphemes = get_graphemes(text, length); + // The truncation symbol should only be added if we truncated + let truncated_and_symbol = if length < graphemes_len(text) { + let truncation_symbol = get_graphemes(truncation_symbol, 1); + truncated_graphemes + truncation_symbol.as_str() + } else { + truncated_graphemes + }; + + truncated_and_symbol +} + +fn get_graphemes(text: &str, length: usize) -> String { + UnicodeSegmentation::graphemes(text, true) + .take(length) + .collect::>() + .concat() +} + +fn graphemes_len(text: &str) -> usize { + UnicodeSegmentation::graphemes(text, true).count() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multi_char_truncation_symbol() { + let actual = truncate_text("1337_hello_world", 15, "apple"); + + assert_eq!("1337_hello_worla", actual); + } + + #[test] + fn test_changed_truncation_symbol() { + test_truncate_length("1337_hello_world", 15, "1337_hello_worl", "%") + } + + #[test] + fn test_no_truncation_symbol() { + test_truncate_length("1337_hello_world", 15, "1337_hello_worl", "") + } + + #[test] + fn test_ascii_boundary_below() { + test_truncate_length("1337_hello_world", 15, "1337_hello_worl", "…") + } + + #[test] + fn test_ascii_boundary_on() { + test_truncate_length("1337_hello_world", 16, "1337_hello_world", "") + } + + #[test] + fn test_ascii_boundary_above() { + test_truncate_length("1337_hello_world", 17, "1337_hello_world", "") + } + + #[test] + fn test_one() { + test_truncate_length("1337_hello_world", 1, "1", "…") + } + + #[test] + fn test_negative() { + test_truncate_length("1337_hello_world", -1, "1337_hello_world", "") + } + + #[test] + fn test_hindi_truncation() { + test_truncate_length("नमस्ते", 3, "नमस्", "…") + } + + #[test] + fn test_hindi_truncation2() { + test_truncate_length("नमस्त", 3, "नमस्", "…") + } + + #[test] + fn test_japanese_truncation() { + test_truncate_length("がんばってね", 4, "がんばっ", "…") + } + + fn test_truncate_length( + text: &str, + truncate_length: i64, + expected: &str, + truncation_symbol: &str, + ) { + let actual = truncate_text(text, truncate_length as usize, truncation_symbol); + + assert_eq!(format!("{}{}", expected, truncation_symbol), actual); + } +}