From 41eb98b310cd8134cec7bd8dcb55869a984653cf Mon Sep 17 00:00:00 2001 From: Vegard Skui Date: Tue, 31 Jan 2023 09:06:46 +0100 Subject: [PATCH] feat(fossil_branch): add fossil_branch module (#4806) Co-authored-by: David Knaack --- .github/config-schema.json | 46 ++++ .../presets/toml/bracketed-segments.toml | 3 + .../presets/toml/nerd-font-symbols.toml | 3 + .../presets/toml/plain-text-symbols.toml | 3 + docs/config/README.md | 37 +++ src/configs/fossil_branch.rs | 30 +++ src/configs/mod.rs | 3 + src/configs/starship_root.rs | 1 + src/module.rs | 1 + src/modules/fossil_branch.rs | 215 ++++++++++++++++++ src/modules/mod.rs | 3 + src/test/mod.rs | 10 + src/utils.rs | 12 + 13 files changed, 367 insertions(+) create mode 100644 src/configs/fossil_branch.rs create mode 100644 src/modules/fossil_branch.rs diff --git a/.github/config-schema.json b/.github/config-schema.json index b2323378..c236f1e0 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -504,6 +504,21 @@ } ] }, + "fossil_branch": { + "default": { + "disabled": true, + "format": "on [$symbol$branch]($style) ", + "style": "bold purple", + "symbol": " ", + "truncation_length": 9223372036854775807, + "truncation_symbol": "…" + }, + "allOf": [ + { + "$ref": "#/definitions/FossilBranchConfig" + } + ] + }, "gcloud": { "default": { "disabled": false, @@ -2981,6 +2996,37 @@ }, "additionalProperties": false }, + "FossilBranchConfig": { + "type": "object", + "properties": { + "format": { + "default": "on [$symbol$branch]($style) ", + "type": "string" + }, + "symbol": { + "default": " ", + "type": "string" + }, + "style": { + "default": "bold purple", + "type": "string" + }, + "truncation_length": { + "default": 9223372036854775807, + "type": "integer", + "format": "int64" + }, + "truncation_symbol": { + "default": "…", + "type": "string" + }, + "disabled": { + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + }, "GcloudConfig": { "type": "object", "properties": { diff --git a/docs/.vuepress/public/presets/toml/bracketed-segments.toml b/docs/.vuepress/public/presets/toml/bracketed-segments.toml index 98d3cef2..c75fc3a7 100644 --- a/docs/.vuepress/public/presets/toml/bracketed-segments.toml +++ b/docs/.vuepress/public/presets/toml/bracketed-segments.toml @@ -49,6 +49,9 @@ format = '\[[$symbol($version)]($style)\]' [fennel] format = '\[[$symbol($version)]($style)\]' +[fossil_branch] +format = '\[[$symbol$branch]($style)\]' + [gcloud] format = '\[[$symbol$account(@$domain)(\($region\))]($style)\]' diff --git a/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml b/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml index 49945e1a..b27e8394 100644 --- a/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml +++ b/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml @@ -25,6 +25,9 @@ symbol = " " [elm] symbol = " " +[fossil_branch] +symbol = " " + [git_branch] symbol = " " diff --git a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml index eb7d43a1..46c070d9 100644 --- a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml +++ b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml @@ -61,6 +61,9 @@ symbol = "elm " [fennel] symbol = "fnl " +[fossil_branch] +symbol = "fossil " + [git_branch] symbol = "git " diff --git a/docs/config/README.md b/docs/config/README.md index 601b86e9..5edc8d1f 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -265,6 +265,7 @@ $singularity\ $kubernetes\ $directory\ $vcsh\ +$fossil_branch\ $git_branch\ $git_commit\ $git_state\ @@ -1556,6 +1557,42 @@ Produces a prompt that looks like: AA -------------------------------------------- BB -------------------------------------------- CC ``` +## Fossil Branch + +The `fossil_branch` module shows the name of the active branch of the check-out in your current directory. + +### Options + +| Option | Default | Description | +| ------------------- | -------------------------------- | ---------------------------------------------------------------------------------------- | +| `format` | `'on [$symbol$branch]($style) '` | The format for the module. Use `'$branch'` to refer to the current branch name. | +| `symbol` | `' '` | The symbol used before the branch name of the check-out in your current directory. | +| `style` | `'bold purple'` | The style for the module. | +| `truncation_length` | `2^63 - 1` | Truncates a Fossil branch name to `N` graphemes | +| `truncation_symbol` | `'…'` | The symbol used to indicate a branch name was truncated. You can use `''` for no symbol. | +| `disabled` | `true` | Disables the `fossil_branch` module. | + +### Variables + +| Variable | Example | Description | +| -------- | ------- | ------------------------------------ | +| branch | `trunk` | The active Fossil branch | +| 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 + +[fossil_branch] +symbol = '🦎 ' +truncation_length = 4 +truncation_symbol = '' +``` + ## Google Cloud (`gcloud`) The `gcloud` module shows the current configuration for [`gcloud`](https://cloud.google.com/sdk/gcloud) CLI. diff --git a/src/configs/fossil_branch.rs b/src/configs/fossil_branch.rs new file mode 100644 index 00000000..c2d6b912 --- /dev/null +++ b/src/configs/fossil_branch.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 FossilBranchConfig<'a> { + pub format: &'a str, + pub symbol: &'a str, + pub style: &'a str, + pub truncation_length: i64, + pub truncation_symbol: &'a str, + pub disabled: bool, +} + +impl<'a> Default for FossilBranchConfig<'a> { + fn default() -> Self { + FossilBranchConfig { + format: "on [$symbol$branch]($style) ", + symbol: " ", + style: "bold purple", + truncation_length: std::i64::MAX, + truncation_symbol: "…", + disabled: true, + } + } +} diff --git a/src/configs/mod.rs b/src/configs/mod.rs index 0aee1fe4..a897e915 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -27,6 +27,7 @@ pub mod env_var; pub mod erlang; pub mod fennel; pub mod fill; +pub mod fossil_branch; pub mod gcloud; pub mod git_branch; pub mod git_commit; @@ -155,6 +156,8 @@ pub struct FullConfig<'a> { #[serde(borrow)] fill: fill::FillConfig<'a>, #[serde(borrow)] + fossil_branch: fossil_branch::FossilBranchConfig<'a>, + #[serde(borrow)] gcloud: gcloud::GcloudConfig<'a>, #[serde(borrow)] git_branch: git_branch::GitBranchConfig<'a>, diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index 3f29d0f6..717b1ae1 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -38,6 +38,7 @@ pub const PROMPT_ORDER: &[&str] = &[ "kubernetes", "directory", "vcsh", + "fossil_branch", "git_branch", "git_commit", "git_state", diff --git a/src/module.rs b/src/module.rs index f678b312..4e1caf2a 100644 --- a/src/module.rs +++ b/src/module.rs @@ -34,6 +34,7 @@ pub const ALL_MODULES: &[&str] = &[ "erlang", "fennel", "fill", + "fossil_branch", "gcloud", "git_branch", "git_commit", diff --git a/src/modules/fossil_branch.rs b/src/modules/fossil_branch.rs new file mode 100644 index 00000000..73016069 --- /dev/null +++ b/src/modules/fossil_branch.rs @@ -0,0 +1,215 @@ +use super::{Context, Module, ModuleConfig}; + +use crate::configs::fossil_branch::FossilBranchConfig; +use crate::formatter::StringFormatter; +use crate::modules::utils::truncate::truncate_text; + +/// Creates a module with the Fossil branch of the check-out in the current directory +/// +/// Will display the branch name if the current directory is a Fossil check-out +pub fn module<'a>(context: &'a Context) -> Option> { + let mut module = context.new_module("fossil_branch"); + let config = FossilBranchConfig::try_load(module.config); + + // As we default to disabled=true, we have to check here after loading our config module, + // before it was only checking against whatever is in the config starship.toml + if config.disabled { + return None; + }; + + let is_checkout = context + .try_begin_scan()? + .set_files(&[".fslckout"]) + .is_match(); + + if !is_checkout { + return None; + } + + let len = if config.truncation_length <= 0 { + log::warn!( + "\"truncation_length\" should be a positive value, found {}", + config.truncation_length + ); + std::usize::MAX + } else { + config.truncation_length as usize + }; + + let truncated_branch_name = { + let output = context.exec_cmd("fossil", &["branch", "current"])?.stdout; + truncate_text(output.trim(), len, 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 { + "branch" => Some(Ok(truncated_branch_name.as_str())), + _ => None, + }) + .parse(None, Some(context)) + }); + + module.set_segments(match parsed { + Ok(segments) => segments, + Err(error) => { + log::warn!("Error in module `fossil_branch`:\n{}", error); + return None; + } + }); + + Some(module) +} + +#[cfg(test)] +mod tests { + use std::io; + use std::path::Path; + + use nu_ansi_term::{Color, Style}; + + use crate::test::{fixture_repo, FixtureProvider, ModuleRenderer}; + + enum Expect<'a> { + BranchName(&'a str), + Empty, + NoTruncation, + Symbol(&'a str), + Style(Style), + TruncationSymbol(&'a str), + } + + #[test] + fn show_nothing_on_empty_dir() -> io::Result<()> { + let checkout_dir = tempfile::tempdir()?; + + let actual = ModuleRenderer::new("fossil_branch") + .path(checkout_dir.path()) + .collect(); + let expected = None; + assert_eq!(expected, actual); + + checkout_dir.close() + } + + #[test] + fn test_fossil_branch_disabled_per_default() -> io::Result<()> { + let tempdir = fixture_repo(FixtureProvider::Fossil)?; + let checkout_dir = tempdir.path(); + expect_fossil_branch_with_config( + checkout_dir, + Some(toml::toml! { + // no "disabled=false" in config! + [fossil_branch] + truncation_length = 14 + }), + &[Expect::Empty], + ); + tempdir.close() + } + + #[test] + fn test_fossil_branch_autodisabled() -> io::Result<()> { + let tempdir = tempfile::tempdir()?; + expect_fossil_branch_with_config(tempdir.path(), None, &[Expect::Empty]); + tempdir.close() + } + + #[test] + fn test_fossil_branch() -> io::Result<()> { + let tempdir = fixture_repo(FixtureProvider::Fossil)?; + let checkout_dir = tempdir.path(); + run_fossil(&["branch", "new", "topic-branch", "trunk"], checkout_dir)?; + run_fossil(&["update", "topic-branch"], checkout_dir)?; + expect_fossil_branch_with_config( + checkout_dir, + None, + &[Expect::BranchName("topic-branch"), Expect::NoTruncation], + ); + tempdir.close() + } + + #[test] + fn test_fossil_branch_configured() -> io::Result<()> { + let tempdir = fixture_repo(FixtureProvider::Fossil)?; + let checkout_dir = tempdir.path(); + run_fossil(&["branch", "new", "topic-branch", "trunk"], checkout_dir)?; + run_fossil(&["update", "topic-branch"], checkout_dir)?; + expect_fossil_branch_with_config( + checkout_dir, + Some(toml::toml! { + [fossil_branch] + style = "underline blue" + symbol = "F " + truncation_length = 10 + truncation_symbol = "%" + disabled = false + }), + &[ + Expect::BranchName("topic-bran"), + Expect::Style(Color::Blue.underline()), + Expect::Symbol("F"), + Expect::TruncationSymbol("%"), + ], + ); + tempdir.close() + } + + fn expect_fossil_branch_with_config( + checkout_dir: &Path, + config: Option, + expectations: &[Expect], + ) { + let actual = ModuleRenderer::new("fossil_branch") + .path(checkout_dir.to_str().unwrap()) + .config(config.unwrap_or_else(|| { + toml::toml! { + [fossil_branch] + disabled = false + } + })) + .collect(); + + let mut expect_branch_name = "trunk"; + let mut expect_style = Color::Purple.bold(); + let mut expect_symbol = "\u{e0a0}"; + let mut expect_truncation_symbol = "…"; + + for expect in expectations { + match expect { + Expect::Empty => { + assert_eq!(None, actual); + return; + } + Expect::Symbol(symbol) => expect_symbol = symbol, + Expect::TruncationSymbol(truncation_symbol) => { + expect_truncation_symbol = truncation_symbol + } + Expect::NoTruncation => expect_truncation_symbol = "", + Expect::BranchName(branch_name) => expect_branch_name = branch_name, + Expect::Style(style) => expect_style = *style, + } + } + + let expected = Some(format!( + "on {} ", + expect_style.paint(format!( + "{expect_symbol} {expect_branch_name}{expect_truncation_symbol}" + )) + )); + assert_eq!(expected, actual); + } + + fn run_fossil(args: &[&str], _checkout_dir: &Path) -> io::Result<()> { + crate::utils::mock_cmd("fossil", args).ok_or(io::ErrorKind::Unsupported)?; + Ok(()) + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 4ca7adea..28534e6b 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -24,6 +24,7 @@ mod env_var; mod erlang; mod fennel; mod fill; +mod fossil_branch; mod gcloud; mod git_branch; mod git_commit; @@ -126,6 +127,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "env_var" => env_var::module(None, context), "fennel" => fennel::module(context), "fill" => fill::module(context), + "fossil_branch" => fossil_branch::module(context), "gcloud" => gcloud::module(context), "git_branch" => git_branch::module(context), "git_commit" => git_commit::module(context), @@ -239,6 +241,7 @@ pub fn description(module: &str) -> &'static str { "erlang" => "Current OTP version", "fennel" => "The currently installed version of Fennel", "fill" => "Fills the remaining space on the line with a pad string", + "fossil_branch" => "The active branch of the check-out in your current directory", "gcloud" => "The current GCP client configuration", "git_branch" => "The active branch of the repo in your current directory", "git_commit" => "The active commit (and tag if any) of the repo in your current directory", diff --git a/src/test/mod.rs b/src/test/mod.rs index 9942b134..0e97bff8 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -167,6 +167,7 @@ impl<'a> ModuleRenderer<'a> { #[derive(Clone, Copy)] pub enum FixtureProvider { + Fossil, Git, Hg, Pijul, @@ -174,6 +175,15 @@ pub enum FixtureProvider { pub fn fixture_repo(provider: FixtureProvider) -> io::Result { match provider { + FixtureProvider::Fossil => { + let path = tempfile::tempdir()?; + fs::OpenOptions::new() + .create(true) + .write(true) + .open(path.path().join(".fslckout"))? + .sync_all()?; + Ok(path) + } FixtureProvider::Git => { let path = tempfile::tempdir()?; diff --git a/src/utils.rs b/src/utils.rs index 1cbed2e3..54b9e739 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -249,6 +249,18 @@ Elixir 1.10 (compiled with Erlang/OTP 22)\n", stdout: String::from("Fennel 1.2.1 on PUC Lua 5.4\n"), stderr: String::default(), }), + "fossil branch current" => Some(CommandOutput{ + stdout: String::from("topic-branch"), + stderr: String::default(), + }), + "fossil branch new topic-branch trunk" => Some(CommandOutput{ + stdout: String::default(), + stderr: String::default(), + }), + "fossil update topic-branch" => Some(CommandOutput{ + stdout: String::default(), + stderr: String::default(), + }), "go version" => Some(CommandOutput { stdout: String::from("go version go1.12.1 linux/amd64\n"), stderr: String::default(),