diff --git a/docs/.vuepress/public/presets/toml/bracketed-segments.toml b/docs/.vuepress/public/presets/toml/bracketed-segments.toml index 726b3ddf..2d41c369 100644 --- a/docs/.vuepress/public/presets/toml/bracketed-segments.toml +++ b/docs/.vuepress/public/presets/toml/bracketed-segments.toml @@ -49,6 +49,9 @@ format = '([\[$all_status$ahead_behind\]]($style))' [golang] format = '\[[$symbol($version)]($style)\]' +[haskell] +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 bdd64540..c02b354c 100644 --- a/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml +++ b/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml @@ -28,6 +28,9 @@ symbol = " " [golang] symbol = " " +[haskell] +symbol = " " + [hg_branch] symbol = " " diff --git a/docs/config/README.md b/docs/config/README.md index 727a3198..911284b1 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -217,6 +217,7 @@ $elixir\ $elm\ $erlang\ $golang\ +$haskell\ $helm\ $java\ $julia\ @@ -1672,6 +1673,39 @@ By default the module will be shown if any of the following conditions are met: format = "via [🏎💨 $version](bold cyan) " ``` +## Haskell + +The `haskell` module finds the current selected GHC version and/or the selected Stack snapshot. + +By default the module will be shown if any of the following conditions are met: + +- The current directory contains a `stack.yaml` file +- The current directory contains any `.hs`, `.cabal`, or `.hs-boot` file + +### Options + +| Option | Default | Description | +| ------------------- | ------------------------------------ | -------------------------------------------------- | +| `format` | `"via [$symbol($version )]($style)"` | The format for the module. | +| `symbol` | `"λ "` | A format string representing the symbol of Haskell | +| `detect_extensions` | `["hs", "cabal", "hs-boot"]` | Which extensions should trigger this module. | +| `detect_files` | `["stack.yaml", "cabal.project"]` | Which filenames should trigger this module. | +| `detect_folders` | `[]` | Which folders should trigger this module. | +| `style` | `"bold purple"` | The style for the module. | +| `disabled` | `false` | Disables the `haskell` module. | + +### Variables + +| Variable | Example | Description | +| ------------ | ----------- | --------------------------------------------------------------------------------------- | +| version | | `ghc_version` or `snapshot` depending on whether the current project is a Stack project | +| snapshot | `lts-18.12` | Currently selected Stack snapshot | +| ghc\_version | `9.2.1` | Currently installed GHC version | +| 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 + ## Helm The `helm` module shows the currently installed version of [Helm](https://helm.sh/). diff --git a/src/configs/haskell.rs b/src/configs/haskell.rs new file mode 100644 index 00000000..40319461 --- /dev/null +++ b/src/configs/haskell.rs @@ -0,0 +1,31 @@ +use crate::config::ModuleConfig; + +use serde::Serialize; +use starship_module_config_derive::ModuleConfig; + +#[derive(Clone, ModuleConfig, Serialize)] +pub struct HaskellConfig<'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 HaskellConfig<'a> { + fn default() -> Self { + HaskellConfig { + format: "via [$symbol($version )]($style)", + version_format: "v${raw}", + symbol: "λ ", + style: "bold purple", + disabled: false, + detect_extensions: vec!["hs", "cabal", "hs-boot"], + detect_files: vec!["stack.yaml", "cabal.project"], + detect_folders: vec![], + } + } +} diff --git a/src/configs/mod.rs b/src/configs/mod.rs index a510cf82..d945af85 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -32,6 +32,7 @@ pub mod git_metrics; pub mod git_state; pub mod git_status; pub mod go; +pub mod haskell; pub mod helm; pub mod hg_branch; pub mod hostname; @@ -116,6 +117,7 @@ pub struct FullConfig<'a> { git_state: git_state::GitStateConfig<'a>, git_status: git_status::GitStatusConfig<'a>, golang: go::GoConfig<'a>, + haskell: haskell::HaskellConfig<'a>, helm: helm::HelmConfig<'a>, hg_branch: hg_branch::HgBranchConfig<'a>, hostname: hostname::HostnameConfig<'a>, @@ -198,6 +200,7 @@ impl<'a> Default for FullConfig<'a> { git_state: Default::default(), git_status: Default::default(), golang: Default::default(), + haskell: Default::default(), helm: Default::default(), hg_branch: Default::default(), hostname: Default::default(), diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index 0a519b0f..da2c1646 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -45,6 +45,7 @@ pub const PROMPT_ORDER: &[&str] = &[ "elm", "erlang", "golang", + "haskell", "helm", "java", "julia", diff --git a/src/module.rs b/src/module.rs index a385243f..0bd91981 100644 --- a/src/module.rs +++ b/src/module.rs @@ -37,6 +37,7 @@ pub const ALL_MODULES: &[&str] = &[ "git_state", "git_status", "golang", + "haskell", "helm", "hg_branch", "hostname", diff --git a/src/modules/haskell.rs b/src/modules/haskell.rs new file mode 100644 index 00000000..93ae25ca --- /dev/null +++ b/src/modules/haskell.rs @@ -0,0 +1,174 @@ +use super::{Context, Module, RootModuleConfig}; + +use crate::configs::haskell::HaskellConfig; +use crate::formatter::StringFormatter; +use crate::utils; + +/// Creates a module with the current Haskell version +pub fn module<'a>(context: &'a Context) -> Option> { + let mut module = context.new_module("haskell"); + let config = HaskellConfig::try_load(module.config); + + let is_hs_project = context + .try_begin_scan()? + .set_files(&config.detect_files) + .set_extensions(&config.detect_extensions) + .set_folders(&config.detect_folders) + .is_match(); + + if !is_hs_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" => get_version(context).map(Ok), + "ghc_version" => get_ghc_version(context).map(Ok), + "snapshot" => get_snapshot(context).map(Ok), + _ => None, + }) + .parse(None, Some(context)) + }); + + module.set_segments(match parsed { + Ok(segments) => segments, + Err(error) => { + log::warn!("Error in module `haskell`:\n{}", error); + return None; + } + }); + + Some(module) +} + +fn get_ghc_version(context: &Context) -> Option { + Some( + context + .exec_cmd("ghc", &["--numeric-version"])? + .stdout + .trim() + .to_string(), + ) +} + +fn get_snapshot(context: &Context) -> Option { + if !is_stack_project(context) { + return None; + } + let file_contents = utils::read_file(context.current_dir.join("stack.yaml")).ok()?; + let yaml = yaml_rust::YamlLoader::load_from_str(&file_contents).ok()?; + let version = yaml.first()?["resolver"] + .as_str() + .or_else(|| yaml.first()?["snapshot"].as_str()) + .filter(|s| s.starts_with("lts") || s.starts_with("nightly") || s.starts_with("ghc")) + .unwrap_or(""); + Some(version.to_string()) +} + +fn get_version(context: &Context) -> Option { + get_snapshot(context).or_else(|| get_ghc_version(context)) +} + +fn is_stack_project(context: &Context) -> bool { + match context.dir_contents() { + Ok(dir) => dir.has_file_name("stack.yaml"), + Err(_) => false, + } +} + +#[cfg(test)] +mod tests { + use crate::test::ModuleRenderer; + use ansi_term::Color; + use std::fs::File; + use std::io; + use std::io::Write; + + #[test] + fn folder_without_hs_files() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let actual = ModuleRenderer::new("haskell").path(dir.path()).collect(); + let expected = None; + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_stack() -> io::Result<()> { + let cases = vec![ + ("resolver: lts-18.12\n", "lts-18.12"), + ("snapshot:\tnightly-2011-11-11", "nightly-2011-11-11"), + ("snapshot: ghc-8.10.7", "ghc-8.10.7"), + ( + "snapshot: https://github.com/whatever/xxx.yaml\n", + "", + ), + ( + "resolver:\n url: https://github.com/whatever/xxx.yaml\n", + "", + ), + (REANIMATE_STACK_YAML, "lts-14.27"), + ]; + for (yaml, resolver) in &cases { + let dir = tempfile::tempdir()?; + let mut file = File::create(dir.path().join("stack.yaml"))?; + file.write_all(yaml.as_bytes())?; + file.sync_all()?; + let actual = ModuleRenderer::new("haskell").path(dir.path()).collect(); + let expected = Some(format!( + "via {}", + Color::Purple.bold().paint(format!("λ {} ", resolver)) + )); + assert_eq!(expected, actual); + dir.close()?; + } + Ok(()) + } + + #[test] + fn folder_cabal() -> io::Result<()> { + let should_trigger = vec!["a.hs", "b.hs-boot", "cabal.project"]; + for hs_file in &should_trigger { + let dir = tempfile::tempdir()?; + File::create(dir.path().join(hs_file))?.sync_all()?; + let actual = ModuleRenderer::new("haskell").path(dir.path()).collect(); + let expected = Some(format!("via {}", Color::Purple.bold().paint("λ 9.2.1 "))); + assert_eq!(expected, actual); + dir.close()?; + } + Ok(()) + } + + static REANIMATE_STACK_YAML: &str = r" +resolver: lts-14.27 + +allow-newer: false + +packages: +- . + +extra-deps: +- reanimate-svg-0.13.0.1 +- chiphunk-0.1.2.1 +- cubicbezier-0.6.0.6@sha256:2191ff47144d9a13a2784651a33d340cd31be1926a6c188925143103eb3c8db3 +- fast-math-1.0.2@sha256:91181eb836e54413cc5a841e797c42b2264954e893ea530b6fc4da0dccf6a8b7 +- matrices-0.5.0@sha256:b2761813f6a61c84224559619cc60a16a858ac671c8436bbac8ec89e85473058 +- hmatrix-0.20.0.0@sha256:d79a9218e314f1a2344457c3851bd1d2536518ecb5f1a2fcd81daa45e46cd025,4870 +- earcut-0.1.0.4@sha256:d5118b3eecf24d130263d81fb30f1ff56b1db43036582bfd1d8cc9ba3adae8be,1010 +- tasty-rerun-1.1.17@sha256:d4a3ccb0f63f499f36edc71b33c0f91c850eddb22dd92b928aa33b8459f3734a,1373 +- hgeometry-0.11.0.0@sha256:09ead201a6ac3492c0be8dda5a6b32792b9ae87cab730b8362d46ee8d5c2acb4,11714 +- hgeometry-combinatorial-0.11.0.0@sha256:03176f235a1c49a415fe1266274dafca84deb917cbcbf9a654452686b4cd2bfe,8286 +- vinyl-0.13.0@sha256:0f247cd3f8682b30881a07de18e6fec52d540646fbcb328420049cc8d63cd407,3724 +- hashable-1.3.0.0@sha256:4c70f1407881059e93550d3742191254296b2737b793a742bd901348fb3e1fb1,5206 +- network-3.1.2.1@sha256:188d6daea8cd91bc3553efd5a90a1e7c6d0425fa66a53baa74db5b6d9fd75c8b,4968 +"; +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 6a392a73..4bdcc234 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -27,6 +27,7 @@ mod git_metrics; mod git_state; mod git_status; mod golang; +mod haskell; mod helm; mod hg_branch; mod hostname; @@ -115,6 +116,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), + "haskell" => haskell::module(context), "helm" => helm::module(context), "hg_branch" => hg_branch::module(context), "hostname" => hostname::module(context), @@ -208,6 +210,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", + "haskell" => "The selected version of the Haskell toolchain", "helm" => "The currently installed version of Helm", "hg_branch" => "The active branch of the repo in your current directory", "hostname" => "The system hostname", diff --git a/src/utils.rs b/src/utils.rs index 9f55f0c0..db78172c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -187,6 +187,10 @@ Elixir 1.10 (compiled with Erlang/OTP 22)\n", stdout: String::from("go version go1.12.1 linux/amd64\n"), stderr: String::default(), }), + "ghc --numeric-version" => Some(CommandOutput { + stdout: String::from("9.2.1\n"), + stderr: String::default(), + }), "helm version --short --client" => Some(CommandOutput { stdout: String::from("v3.1.1+gafe7058\n"), stderr: String::default(),