diff --git a/.github/config-schema.json b/.github/config-schema.json index 2cb9bb5f1..404cdd6aa 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -621,6 +621,29 @@ } ] }, + "gradle": { + "default": { + "detect_extensions": [ + "gradle", + "gradle.kts" + ], + "detect_files": [], + "detect_folders": [ + "gradle" + ], + "disabled": false, + "format": "via [$symbol($version )]($style)", + "recursive": false, + "style": "bold bright-cyan", + "symbol": "🅶 ", + "version_format": "v${raw}" + }, + "allOf": [ + { + "$ref": "#/definitions/GradleConfig" + } + ] + }, "guix_shell": { "default": { "disabled": false, @@ -3189,6 +3212,62 @@ }, "additionalProperties": false }, + "GradleConfig": { + "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 bright-cyan", + "type": "string" + }, + "disabled": { + "default": false, + "type": "boolean" + }, + "recursive": { + "default": false, + "type": "boolean" + }, + "detect_extensions": { + "default": [ + "gradle", + "gradle.kts" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "detect_files": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "detect_folders": { + "default": [ + "gradle" + ], + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "GuixShellConfig": { "type": "object", "properties": { diff --git a/docs/.vuepress/public/presets/toml/bracketed-segments.toml b/docs/.vuepress/public/presets/toml/bracketed-segments.toml index f871774dd..c86ed0479 100644 --- a/docs/.vuepress/public/presets/toml/bracketed-segments.toml +++ b/docs/.vuepress/public/presets/toml/bracketed-segments.toml @@ -58,6 +58,9 @@ format = '([\[$all_status$ahead_behind\]]($style))' [golang] format = '\[[$symbol($version)]($style)\]' +[gradle] +format = '\[[$symbol($version)]($style)\]' + [guix_shell] format = '\[[$symbol]($style)\]' diff --git a/docs/.vuepress/public/presets/toml/no-runtime-versions.toml b/docs/.vuepress/public/presets/toml/no-runtime-versions.toml index 28ffb7fca..97b6a3ed7 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)' +[gradle] +format = 'via [$symbol]($style)' + [haxe] format = 'via [$symbol]($style)' diff --git a/docs/.vuepress/public/presets/toml/pastel-powerline.toml b/docs/.vuepress/public/presets/toml/pastel-powerline.toml index 6c240d063..f5cee6ecc 100644 --- a/docs/.vuepress/public/presets/toml/pastel-powerline.toml +++ b/docs/.vuepress/public/presets/toml/pastel-powerline.toml @@ -12,6 +12,7 @@ $c\ $elixir\ $elm\ $golang\ +$gradle\ $haskell\ $java\ $julia\ @@ -97,6 +98,10 @@ symbol = " " style = "bg:#86BBD8" format = '[ $symbol ($version) ]($style)' +[gradle] +style = "bg:#86BBD8" +format = '[ $symbol ($version) ]($style)' + [haskell] symbol = " " style = "bg:#86BBD8" diff --git a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml index bf6591980..1efccd243 100644 --- a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml +++ b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml @@ -64,6 +64,9 @@ symbol = "git " [golang] symbol = "go " +[gradle] +symbol = "gradle " + [guix_shell] symbol = "guix " diff --git a/docs/config/README.md b/docs/config/README.md index 0d4d13ac8..4d1a7161f 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -291,6 +291,7 @@ $helm\ $java\ $julia\ $kotlin\ +$gradle\ $lua\ $nim\ $nodejs\ @@ -1927,6 +1928,42 @@ disabled = true format = 'via [🐂](yellow bold) ' ``` +## Gradle + +The `gradle` module shows the version of the [Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) +currently used in the project directory. + +By default the module will be shown if any of the following conditions are met: + +- The current directory contains a `gradle/wrapper/gradle-wrapper.properties` directory. +- The current directory contains a file ending with `.gradle` or `.gradle.kts`. + +The `gradle` module is only able to read your Gradle Wrapper version from your config file, we don't execute your wrapper, because of the security concerns. + +### 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` | +| `symbol` | `"🅶 "` | A format string representing the symbol of Gradle. | +| `detect_extensions` | `["gradle", "gradle.kts"]` | Which extensions should trigger this module. | +| `detect_files` | `[]` | Which filenames should trigger this module. | +| `detect_folders` | `["gradle"]` | Which folders should trigger this module. | +| `style` | `"bold bright-cyan"` | The style for the module. | +| `disabled` | `false` | Disables the `gradle` module. | +| `recursive` | `false` | Enables recursive finding for the `gradle` directory. | + +### Variables + +| Variable | Example | Description | +| -------- | -------- | ------------------------------------ | +| version | `v7.5.1` | The version of `gradle` | +| 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 + ## Haskell The `haskell` module finds the current selected GHC version and/or the selected Stack snapshot. diff --git a/src/configs/gradle.rs b/src/configs/gradle.rs new file mode 100644 index 000000000..5026f83e5 --- /dev/null +++ b/src/configs/gradle.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Deserialize, Serialize)] +#[cfg_attr( + feature = "config-schema", + derive(schemars::JsonSchema), + schemars(deny_unknown_fields) +)] +#[serde(default)] +pub struct GradleConfig<'a> { + pub format: &'a str, + pub version_format: &'a str, + pub symbol: &'a str, + pub style: &'a str, + pub disabled: bool, + pub recursive: bool, + pub detect_extensions: Vec<&'a str>, + pub detect_files: Vec<&'a str>, + pub detect_folders: Vec<&'a str>, +} + +impl<'a> Default for GradleConfig<'a> { + fn default() -> Self { + GradleConfig { + format: "via [$symbol($version )]($style)", + version_format: "v${raw}", + symbol: "🅶 ", + style: "bold bright-cyan", + disabled: false, + recursive: false, + detect_extensions: vec!["gradle", "gradle.kts"], + detect_files: vec![], + detect_folders: vec!["gradle"], + } + } +} diff --git a/src/configs/mod.rs b/src/configs/mod.rs index a7717c3ac..88d1d8d97 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -33,6 +33,7 @@ pub mod git_metrics; pub mod git_state; pub mod git_status; pub mod go; +pub mod gradle; pub mod guix_shell; pub mod haskell; pub mod haxe; @@ -164,6 +165,8 @@ pub struct FullConfig<'a> { #[serde(borrow)] golang: go::GoConfig<'a>, #[serde(borrow)] + gradle: gradle::GradleConfig<'a>, + #[serde(borrow)] guix_shell: guix_shell::GuixShellConfig<'a>, #[serde(borrow)] haskell: haskell::HaskellConfig<'a>, diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index fd67bc65c..3fdd06f68 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -58,6 +58,7 @@ pub const PROMPT_ORDER: &[&str] = &[ "elm", "erlang", "golang", + "gradle", "haskell", "haxe", "helm", diff --git a/src/module.rs b/src/module.rs index f0d1a0e0d..64518afd6 100644 --- a/src/module.rs +++ b/src/module.rs @@ -41,6 +41,7 @@ pub const ALL_MODULES: &[&str] = &[ "git_state", "git_status", "golang", + "gradle", "guix_shell", "haskell", "haxe", diff --git a/src/modules/gradle.rs b/src/modules/gradle.rs new file mode 100644 index 000000000..e19c9a4c9 --- /dev/null +++ b/src/modules/gradle.rs @@ -0,0 +1,220 @@ +use crate::{ + config::ModuleConfig, + configs::gradle::GradleConfig, + context::Context, + formatter::{StringFormatter, VersionFormatter}, + module::Module, + utils, +}; + +pub fn module<'a>(context: &'a Context) -> Option> { + let mut module = context.new_module("gradle"); + let config = GradleConfig::try_load(module.config); + let is_gradle_project = context + .try_begin_scan()? + .set_files(&config.detect_files) + .set_extensions(&config.detect_extensions) + .set_folders(&config.detect_folders) + .is_match(); + + if !is_gradle_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 gradle_version = { + let properties = get_wrapper_properties_file(context, config.recursive)?; + parse_gradle_version_from_properties(&properties)? + }; + VersionFormatter::format_module_version( + module.get_name(), + &gradle_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 `gradle`:\n{}", error); + return None; + } + }); + + Some(module) +} + +fn parse_gradle_version_from_properties(wrapper_properties: &str) -> Option { + // example gradle.properties content + /* + distributionBase=GRADLE_USER_HOME + distributionPath=wrapper/dists + distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip + zipStoreBase=GRADLE_USER_HOME + zipStorePath=wrapper/dists + */ + let version = wrapper_properties + .lines() + .find(|line| line.starts_with("distributionUrl="))? + .rsplit_once('/')? + .1 + .strip_prefix("gradle-")? + .split_once('-')? + .0; + Some(version.to_string()) +} + +/// Tries to find the gradle-wrapper.properties file. +fn get_wrapper_properties_file(context: &Context, recursive: bool) -> Option { + let mut properties = None; + if context + .try_begin_scan()? + .set_folders(&["gradle"]) + .is_match() + { + properties = utils::read_file( + &context + .current_dir + .join("gradle/wrapper/gradle-wrapper.properties"), + ) + .ok(); + }; + if recursive && properties.is_none() { + for dir in context.current_dir.ancestors().skip(1) { + properties = + utils::read_file(&dir.join("gradle/wrapper/gradle-wrapper.properties")).ok(); + if properties.is_some() { + break; + } + } + } + properties +} + +#[cfg(test)] +mod tests { + use nu_ansi_term::Color; + + use super::*; + use crate::test::ModuleRenderer; + use std::fs::{self, File}; + use std::io::{self, Write}; + + #[test] + fn folder_without_gradle_files() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let actual = ModuleRenderer::new("gradle").path(dir.path()).collect(); + + let expected = None; + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_with_gradle_wrapper_properties() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let properties = dir + .path() + .join("gradle") + .join("wrapper") + .join("gradle-wrapper.properties"); + // create gradle/wrapper/ directories + fs::create_dir_all(properties.parent().unwrap())?; + // create build.gradle file to mark it as a gradle project + File::create(dir.path().join("build.gradle"))?.sync_all()?; + let mut file = File::create(properties)?; + file.write_all( + b"\ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists", + )?; + file.sync_all()?; + + let actual = ModuleRenderer::new("gradle").path(dir.path()).collect(); + + let expected = Some(format!( + "via {}", + Color::LightCyan.bold().paint("🅶 v7.5.1 ") + )); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn gradle_wrapper_recursive() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let properties = dir + .path() + .join("gradle") + .join("wrapper") + .join("gradle-wrapper.properties"); + // create gradle/wrapper/ directories + fs::create_dir_all(properties.parent().unwrap())?; + // create build.gradle file to mark it as a gradle project + File::create(dir.path().join("build.gradle"))?.sync_all()?; + let mut file = File::create(properties)?; + file.write_all( + b"\ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists", + )?; + file.sync_all()?; + + let target_dir = dir.path().join("working_dir"); + fs::create_dir(&target_dir)?; + File::create(target_dir.join("build.gradle.kts"))?.sync_all()?; + + let actual = ModuleRenderer::new("gradle") + .config(toml::toml! { + [gradle] + recursive = true + }) + .path(target_dir) + .collect(); + + let expected = Some(format!( + "via {}", + Color::LightCyan.bold().paint("🅶 v7.5.1 ") + )); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn test_format_wrapper_properties() { + let input = "\ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists + "; + assert_eq!( + parse_gradle_version_from_properties(input), + Some("7.5.1".to_string()) + ); + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 12b7f8ddc..8e463aa55 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -30,6 +30,7 @@ mod git_metrics; mod git_state; mod git_status; mod golang; +mod gradle; mod guix_shell; mod haskell; mod haxe; @@ -129,6 +130,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "git_state" => git_state::module(context), "git_status" => git_status::module(context), "golang" => golang::module(context), + "gradle" => gradle::module(context), "guix_shell" => guix_shell::module(context), "haskell" => haskell::module(context), "haxe" => haxe::module(context), @@ -239,6 +241,7 @@ pub fn description(module: &str) -> &'static str { "git_state" => "The current git operation, and it's progress", "git_status" => "Symbol representing the state of the repo", "golang" => "The currently installed version of Golang", + "gradle" => "The currently installed version of Gradle", "guix_shell" => "The guix-shell environment", "haskell" => "The selected version of the Haskell toolchain", "haxe" => "The currently installed version of Haxe",