diff --git a/.github/config-schema.json b/.github/config-schema.json index e4111cbc..702d57cb 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -1215,6 +1215,21 @@ } ] }, + "pijul_channel": { + "default": { + "disabled": true, + "format": "on [$symbol$channel]($style) ", + "style": "bold purple", + "symbol": " ", + "truncation_length": 9223372036854775807, + "truncation_symbol": "…" + }, + "allOf": [ + { + "$ref": "#/definitions/PijulConfig" + } + ] + }, "pulumi": { "default": { "disabled": false, @@ -4495,6 +4510,37 @@ }, "additionalProperties": false }, + "PijulConfig": { + "type": "object", + "properties": { + "symbol": { + "default": " ", + "type": "string" + }, + "style": { + "default": "bold purple", + "type": "string" + }, + "format": { + "default": "on [$symbol$channel]($style) ", + "type": "string" + }, + "truncation_length": { + "default": 9223372036854775807, + "type": "integer", + "format": "int64" + }, + "truncation_symbol": { + "default": "…", + "type": "string" + }, + "disabled": { + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + }, "PulumiConfig": { "type": "object", "properties": { diff --git a/docs/.vuepress/public/presets/toml/bracketed-segments.toml b/docs/.vuepress/public/presets/toml/bracketed-segments.toml index deaa6ae8..98d3cef2 100644 --- a/docs/.vuepress/public/presets/toml/bracketed-segments.toml +++ b/docs/.vuepress/public/presets/toml/bracketed-segments.toml @@ -130,6 +130,9 @@ format = '\[[$symbol($version)]($style)\]' [php] format = '\[[$symbol($version)]($style)\]' +[pijul_channel] +format = '\[[$symbol$channel]($style)\]' + [pulumi] format = '\[[$symbol$stack]($style)\]' diff --git a/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml b/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml index 18cb4703..49945e1a 100644 --- a/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml +++ b/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml @@ -108,6 +108,9 @@ Windows = " " [package] symbol = " " +[pijul_channel] +symbol = "🪺 " + [python] symbol = " " diff --git a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml index e9ee0b95..eb7d43a1 100644 --- a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml +++ b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml @@ -156,6 +156,9 @@ symbol = "pl " [php] symbol = "php " +[pijul_channel] +symbol = "pijul " + [pulumi] symbol = "pulumi " diff --git a/docs/config/README.md b/docs/config/README.md index 1fc4968f..601b86e9 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -271,6 +271,7 @@ $git_state\ $git_metrics\ $git_status\ $hg_branch\ +$pijul_channel\ $docker_context\ $package\ $c\ @@ -3175,6 +3176,21 @@ By default the module will be shown if any of the following conditions are met: format = 'via [🔹 $version](147 bold) ' ``` +## Pijul Channel + +The `pijul_channel` module shows the active channel of the repo in your current directory. + +### Options + +| Option | Default | Description | +| ------------------- | --------------------------------- | ------------------------------------------------------------------------------------ | +| `symbol` | `' '` | The symbol used before the pijul channel name of the repo in your current directory. | +| `style` | `'bold purple'` | The style for the module. | +| `format` | `'on [$symbol$channel]($style) '` | The format for the module. | +| `truncation_length` | `2^63 - 1` | Truncates the pijul channel name to `N` graphemes | +| `truncation_symbol` | `'…'` | The symbol used to indicate a branch name was truncated. | +| `disabled` | `true` | Disables the `pijul` module. | + ## Pulumi The `pulumi` module shows the current username, selected [Pulumi Stack](https://www.pulumi.com/docs/intro/concepts/stack/), and version. diff --git a/src/configs/mod.rs b/src/configs/mod.rs index 8a712e5f..0aee1fe4 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -61,6 +61,7 @@ pub mod os; pub mod package; pub mod perl; pub mod php; +pub mod pijul_channel; pub mod pulumi; pub mod purescript; pub mod python; @@ -221,6 +222,8 @@ pub struct FullConfig<'a> { #[serde(borrow)] php: php::PhpConfig<'a>, #[serde(borrow)] + pijul_channel: pijul_channel::PijulConfig<'a>, + #[serde(borrow)] pulumi: pulumi::PulumiConfig<'a>, #[serde(borrow)] purescript: purescript::PureScriptConfig<'a>, diff --git a/src/configs/pijul_channel.rs b/src/configs/pijul_channel.rs new file mode 100644 index 00000000..031bea6c --- /dev/null +++ b/src/configs/pijul_channel.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 PijulConfig<'a> { + pub symbol: &'a str, + pub style: &'a str, + pub format: &'a str, + pub truncation_length: i64, + pub truncation_symbol: &'a str, + pub disabled: bool, +} + +impl<'a> Default for PijulConfig<'a> { + fn default() -> Self { + PijulConfig { + symbol: " ", + style: "bold purple", + format: "on [$symbol$channel]($style) ", + truncation_length: std::i64::MAX, + truncation_symbol: "…", + disabled: true, + } + } +} diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index 891e9074..d857ae55 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -42,6 +42,7 @@ pub const PROMPT_ORDER: &[&str] = &[ "git_metrics", "git_status", "hg_branch", + "pijul_channel", "docker_context", "package", // ↓ Toolchain version modules ↓ diff --git a/src/module.rs b/src/module.rs index 17a345b6..f678b312 100644 --- a/src/module.rs +++ b/src/module.rs @@ -68,6 +68,7 @@ pub const ALL_MODULES: &[&str] = &[ "package", "perl", "php", + "pijul_channel", "pulumi", "purescript", "python", diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 53c36ec9..4ca7adea 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -58,6 +58,7 @@ mod os; mod package; mod perl; mod php; +mod pijul_channel; mod pulumi; mod purescript; mod python; @@ -159,6 +160,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "package" => package::module(context), "perl" => perl::module(context), "php" => php::module(context), + "pijul_channel" => pijul_channel::module(context), "pulumi" => pulumi::module(context), "purescript" => purescript::module(context), "python" => python::module(context), @@ -273,6 +275,7 @@ pub fn description(module: &str) -> &'static str { "package" => "The package version of the current directory's project", "perl" => "The currently installed version of Perl", "php" => "The currently installed version of PHP", + "pijul_channel" => "The current channel of the repo in the current directory", "pulumi" => "The current username, stack, and installed version of Pulumi", "purescript" => "The currently installed version of PureScript", "python" => "The currently installed version of Python", diff --git a/src/modules/pijul_channel.rs b/src/modules/pijul_channel.rs new file mode 100644 index 00000000..c5af4814 --- /dev/null +++ b/src/modules/pijul_channel.rs @@ -0,0 +1,220 @@ +use super::utils::truncate::truncate_text; +use super::{Context, Module, ModuleConfig}; + +use crate::configs::pijul_channel::PijulConfig; +use crate::formatter::StringFormatter; + +/// Creates a module with the Pijul channel in the current directory +/// +/// Will display the channel lame if the current directory is a pijul repo +pub fn module<'a>(context: &'a Context) -> Option> { + let is_repo = context + .try_begin_scan()? + .set_folders(&[".pijul"]) + .is_match(); + + if !is_repo { + return None; + } + + let mut module = context.new_module("pijul_channel"); + let config: PijulConfig = PijulConfig::try_load(module.config); + + // We default to disabled=true, so we have to check after loading our config module. + if config.disabled { + return None; + }; + + let channel_name = get_pijul_current_channel(context)?; + + let truncated_text = truncate_text( + &channel_name, + 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 { + "channel" => Some(Ok(&truncated_text)), + _ => None, + }) + .parse(None, Some(context)) + }); + + module.set_segments(match parsed { + Ok(segments) => segments, + Err(error) => { + log::warn!("Error in module `pijul_channel`:\n{}", error); + return None; + } + }); + + Some(module) +} + +fn get_pijul_current_channel(ctx: &Context) -> Option { + let output = ctx.exec_cmd("pijul", &["channel"])?.stdout; + + output + .lines() + .find_map(|l| l.strip_prefix("* ")) + .map(str::to_owned) +} + +#[cfg(test)] +mod tests { + use nu_ansi_term::{Color, Style}; + use std::io; + use std::path::Path; + + use crate::test::{fixture_repo, FixtureProvider, ModuleRenderer}; + + enum Expect<'a> { + ChannelName(&'a str), + Empty, + NoTruncation, + Symbol(&'a str), + Style(Style), + TruncationSymbol(&'a str), + } + + #[test] + fn show_nothing_on_empty_dir() -> io::Result<()> { + let repo_dir = tempfile::tempdir()?; + + let actual = ModuleRenderer::new("pijul_channel") + .path(repo_dir.path()) + .collect(); + + let expected = None; + assert_eq!(expected, actual); + repo_dir.close() + } + + #[test] + fn test_pijul_disabled_per_default() -> io::Result<()> { + let tempdir = fixture_repo(FixtureProvider::Pijul)?; + let repo_dir = tempdir.path(); + expect_pijul_with_config( + repo_dir, + Some(toml::toml! { + [pijul_channel] + truncation_length = 14 + }), + &[Expect::Empty], + ); + tempdir.close() + } + + #[test] + fn test_pijul_autodisabled() -> io::Result<()> { + let tempdir = tempfile::tempdir()?; + expect_pijul_with_config(tempdir.path(), None, &[Expect::Empty]); + tempdir.close() + } + + #[test] + fn test_pijul_channel() -> io::Result<()> { + let tempdir = fixture_repo(FixtureProvider::Pijul)?; + let repo_dir = tempdir.path(); + run_pijul(&["channel", "new", "tributary-48198"], repo_dir)?; + run_pijul(&["channel", "switch", "tributary-48198"], repo_dir)?; + expect_pijul_with_config( + repo_dir, + None, + &[Expect::ChannelName("tributary-48198"), Expect::NoTruncation], + ); + tempdir.close() + } + + #[test] + fn test_pijul_configured() -> io::Result<()> { + let tempdir = fixture_repo(FixtureProvider::Pijul)?; + let repo_dir = tempdir.path(); + run_pijul(&["channel", "new", "tributary-48198"], repo_dir)?; + run_pijul(&["channel", "switch", "tributary-48198"], repo_dir)?; + expect_pijul_with_config( + repo_dir, + Some(toml::toml! { + [pijul_channel] + style = "underline blue" + symbol = "P " + truncation_length = 14 + truncation_symbol = "%" + disabled = false + }), + &[ + Expect::ChannelName("tributary-4819"), + Expect::Style(Color::Blue.underline()), + Expect::Symbol("P"), + Expect::TruncationSymbol("%"), + ], + ); + tempdir.close() + } + + fn expect_pijul_with_config( + repo_dir: &Path, + config: Option, + expectations: &[Expect], + ) { + let actual = ModuleRenderer::new("pijul_channel") + .path(repo_dir.to_str().unwrap()) + .config(config.unwrap_or_else(|| { + toml::toml! { + [pijul_channel] + disabled = false + } + })) + .collect(); + + let mut expect_channel_name = "main"; + 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::ChannelName(channel_name) => { + expect_channel_name = channel_name; + } + Expect::Style(style) => expect_style = *style, + } + } + + let expected = Some(format!( + "on {} ", + expect_style.paint(format!( + "{expect_symbol} {expect_channel_name}{expect_truncation_symbol}" + )), + )); + assert_eq!(expected, actual); + } + + fn run_pijul(args: &[&str], _repo_dir: &Path) -> io::Result<()> { + crate::utils::mock_cmd("pijul", args).ok_or(io::ErrorKind::Unsupported)?; + Ok(()) + } +} diff --git a/src/test/mod.rs b/src/test/mod.rs index c6320226..c761bb98 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -7,6 +7,7 @@ use crate::{ }; use log::{Level, LevelFilter}; use once_cell::sync::Lazy; +use std::fs; use std::io; use std::path::{Path, PathBuf}; use tempfile::TempDir; @@ -165,6 +166,7 @@ impl<'a> ModuleRenderer<'a> { pub enum FixtureProvider { Git, Hg, + Pijul, } pub fn fixture_repo(provider: FixtureProvider) -> io::Result { @@ -223,5 +225,10 @@ pub fn fixture_repo(provider: FixtureProvider) -> io::Result { Ok(path) } + FixtureProvider::Pijul => { + let path = tempfile::tempdir()?; + fs::create_dir(path.path().join(".pijul"))?; + Ok(path) + } } } diff --git a/src/utils.rs b/src/utils.rs index 1d035dac..1cbed2e3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -337,6 +337,18 @@ WebAssembly: unavailable stderr: String::default(), }) }, + "pijul channel" => Some(CommandOutput{ + stdout: String::from(" main\n* tributary-48198"), + stderr: String::default(), + }), + "pijul channel new tributary-48198" => Some(CommandOutput{ + stdout: String::default(), + stderr: String::default(), + }), + "pijul channel switch tributary-48198" => Some(CommandOutput{ + stdout: String::from("Outputting repository ↖"), + stderr: String::default(), + }), "pulumi version" => Some(CommandOutput{ stdout: String::from("1.2.3-ver.1631311768+e696fb6c"), stderr: String::default(),