diff --git a/.github/config-schema.json b/.github/config-schema.json index 1bea08fe..5631940b 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -1091,6 +1091,31 @@ } ] }, + "raku": { + "default": { + "detect_extensions": [ + "p6", + "pm6", + "pod6", + "raku", + "rakumod" + ], + "detect_files": [ + "META6.json" + ], + "detect_folders": [], + "disabled": false, + "format": "via [$symbol($version-$vm_version )]($style)", + "style": "149 bold", + "symbol": "🦋 ", + "version_format": "${raw}" + }, + "allOf": [ + { + "$ref": "#/definitions/RakuConfig" + } + ] + }, "red": { "default": { "detect_extensions": [ @@ -3948,6 +3973,60 @@ } ] }, + "RakuConfig": { + "type": "object", + "properties": { + "format": { + "default": "via [$symbol($version-$vm_version )]($style)", + "type": "string" + }, + "version_format": { + "default": "${raw}", + "type": "string" + }, + "symbol": { + "default": "🦋 ", + "type": "string" + }, + "style": { + "default": "149 bold", + "type": "string" + }, + "disabled": { + "default": false, + "type": "boolean" + }, + "detect_extensions": { + "default": [ + "p6", + "pm6", + "pod6", + "raku", + "rakumod" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "detect_files": { + "default": [ + "META6.json" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "detect_folders": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, "RedConfig": { "type": "object", "properties": { diff --git a/docs/.vuepress/public/presets/toml/bracketed-segments.toml b/docs/.vuepress/public/presets/toml/bracketed-segments.toml index 9e84c5d1..1bd05a1c 100644 --- a/docs/.vuepress/public/presets/toml/bracketed-segments.toml +++ b/docs/.vuepress/public/presets/toml/bracketed-segments.toml @@ -115,6 +115,9 @@ format = '\[[$symbol($version)]($style)\]' [python] format = '\[[${symbol}${pyenv_prefix}(${version})(\($virtualenv\))]($style)\]' +[raku] +format = '\[[$symbol($version-$vm_version)]($style)\]' + [red] format = '\[[$symbol($version)]($style)\]' diff --git a/docs/.vuepress/public/presets/toml/no-runtime-versions.toml b/docs/.vuepress/public/presets/toml/no-runtime-versions.toml index 0f28eb80..1728d816 100644 --- a/docs/.vuepress/public/presets/toml/no-runtime-versions.toml +++ b/docs/.vuepress/public/presets/toml/no-runtime-versions.toml @@ -70,6 +70,9 @@ format = 'via [$symbol]($style)' [python] format = 'via [$symbol]($style)' +[raku] +format = 'via [$symbol]($style)' + [red] 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 0a433339..297bb631 100644 --- a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml +++ b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml @@ -109,6 +109,9 @@ symbol = "purs " [python] symbol = "py " +[raku] +symbol = "raku " + [ruby] symbol = "rb " diff --git a/docs/config/README.md b/docs/config/README.md index f7965e5d..77c1927c 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -2928,6 +2928,45 @@ any of the following conditions are met: format = "with [📐 $version](blue bold) " ``` +## Raku + +The `raku` module shows the currently installed version of [Raku](https://www.raku.org/). +By default the module will be shown if any of the following conditions are met: + +- The current directory contains a `META6.json` file +- The current directory contains a `.p6`, `.pm6`, `.raku`, `.rakumod` or `.pod6` + +### Options + +| Option | Default | Description | +| ------------------- | ------------------------------------------------ | ------------------------------------------------------------------------- | +| `format` | `"via [$symbol($version-$vm_version )]($style)"` | The format string for the module. | +| `version_format` | `"v${raw}"` | The version format. Available vars are `raw`, `major`, `minor`, & `patch` | +| `symbol` | `"🦋 "` | The symbol used before displaying the version of Raku | +| `detect_extensions` | `["p6", "pm6", "pod6", "raku", "rakumod"]` | Which extensions should trigger this module. | +| `detect_files` | `["META6.json"]` | Which filenames should trigger this module. | +| `detect_folders` | `[]` | Which folders should trigger this module. | +| `style` | `"bold 149"` | The style for the module. | +| `disabled` | `false` | Disables the `raku` module. | + +### Variables + +| Variable | Example | Description | +| ---------- | ------- | ------------------------------------ | +| version | `v6.d` | The version of `raku` | +| vm_version | `moar` | The version of VM `raku` is built on | +| symbol | | Mirrors the value of option `symbol` | +| style\* | | Mirrors the value of option `style` | + +### Example + +```toml +# ~/.config/starship.toml + +[raku] +format = "via [🦪 $version]($style) " +``` + ## Red By default the `red` module shows the currently installed version of [Red](https://www.red-lang.org/). diff --git a/src/configs/mod.rs b/src/configs/mod.rs index 04bba7e9..9b24cbd1 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -56,6 +56,7 @@ pub mod php; pub mod pulumi; pub mod purescript; pub mod python; +pub mod raku; pub mod red; pub mod rlang; pub mod ruby; @@ -198,6 +199,8 @@ pub struct FullConfig<'a> { #[serde(borrow)] python: python::PythonConfig<'a>, #[serde(borrow)] + raku: raku::RakuConfig<'a>, + #[serde(borrow)] red: red::RedConfig<'a>, #[serde(borrow)] rlang: rlang::RLangConfig<'a>, diff --git a/src/configs/raku.rs b/src/configs/raku.rs new file mode 100644 index 00000000..109d2b0f --- /dev/null +++ b/src/configs/raku.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] +#[serde(default)] +pub struct RakuConfig<'a> { + pub format: &'a str, + pub version_format: &'a str, + pub symbol: &'a str, + pub style: &'a str, + pub disabled: bool, + pub detect_extensions: Vec<&'a str>, + pub detect_files: Vec<&'a str>, + pub detect_folders: Vec<&'a str>, +} + +impl<'a> Default for RakuConfig<'a> { + fn default() -> Self { + RakuConfig { + format: "via [$symbol($version-$vm_version )]($style)", + version_format: "${raw}", + symbol: "🦋 ", + style: "149 bold", + disabled: false, + detect_extensions: vec!["p6", "pm6", "pod6", "raku", "rakumod"], + detect_files: vec!["META6.json"], + detect_folders: vec![], + } + } +} diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index 9c61aebc..9f855957 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -61,6 +61,7 @@ pub const PROMPT_ORDER: &[&str] = &[ "pulumi", "purescript", "python", + "raku", "rlang", "red", "ruby", diff --git a/src/module.rs b/src/module.rs index a5f80538..79d5553b 100644 --- a/src/module.rs +++ b/src/module.rs @@ -64,6 +64,7 @@ pub const ALL_MODULES: &[&str] = &[ "pulumi", "purescript", "python", + "raku", "red", "rlang", "ruby", diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 5eeace43..07425cec 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -53,6 +53,7 @@ mod php; mod pulumi; mod purescript; mod python; +mod raku; mod red; mod rlang; mod ruby; @@ -145,6 +146,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "pulumi" => pulumi::module(context), "purescript" => purescript::module(context), "python" => python::module(context), + "raku" => raku::module(context), "rlang" => rlang::module(context), "red" => red::module(context), "ruby" => ruby::module(context), @@ -248,6 +250,7 @@ pub fn description(module: &str) -> &'static str { "pulumi" => "The current username, stack, and installed version of Pulumi", "purescript" => "The currently installed version of PureScript", "python" => "The currently installed version of Python", + "raku" => "The currently installed version of Raku", "red" => "The currently installed version of Red", "rlang" => "The currently installed version of R", "ruby" => "The currently installed version of Ruby", diff --git a/src/modules/raku.rs b/src/modules/raku.rs new file mode 100644 index 00000000..377d4959 --- /dev/null +++ b/src/modules/raku.rs @@ -0,0 +1,199 @@ +use super::{Context, Module, ModuleConfig}; + +use crate::configs::raku::RakuConfig; +use crate::formatter::StringFormatter; +use crate::formatter::VersionFormatter; +use once_cell::sync::Lazy; +use std::ops::Deref; + +/// Creates a module with the current raku version +pub fn module<'a>(context: &'a Context) -> Option> { + let mut module = context.new_module("raku"); + let config: RakuConfig = RakuConfig::try_load(module.config); + let is_raku_project = context + .try_begin_scan()? + .set_extensions(&config.detect_extensions) + .set_files(&config.detect_files) + .set_folders(&config.detect_folders) + .is_match(); + + if !is_raku_project { + return None; + } + + let versions = Lazy::new(|| get_raku_version(context)); + + 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(|variable| match variable { + "version" => versions + .deref() + .as_ref() + .map(|(raku_version, _)| raku_version) + .map(|raku_version| { + VersionFormatter::format_module_version( + module.get_name(), + raku_version, + config.version_format, + ) + })? + .map(Ok), + "vm_version" => versions + .deref() + .as_ref() + .map(|(_, vm_version)| vm_version.to_string()) + .map(Ok), + _ => None, + }) + .parse(None, Some(context)) + }); + + module.set_segments(match parsed { + Ok(segments) => segments, + Err(error) => { + log::warn!("Error in module `raku`:\n{}", error); + return None; + } + }); + + Some(module) +} + +fn get_raku_version(context: &Context) -> Option<(String, String)> { + let output = context.exec_cmd("raku", &["--version"])?.stdout; + + parse_raku_version(&output) +} + +fn parse_raku_version(version: &str) -> Option<(String, String)> { + let mut lines = version.lines(); + // skip 1st line + let _ = lines.next()?; + // split 2nd line into ["Implement", "the", "Raku®", ..., "v6.d."], take "v6.d." + // get rid of the trailing "." + let raku_version = lines + .next()? + .split_whitespace() + .nth(5)? + .strip_suffix('.')? + .to_string(); + + // split line into ["Built", "on", "MoarVM", ...], take "MoarVM" + // and change MoarVM to Moar (community's preference), leave other VMs as they are + let vm_version = lines + .next()? + .split_whitespace() + .nth(2)? + .replace("MoarVM", "Moar"); + + Some((raku_version.to_lowercase(), vm_version.to_lowercase())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::ModuleRenderer; + use ansi_term::Color; + use std::fs::File; + use std::io; + + #[test] + fn test_parse_raku_version() { + let moar_input = "\ +Welcome to Rakudo™ v2021.12. +Implementing the Raku® Programming Language v6.d. +Built on MoarVM version 2021.12. +"; + let jvm_input = "\ +Welcome to Rakudo™ v2021.12. +Implementing the Raku® Programming Language v6.d. +Built on JVM version 2021.12. +"; + assert_eq!( + parse_raku_version(moar_input), + Some(("v6.d".to_string(), "moar".to_string())) + ); + assert_eq!( + parse_raku_version(jvm_input), + Some(("v6.d".to_string(), "jvm".to_string())) + ); + } + + #[test] + fn folder_without_raku_files() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let actual = ModuleRenderer::new("raku").path(dir.path()).collect(); + let expected = None; + assert_eq!(expected, actual); + + dir.close() + } + + #[test] + fn folder_with_meta6_json_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("META6.json"))?.sync_all()?; + + let actual = ModuleRenderer::new("raku").path(dir.path()).collect(); + + let expected = Some(format!( + "via {}", + Color::Fixed(149).bold().paint("🦋 v6.d-moar ") + )); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_with_raku_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("any.raku"))?.sync_all()?; + + let actual = ModuleRenderer::new("raku").path(dir.path()).collect(); + + let expected = Some(format!( + "via {}", + Color::Fixed(149).bold().paint("🦋 v6.d-moar ") + )); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_with_raku_module_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("any.rakumod"))?.sync_all()?; + + let actual = ModuleRenderer::new("raku").path(dir.path()).collect(); + + let expected = Some(format!( + "via {}", + Color::Fixed(149).bold().paint("🦋 v6.d-moar ") + )); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_with_rakudoc_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("any.pod6"))?.sync_all()?; + + let actual = ModuleRenderer::new("raku").path(dir.path()).collect(); + + let expected = Some(format!( + "via {}", + Color::Fixed(149).bold().paint("🦋 v6.d-moar ") + )); + assert_eq!(expected, actual); + dir.close() + } +} diff --git a/src/utils.rs b/src/utils.rs index a2e66d57..07b5bc2e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -319,6 +319,15 @@ For more information about these matters see https://www.gnu.org/licenses/."# ), }), + "raku --version" => Some(CommandOutput { + stdout: String::from( + "\ +Welcome to Rakudo™ v2021.12. +Implementing the Raku® Programming Language v6.d. +Built on MoarVM version 2021.12.\n", + ), + stderr: String::default(), + }), "red --version" => Some(CommandOutput { stdout: String::from("0.6.4\n"), stderr: String::default()