diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index c4900c6b..ac281d0b 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -133,11 +133,6 @@ jobs: ARGS: --resolver nightly-2019-09-21 run: stack $ARGS ghc -- --numeric-version --no-install-ghc - # Install Node.js at a fixed version - - uses: actions/setup-node@v1 - with: - node-version: "12.0.0" - # Install Golang at a fixed version - uses: actions/setup-go@v1 with: diff --git a/src/context.rs b/src/context.rs index ead8e1e7..460ede15 100644 --- a/src/context.rs +++ b/src/context.rs @@ -31,6 +31,9 @@ pub struct Context<'a> { /// Private field to store Git information for modules who need it repo: OnceCell, + + /// The shell the user is assumed to be running + pub shell: Shell, } impl<'a> Context<'a> { @@ -71,12 +74,15 @@ impl<'a> Context<'a> { // TODO: Currently gets the physical directory. Get the logical directory. let current_dir = Context::expand_tilde(dir.into()); + let shell = Context::get_shell(); + Context { config, properties, current_dir, dir_files: OnceCell::new(), repo: OnceCell::new(), + shell, } } @@ -160,6 +166,18 @@ impl<'a> Context<'a> { Ok(dir_files) }) } + + fn get_shell() -> Shell { + let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default(); + match shell.as_str() { + "bash" => Shell::Bash, + "fish" => Shell::Fish, + "ion" => Shell::Ion, + "powershell" => Shell::PowerShell, + "zsh" => Shell::Zsh, + _ => Shell::Unknown, + } + } } pub struct Repo { @@ -252,6 +270,16 @@ fn get_current_branch(repository: &Repository) -> Option { shorthand.map(std::string::ToString::to_string) } +#[derive(Debug, Clone)] +pub enum Shell { + Bash, + Fish, + Ion, + PowerShell, + Zsh, + Unknown, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/module.rs b/src/module.rs index 34f3a652..f09c6e5d 100644 --- a/src/module.rs +++ b/src/module.rs @@ -1,4 +1,5 @@ use crate::config::SegmentConfig; +use crate::context::Shell; use crate::segment::Segment; use ansi_term::Style; use ansi_term::{ANSIString, ANSIStrings}; @@ -134,10 +135,10 @@ impl<'a> Module<'a> { /// Returns a vector of colored ANSIString elements to be later used with /// `ANSIStrings()` to optimize ANSI codes pub fn ansi_strings(&self) -> Vec { - self.ansi_strings_for_prompt(true) + self.ansi_strings_for_shell(Shell::Unknown) } - pub fn ansi_strings_for_prompt(&self, is_prompt: bool) -> Vec { + pub fn ansi_strings_for_shell(&self, shell: Shell) -> Vec { let mut ansi_strings = self .segments .iter() @@ -147,20 +148,17 @@ impl<'a> Module<'a> { ansi_strings.insert(0, self.prefix.ansi_string()); ansi_strings.push(self.suffix.ansi_string()); - if is_prompt { - let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default(); - ansi_strings = match shell.as_str() { - "bash" => ansi_strings_modified(ansi_strings, shell), - "zsh" => ansi_strings_modified(ansi_strings, shell), - _ => ansi_strings, - }; - } + ansi_strings = match shell { + Shell::Bash => ansi_strings_modified(ansi_strings, shell), + Shell::Zsh => ansi_strings_modified(ansi_strings, shell), + _ => ansi_strings, + }; ansi_strings } - pub fn to_string_without_prefix(&self) -> String { - ANSIStrings(&self.ansi_strings()[1..]).to_string() + pub fn to_string_without_prefix(&self, shell: Shell) -> String { + ANSIStrings(&self.ansi_strings_for_shell(shell)[1..]).to_string() } } @@ -174,7 +172,7 @@ impl<'a> fmt::Display for Module<'a> { /// Many shells cannot deal with raw unprintable characters (like ANSI escape sequences) and /// miscompute the cursor position as a result, leading to strange visual bugs. Here, we wrap these /// characters in shell-specific escape codes to indicate to the shell that they are zero-length. -fn ansi_strings_modified(ansi_strings: Vec, shell: String) -> Vec { +fn ansi_strings_modified(ansi_strings: Vec, shell: Shell) -> Vec { const ESCAPE_BEGIN: char = '\u{1b}'; const MAYBE_ESCAPE_END: char = 'm'; ansi_strings @@ -187,18 +185,18 @@ fn ansi_strings_modified(ansi_strings: Vec, shell: String) -> Vec { escaped = true; - match shell.as_str() { - "bash" => String::from("\u{5c}\u{5b}\u{1b}"), // => \[ESC - "zsh" => String::from("\u{25}\u{7b}\u{1b}"), // => %{ESC + match shell { + Shell::Bash => String::from("\u{5c}\u{5b}\u{1b}"), // => \[ESC + Shell::Zsh => String::from("\u{25}\u{7b}\u{1b}"), // => %{ESC _ => x.to_string(), } } MAYBE_ESCAPE_END => { if escaped { escaped = false; - match shell.as_str() { - "bash" => String::from("m\u{5c}\u{5d}"), // => m\] - "zsh" => String::from("m\u{25}\u{7d}"), // => m%} + match shell { + Shell::Bash => String::from("m\u{5c}\u{5d}"), // => m\] + Shell::Zsh => String::from("m\u{25}\u{7d}"), // => m%} _ => x.to_string(), } } else { diff --git a/src/modules/battery.rs b/src/modules/battery.rs index 8c651a95..55e248e5 100644 --- a/src/modules/battery.rs +++ b/src/modules/battery.rs @@ -1,13 +1,12 @@ -use super::{Context, Module, RootModuleConfig}; +use super::{Context, Module, RootModuleConfig, Shell}; use crate::configs::battery::BatteryConfig; /// Creates a module for the battery percentage and charging state pub fn module<'a>(context: &'a Context) -> Option> { // TODO: Update when v1.0 printing refactor is implemented to only // print escapes in a prompt context. - let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default(); - let percentage_char = match shell.as_str() { - "zsh" => "%%", // % is an escape in zsh, see PROMPT in `man zshmisc` + let percentage_char = match context.shell { + Shell::Zsh => "%%", // % is an escape in zsh, see PROMPT in `man zshmisc` _ => "%", }; diff --git a/src/modules/character.rs b/src/modules/character.rs index cc954517..e0c0ffe9 100644 --- a/src/modules/character.rs +++ b/src/modules/character.rs @@ -1,5 +1,4 @@ -use super::{Context, Module, RootModuleConfig}; - +use super::{Context, Module, RootModuleConfig, Shell}; use crate::configs::character::CharacterConfig; /// Creates a module for the prompt character @@ -25,7 +24,6 @@ pub fn module<'a>(context: &'a Context) -> Option> { let props = &context.properties; let exit_code_default = std::string::String::from("0"); let exit_code = props.get("status_code").unwrap_or(&exit_code_default); - let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default(); let keymap_default = std::string::String::from("viins"); let keymap = props.get("keymap").unwrap_or(&keymap_default); let exit_success = exit_code == "0"; @@ -35,8 +33,8 @@ pub fn module<'a>(context: &'a Context) -> Option> { // Unfortunately, this is also the name of the non-vi default mode. // We do some environment detection in src/init.rs to translate. // The result: in non-vi fish, keymap is always reported as "insert" - let mode = match (shell.as_str(), keymap.as_str()) { - ("fish", "default") | ("zsh", "vicmd") => ShellEditMode::Normal, + let mode = match (&context.shell, keymap.as_str()) { + (Shell::Fish, "default") | (Shell::Zsh, "vicmd") => ShellEditMode::Normal, _ => ASSUMED_MODE, }; diff --git a/src/modules/memory_usage.rs b/src/modules/memory_usage.rs index 5a160ee4..c7d22433 100644 --- a/src/modules/memory_usage.rs +++ b/src/modules/memory_usage.rs @@ -1,7 +1,7 @@ use byte_unit::{Byte, ByteUnit}; use sysinfo::{RefreshKind, SystemExt}; -use super::{Context, Module, RootModuleConfig}; +use super::{Context, Module, RootModuleConfig, Shell}; use crate::configs::memory_usage::MemoryConfig; @@ -19,9 +19,8 @@ pub fn module<'a>(context: &'a Context) -> Option> { // TODO: Update when v1.0 printing refactor is implemented to only // print escapes in a prompt context. - let shell = std::env::var("STARSHIP_SHELL").unwrap_or_default(); - let percent_sign = match shell.as_str() { - "zsh" => "%%", // % is an escape in zsh, see PROMPT in `man zshmisc` + let percent_sign = match context.shell { + Shell::Zsh => "%%", // % is an escape in zsh, see PROMPT in `man zshmisc` _ => "%", }; diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 9c9f403c..74b11b94 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -35,7 +35,7 @@ mod utils; mod battery; use crate::config::{RootModuleConfig, SegmentConfig}; -use crate::context::Context; +use crate::context::{Context, Shell}; use crate::module::Module; pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { diff --git a/src/modules/nodejs.rs b/src/modules/nodejs.rs index 5bd31435..7fec383f 100644 --- a/src/modules/nodejs.rs +++ b/src/modules/nodejs.rs @@ -34,3 +34,55 @@ pub fn module<'a>(context: &'a Context) -> Option> { Some(module) } + +#[cfg(test)] +mod tests { + use crate::modules::utils::test::render_module; + use ansi_term::Color; + use std::fs::{self, File}; + use std::io; + use tempfile; + + #[test] + fn folder_without_node_files() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let actual = render_module("nodejs", dir.path()); + let expected = None; + assert_eq!(expected, actual); + Ok(()) + } + + #[test] + fn folder_with_package_json() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("package.json"))?.sync_all()?; + + let actual = render_module("nodejs", dir.path()); + let expected = Some(format!("via {} ", Color::Green.bold().paint("⬢ v12.0.0"))); + assert_eq!(expected, actual); + Ok(()) + } + + #[test] + fn folder_with_js_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("index.js"))?.sync_all()?; + + let actual = render_module("nodejs", dir.path()); + let expected = Some(format!("via {} ", Color::Green.bold().paint("⬢ v12.0.0"))); + assert_eq!(expected, actual); + Ok(()) + } + + #[test] + fn folder_with_node_modules() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let node_modules = dir.path().join("node_modules"); + fs::create_dir_all(&node_modules)?; + + let actual = render_module("nodejs", dir.path()); + let expected = Some(format!("via {} ", Color::Green.bold().paint("⬢ v12.0.0"))); + assert_eq!(expected, actual); + Ok(()) + } +} diff --git a/src/modules/utils/mod.rs b/src/modules/utils/mod.rs index 6838fb19..345ce7f4 100644 --- a/src/modules/utils/mod.rs +++ b/src/modules/utils/mod.rs @@ -1,2 +1,5 @@ pub mod directory; pub mod java_version_parser; + +#[cfg(test)] +pub mod test; diff --git a/src/modules/utils/test.rs b/src/modules/utils/test.rs new file mode 100644 index 00000000..b3c106b3 --- /dev/null +++ b/src/modules/utils/test.rs @@ -0,0 +1,12 @@ +use crate::config::StarshipConfig; +use crate::context::{Context, Shell}; +use std::path::Path; + +/// Render a specific starship module by name +pub fn render_module(module_name: &str, path: &Path) -> Option { + let mut context = Context::new_with_dir(clap::ArgMatches::default(), path); + context.config = StarshipConfig { config: None }; + context.shell = Shell::Unknown; + + crate::print::get_module(module_name, context) +} diff --git a/src/print.rs b/src/print.rs index ee3b3666..46d44d22 100644 --- a/src/print.rs +++ b/src/print.rs @@ -1,3 +1,4 @@ +use ansi_term::ANSIStrings; use clap::ArgMatches; use rayon::prelude::*; use std::fmt::Write as FmtWrite; @@ -35,10 +36,11 @@ pub fn get_prompt(context: Context) -> String { for module in printable { // Skip printing the prefix of a module after the line_break if print_without_prefix { - let module_without_prefix = module.to_string_without_prefix(); + let module_without_prefix = module.to_string_without_prefix(context.shell.clone()); write!(buf, "{}", module_without_prefix).unwrap() } else { - write!(buf, "{}", module).unwrap(); + let module = module.ansi_strings_for_shell(context.shell.clone()); + write!(buf, "{}", ANSIStrings(&module)).unwrap(); } print_without_prefix = module.get_name() == "line_break" @@ -49,15 +51,14 @@ pub fn get_prompt(context: Context) -> String { pub fn module(module_name: &str, args: ArgMatches) { let context = Context::new(args); - - // If the module returns `None`, print an empty string - let module = modules::handle(module_name, &context) - .map(|m| m.to_string()) - .unwrap_or_default(); - + let module = get_module(module_name, context).unwrap_or_default(); print!("{}", module); } +pub fn get_module(module_name: &str, context: Context) -> Option { + modules::handle(module_name, &context).map(|m| m.to_string()) +} + pub fn explain(args: ArgMatches) { let context = Context::new(args); @@ -73,7 +74,7 @@ pub fn explain(args: ArgMatches) { .into_iter() .filter(|module| !dont_print.contains(&module.get_name().as_str())) .map(|module| { - let ansi_strings = module.ansi_strings_for_prompt(false); + let ansi_strings = module.ansi_strings(); let value = module.get_segments().join(""); ModuleInfo { value: ansi_term::ANSIStrings(&ansi_strings[1..ansi_strings.len() - 1]).to_string(), diff --git a/src/utils.rs b/src/utils.rs index e4a4e7d3..758aeded 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -36,8 +36,11 @@ pub fn exec_cmd(cmd: &str, args: &[&str]) -> Option { 0 => String::from(cmd), _ => format!("{} {}", cmd, args.join(" ")), }; - match command.as_str() { + "node --version" => Some(CommandOutput { + stdout: String::from("v12.0.0"), + stderr: String::default(), + }), "dummy_command" => Some(CommandOutput { stdout: String::from("stdout ok!"), stderr: String::from("stderr ok!"), diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index b7cce943..f12b6078 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -19,7 +19,6 @@ mod jobs; mod line_break; mod modules; mod nix_shell; -mod nodejs; mod python; mod ruby; mod terraform; diff --git a/tests/testsuite/nodejs.rs b/tests/testsuite/nodejs.rs deleted file mode 100644 index c0df62a7..00000000 --- a/tests/testsuite/nodejs.rs +++ /dev/null @@ -1,93 +0,0 @@ -use ansi_term::Color; -use std::fs::{self, File}; -use std::io; -use tempfile; - -use crate::common; - -/// Wrapper around common::render_module("nodejs") to work around platform quirks -fn render_node_module() -> std::process::Command { - let mut command = common::render_module("nodejs"); - - // If SYSTEMROOT is not set on Windows node will refuse to print its version - if cfg!(windows) { - let system_root = std::env::var("SYSTEMROOT") - .map(|i| { - if i.trim().is_empty() { - "C:\\WINDOWS".into() - } else { - i - } - }) - .unwrap_or_else(|_| "C:\\WINDOWS".into()); - command.env("SYSTEMROOT", system_root); - } - command -} - -#[test] -fn folder_without_node_files() -> io::Result<()> { - let dir = tempfile::tempdir()?; - - let output = render_node_module() - .arg("--path") - .arg(dir.path()) - .output()?; - let actual = String::from_utf8(output.stdout).unwrap(); - - let expected = ""; - assert_eq!(expected, actual); - Ok(()) -} - -#[test] -#[ignore] -fn folder_with_package_json() -> io::Result<()> { - let dir = tempfile::tempdir()?; - File::create(dir.path().join("package.json"))?.sync_all()?; - - let output = render_node_module() - .arg("--path") - .arg(dir.path()) - .output()?; - let actual = String::from_utf8(output.stdout).unwrap(); - - let expected = format!("via {} ", Color::Green.bold().paint("⬢ v12.0.0")); - assert_eq!(expected, actual); - Ok(()) -} - -#[test] -#[ignore] -fn folder_with_js_file() -> io::Result<()> { - let dir = tempfile::tempdir()?; - File::create(dir.path().join("index.js"))?.sync_all()?; - - let output = render_node_module() - .arg("--path") - .arg(dir.path()) - .output()?; - let actual = String::from_utf8(output.stdout).unwrap(); - - let expected = format!("via {} ", Color::Green.bold().paint("⬢ v12.0.0")); - assert_eq!(expected, actual); - Ok(()) -} - -#[test] -#[ignore] -fn folder_with_node_modules() -> io::Result<()> { - let dir = tempfile::tempdir()?; - let node_modules = dir.path().join("node_modules"); - fs::create_dir_all(&node_modules)?; - - let output = render_node_module() - .arg("--path") - .arg(dir.path()) - .output()?; - let actual = String::from_utf8(output.stdout).unwrap(); - - let expected = format!("via {} ", Color::Green.bold().paint("⬢ v12.0.0")); - assert_eq!(expected, actual); - Ok(()) -}