diff --git a/.github/config-schema.json b/.github/config-schema.json index d43b396da320..54944b4a62ae 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -658,6 +658,33 @@ } ] }, + "haxe": { + "default": { + "detect_extensions": [ + "hx", + "hxml" + ], + "detect_files": [ + "haxelib.json", + "hxformat.json", + ".haxerc" + ], + "detect_folders": [ + ".haxelib", + "haxe_libraries" + ], + "disabled": false, + "format": "via [$symbol($version )]($style)", + "style": "bold fg:202", + "symbol": "⌘ ", + "version_format": "v${raw}" + }, + "allOf": [ + { + "$ref": "#/definitions/HaxeConfig" + } + ] + }, "helm": { "default": { "detect_extensions": [], @@ -3238,6 +3265,63 @@ }, "additionalProperties": false }, + "HaxeConfig": { + "type": "object", + "properties": { + "format": { + "default": "via [$symbol($version )]($style)", + "type": "string" + }, + "version_format": { + "default": "v${raw}", + "type": "string" + }, + "symbol": { + "default": "⌘ ", + "type": "string" + }, + "style": { + "default": "bold fg:202", + "type": "string" + }, + "disabled": { + "default": false, + "type": "boolean" + }, + "detect_extensions": { + "default": [ + "hx", + "hxml" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "detect_files": { + "default": [ + "haxelib.json", + "hxformat.json", + ".haxerc" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "detect_folders": { + "default": [ + ".haxelib", + "haxe_libraries" + ], + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "HelmConfig": { "type": "object", "properties": { diff --git a/docs/.vuepress/public/presets/toml/bracketed-segments.toml b/docs/.vuepress/public/presets/toml/bracketed-segments.toml index 7317375a45c7..f871774ddd07 100644 --- a/docs/.vuepress/public/presets/toml/bracketed-segments.toml +++ b/docs/.vuepress/public/presets/toml/bracketed-segments.toml @@ -64,6 +64,9 @@ format = '\[[$symbol]($style)\]' [haskell] format = '\[[$symbol($version)]($style)\]' +[haxe] +format = '\[[$symbol($version)]($style)\]' + [helm] 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 c2c935361e7a..18cb470306e0 100644 --- a/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml +++ b/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml @@ -37,6 +37,9 @@ symbol = " " [haskell] symbol = " " +[haxe] +symbol = "⌘ " + [hg_branch] symbol = " " diff --git a/docs/.vuepress/public/presets/toml/no-runtime-versions.toml b/docs/.vuepress/public/presets/toml/no-runtime-versions.toml index fa617d9db555..28ffb7fcaeab 100644 --- a/docs/.vuepress/public/presets/toml/no-runtime-versions.toml +++ b/docs/.vuepress/public/presets/toml/no-runtime-versions.toml @@ -37,6 +37,9 @@ format = 'via [$symbol]($style)' [golang] format = 'via [$symbol]($style)' +[haxe] +format = 'via [$symbol]($style)' + [helm] format = 'via [$symbol]($style)' diff --git a/docs/config/README.md b/docs/config/README.md index 426980cb2bc2..52407bcc528f 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -286,6 +286,7 @@ $erlang\ $golang\ $guix_shell\ $haskell\ +$haxe\ $helm\ $java\ $julia\ @@ -1959,6 +1960,47 @@ By default the module will be shown if any of the following conditions are met: *: This variable can only be used as a part of a style string +## Haxe + +The `haxe` module shows the currently installed version of [Haxe](https://haxe.org/). +By default the module will be shown if any of the following conditions are met: + +- The current directory contains a `project.xml`, `Project.xml`, `application.xml`, `haxelib.json`, `hxformat.json` or `.haxerc` file +- The current directory contains a `.haxelib` or a `haxe_libraries` directory +- The current directory contains a file with the `.hx` or `.hxml` extension + +### Options + +| Option | Default | Description | +| ------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| `format` | `"via [$symbol($version )]($style)"` | The format for the module. | +| `version_format` | `"v${raw}"` | The version format. Available vars are `raw`, `major`, `minor`, & `patch` | +| `detect_extensions` | `["hx", "hxml"]` | Which extensions should trigger this module. | +| `detect_files` | `["project.xml", "Project.xml", "application.xml", "haxelib.json", "hxformat.json", ".haxerc"]` | Which filenames should trigger this module. | +| `detect_folders` | `[".haxelib", "haxe_libraries"]` | Which folders should trigger this modules. | +| `symbol` | `"⌘ "` | A format string representing the symbol of Helm. | +| `style` | `"bold fg:202"` | The style for the module. | +| `disabled` | `false` | Disables the `haxe` module. | + +### Variables + +| Variable | Example | Description | +| -------- | -------- | ------------------------------------ | +| version | `v4.2.5` | The version of `haxe` | +| 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 + +[haxe] +format = "via [⌘ $version](bold fg:202) " +``` + ## Helm The `helm` module shows the currently installed version of [Helm](https://helm.sh/). diff --git a/src/configs/haxe.rs b/src/configs/haxe.rs new file mode 100644 index 000000000000..70392590cbd8 --- /dev/null +++ b/src/configs/haxe.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Deserialize, Serialize)] +#[cfg_attr( + feature = "config-schema", + derive(schemars::JsonSchema), + schemars(deny_unknown_fields) +)] +#[serde(default)] +pub struct HaxeConfig<'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 HaxeConfig<'a> { + fn default() -> Self { + HaxeConfig { + format: "via [$symbol($version )]($style)", + version_format: "v${raw}", + symbol: "⌘ ", + style: "bold fg:202", + disabled: false, + detect_extensions: vec!["hx", "hxml"], + detect_files: vec!["haxelib.json", "hxformat.json", ".haxerc"], + detect_folders: vec![".haxelib", "haxe_libraries"], + } + } +} diff --git a/src/configs/mod.rs b/src/configs/mod.rs index bfb5e95ac648..a7717c3ac897 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -35,6 +35,7 @@ pub mod git_status; pub mod go; pub mod guix_shell; pub mod haskell; +pub mod haxe; pub mod helm; pub mod hg_branch; pub mod hostname; @@ -167,6 +168,8 @@ pub struct FullConfig<'a> { #[serde(borrow)] haskell: haskell::HaskellConfig<'a>, #[serde(borrow)] + haxe: haxe::HaxeConfig<'a>, + #[serde(borrow)] helm: helm::HelmConfig<'a>, #[serde(borrow)] hg_branch: hg_branch::HgBranchConfig<'a>, diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index f15a6fa5f7ee..fd67bc65c3a3 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -59,6 +59,7 @@ pub const PROMPT_ORDER: &[&str] = &[ "erlang", "golang", "haskell", + "haxe", "helm", "java", "julia", diff --git a/src/module.rs b/src/module.rs index 41d11398bfcb..f0d1a0e0daf1 100644 --- a/src/module.rs +++ b/src/module.rs @@ -43,6 +43,7 @@ pub const ALL_MODULES: &[&str] = &[ "golang", "guix_shell", "haskell", + "haxe", "helm", "hg_branch", "hostname", diff --git a/src/modules/haxe.rs b/src/modules/haxe.rs new file mode 100644 index 000000000000..de0a8133c006 --- /dev/null +++ b/src/modules/haxe.rs @@ -0,0 +1,315 @@ +use super::{Context, Module, ModuleConfig}; + +use crate::configs::haxe::HaxeConfig; +use crate::formatter::StringFormatter; +use crate::formatter::VersionFormatter; +use serde_json as json; + +use regex::Regex; +const HAXERC_VERSION_PATTERN: &str = "(?:[0-9a-zA-Z][-+0-9.a-zA-Z]+)"; + +/// Creates a module with the current Haxe version +pub fn module<'a>(context: &'a Context) -> Option> { + let mut module = context.new_module("haxe"); + let config = HaxeConfig::try_load(module.config); + + let is_haxe_project = context + .try_begin_scan()? + .set_files(&config.detect_files) + .set_extensions(&config.detect_extensions) + .set_folders(&config.detect_folders) + .is_match(); + + if !is_haxe_project { + return None; + } + + 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" => { + let haxe_version = get_haxe_version(context)?; + VersionFormatter::format_module_version( + module.get_name(), + &haxe_version, + config.version_format, + ) + .map(Ok) + } + _ => None, + }) + .parse(None, Some(context)) + }); + + module.set_segments(match parsed { + Ok(segments) => segments, + Err(error) => { + log::warn!("Error in module `haxe`:\n{}", error); + return None; + } + }); + + Some(module) +} + +fn get_haxe_version(context: &Context) -> Option { + get_haxerc_version(context).or_else(|| { + let cmd_output = context.exec_cmd("haxe", &["--version"])?; + parse_haxe_version(cmd_output.stdout.as_str()) + }) +} + +fn get_haxerc_version(context: &Context) -> Option { + let raw_json = context.read_file_from_pwd(".haxerc")?; + let package_json: json::Value = json::from_str(&raw_json).ok()?; + + let raw_version = package_json.get("version")?.as_str()?; + if raw_version.contains('/') || raw_version.contains('\\') { + return None; + } + Some(raw_version.to_string()) +} + +fn parse_haxe_version(raw_version: &str) -> Option { + let re = Regex::new(HAXERC_VERSION_PATTERN).ok()?; + if !re.is_match(raw_version) { + return None; + } + Some(raw_version.trim().to_string()) +} + +#[cfg(test)] +mod tests { + use super::parse_haxe_version; + use crate::{test::ModuleRenderer, utils::CommandOutput}; + use nu_ansi_term::Color; + use serde_json as json; + use std::fs::File; + use std::io; + use std::io::Write; + use tempfile::TempDir; + + #[test] + fn haxe_version() { + let ok_versions = [ + "4.2.5", + "4.3.0-rc.1+", + "3.4.7abcdf", + "779b005", + "beta", + "alpha", + "latest", + "/git/779b005/bin/haxe", + "git/779b005/bin/haxe", + ]; + + let all_some = ok_versions.iter().all(|&v| parse_haxe_version(v).is_some()); + + assert!(all_some); + + let sample_haxe_output = "4.3.0-rc.1+\n"; + + assert_eq!( + Some("4.3.0-rc.1+".to_string()), + parse_haxe_version(sample_haxe_output) + ) + } + + #[test] + fn folder_without_haxe() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("haxe.txt"))?.sync_all()?; + let actual = ModuleRenderer::new("haxe") + .cmd( + "haxe --version", + Some(CommandOutput { + stdout: "4.3.0-rc.1+\n".to_owned(), + stderr: "".to_owned(), + }), + ) + .path(dir.path()) + .collect(); + let expected = None; + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_with_hxml_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("build.hxml"))?.sync_all()?; + let actual = ModuleRenderer::new("haxe") + .cmd( + "haxe --version", + Some(CommandOutput { + stdout: "4.3.0-rc.1+\n".to_owned(), + stderr: "".to_owned(), + }), + ) + .path(dir.path()) + .collect(); + let expected = Some(format!( + "via {}", + Color::Fixed(202).bold().paint("⌘ v4.3.0-rc.1+ ") + )); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_with_haxe_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("Main.hx"))?.sync_all()?; + let actual = ModuleRenderer::new("haxe") + .cmd( + "haxe --version", + Some(CommandOutput { + stdout: "4.3.0-rc.1+\n".to_owned(), + stderr: "".to_owned(), + }), + ) + .path(dir.path()) + .collect(); + let expected = Some(format!( + "via {}", + Color::Fixed(202).bold().paint("⌘ v4.3.0-rc.1+ ") + )); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_with_invalid_haxerc_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let haxerc_name = ".haxerc"; + let haxerc_content = json::json!({ + "resolveLibs": "scoped" + }) + .to_string(); + fill_config(&dir, haxerc_name, Some(&haxerc_content))?; + let actual = ModuleRenderer::new("haxe") + .cmd( + "haxe --version", + Some(CommandOutput { + stdout: "4.3.0-rc.1+\n".to_owned(), + stderr: "".to_owned(), + }), + ) + .path(dir.path()) + .collect(); + let expected = Some(format!( + "via {}", + Color::Fixed(202).bold().paint("⌘ v4.3.0-rc.1+ ") + )); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_with_haxerc_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let haxerc_name = ".haxerc"; + let haxerc_content = json::json!({ + "version": "4.2.5", + "resolveLibs": "scoped" + }) + .to_string(); + fill_config(&dir, haxerc_name, Some(&haxerc_content))?; + let actual = ModuleRenderer::new("haxe") + .cmd( + "haxe --version", + Some(CommandOutput { + stdout: "4.3.0-rc.1+\n".to_owned(), + stderr: "".to_owned(), + }), + ) + .path(dir.path()) + .collect(); + let expected = Some(format!( + "via {}", + Color::Fixed(202).bold().paint("⌘ v4.2.5 ") + )); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_with_haxerc_nightly_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let haxerc_name = ".haxerc"; + let haxerc_content = json::json!({ + "version": "779b005", + "resolveLibs": "scoped" + }) + .to_string(); + + fill_config(&dir, haxerc_name, Some(&haxerc_content))?; + let actual = ModuleRenderer::new("haxe") + .cmd( + "haxe --version", + Some(CommandOutput { + stdout: "4.3.0-rc.1+\n".to_owned(), + stderr: "".to_owned(), + }), + ) + .path(dir.path()) + .collect(); + let expected = Some(format!( + "via {}", + Color::Fixed(202).bold().paint("⌘ v779b005 ") + )); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_with_haxerc_with_path() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let haxerc_name = ".haxerc"; + let haxerc_content = json::json!({ + "version": "/home/git/haxe/haxe.executable", + "resolveLibs": "scoped" + }) + .to_string(); + + fill_config(&dir, haxerc_name, Some(&haxerc_content))?; + let actual = ModuleRenderer::new("haxe") + .cmd( + "haxe --version", + Some(CommandOutput { + stdout: "4.3.0-rc.1+\n".to_owned(), + stderr: "".to_owned(), + }), + ) + .path(dir.path()) + .collect(); + let expected = Some(format!( + "via {}", + Color::Fixed(202).bold().paint("⌘ v4.3.0-rc.1+ ") + )); + assert_eq!(expected, actual); + dir.close() + } + + fn fill_config( + project_dir: &TempDir, + file_name: &str, + contents: Option<&str>, + ) -> io::Result<()> { + let mut file = File::create(project_dir.path().join(file_name))?; + file.write_all(contents.unwrap_or("").as_bytes())?; + file.sync_all() + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 7a07b73cf802..12b7f8ddcd7d 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -32,6 +32,7 @@ mod git_status; mod golang; mod guix_shell; mod haskell; +mod haxe; mod helm; mod hg_branch; mod hostname; @@ -130,6 +131,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "golang" => golang::module(context), "guix_shell" => guix_shell::module(context), "haskell" => haskell::module(context), + "haxe" => haxe::module(context), "helm" => helm::module(context), "hg_branch" => hg_branch::module(context), "hostname" => hostname::module(context), @@ -239,6 +241,7 @@ pub fn description(module: &str) -> &'static str { "golang" => "The currently installed version of Golang", "guix_shell" => "The guix-shell environment", "haskell" => "The selected version of the Haskell toolchain", + "haxe" => "The currently installed version of Haxe", "helm" => "The currently installed version of Helm", "hg_branch" => "The active branch of the repo in your current directory", "hostname" => "The system hostname",