diff --git a/src/config.rs b/src/config.rs index 53d11a0b..f778cf31 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use crate::configs::Palette; use crate::context::Context; + use crate::serde_utils::{ValueDeserializer, ValueRef}; use crate::utils; use nu_ansi_term::Color; @@ -10,9 +11,9 @@ use serde::{ use std::borrow::Cow; use std::clone::Clone; use std::collections::HashMap; +use std::ffi::OsString; use std::io::ErrorKind; -use std::env; use toml::Value; /// Root config of a module. @@ -120,25 +121,10 @@ pub struct StarshipConfig { pub config: Option, } -pub fn get_config_path() -> Option { - if let Ok(path) = env::var("STARSHIP_CONFIG") { - // Use $STARSHIP_CONFIG as the config path if available - log::debug!("STARSHIP_CONFIG is set: {}", &path); - Some(path) - } else { - // Default to using ~/.config/starship.toml - log::debug!("STARSHIP_CONFIG is not set"); - let config_path = utils::home_dir()?.join(".config/starship.toml"); - let config_path_str = config_path.to_str()?.to_owned(); - log::debug!("Using default config path: {}", config_path_str); - Some(config_path_str) - } -} - impl StarshipConfig { /// Initialize the Config struct - pub fn initialize() -> Self { - Self::config_from_file() + pub fn initialize(config_file_path: &Option) -> Self { + Self::config_from_file(config_file_path) .map(|config| Self { config: Some(config), }) @@ -146,10 +132,30 @@ impl StarshipConfig { } /// Create a config from a starship configuration file - fn config_from_file() -> Option { - let file_path = get_config_path()?; + fn config_from_file(config_file_path: &Option) -> Option { + let toml_content = Self::read_config_content_as_str(config_file_path)?; - let toml_content = match utils::read_file(file_path) { + match toml::from_str(&toml_content) { + Ok(parsed) => { + log::debug!("Config parsed: {:?}", &parsed); + Some(parsed) + } + Err(error) => { + log::error!("Unable to parse the config file: {}", error); + None + } + } + } + + pub fn read_config_content_as_str(config_file_path: &Option) -> Option { + if config_file_path.is_none() { + log::debug!( + "Unable to determine `config_file_path`. Perhaps `utils::home_dir` is not defined on your platform?" + ); + return None; + } + let config_file_path = config_file_path.as_ref().unwrap(); + match utils::read_file(config_file_path) { Ok(content) => { log::trace!("Config file content: \"\n{}\"", &content); Some(content) @@ -164,17 +170,6 @@ impl StarshipConfig { log::log!(level, "Unable to read config file content: {}", &e); None } - }?; - - match toml::from_str(&toml_content) { - Ok(parsed) => { - log::debug!("Config parsed: {:?}", &parsed); - Some(parsed) - } - Err(error) => { - log::error!("Unable to parse the config file: {}", error); - None - } } } @@ -921,4 +916,13 @@ mod tests { // Test default behavior assert!(get_palette(&palettes, None).is_none()); } + + #[test] + fn read_config_no_config_file_path_provided() { + assert_eq!( + None, + StarshipConfig::read_config_content_as_str(&None), + "if the platform doesn't have utils::home_dir(), it should return None" + ); + } } diff --git a/src/configure.rs b/src/configure.rs index a77c8334..50554899 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -1,6 +1,3 @@ -use std::env; -use std::ffi::OsString; -use std::io::ErrorKind; use std::process; use std::process::Stdio; use std::str::FromStr; @@ -8,6 +5,7 @@ use std::str::FromStr; use crate::config::ModuleConfig; use crate::config::StarshipConfig; use crate::configs::PROMPT_ORDER; +use crate::context::Context; use crate::utils; use std::fs::File; use std::io::Write; @@ -18,15 +16,15 @@ const STD_EDITOR: &str = "vi"; #[cfg(windows)] const STD_EDITOR: &str = "notepad.exe"; -pub fn update_configuration(name: &str, value: &str) { - let mut doc = get_configuration_edit(); +pub fn update_configuration(context: &Context, name: &str, value: &str) { + let mut doc = get_configuration_edit(context); match handle_update_configuration(&mut doc, name, value) { Err(e) => { eprintln!("{e}"); process::exit(1); } - _ => write_configuration(&doc), + _ => write_configuration(context, &doc), } } @@ -71,7 +69,7 @@ fn handle_update_configuration(doc: &mut Document, name: &str, value: &str) -> R Ok(()) } -pub fn print_configuration(use_default: bool, paths: &[String]) { +pub fn print_configuration(context: &Context, use_default: bool, paths: &[String]) -> String { let config = if use_default { // Get default config let default_config = crate::configs::FullConfig::default(); @@ -79,7 +77,7 @@ pub fn print_configuration(use_default: bool, paths: &[String]) { toml::value::Value::try_from(default_config).unwrap() } else { // Get config as toml::Value - let user_config = get_configuration(); + let user_config = get_configuration(context); // Convert into FullConfig and fill in default values let user_config = crate::configs::FullConfig::load(&user_config); // Convert back to Value because toml can't serialize FullConfig directly @@ -124,6 +122,7 @@ pub fn print_configuration(use_default: bool, paths: &[String]) { let string_config = toml::to_string_pretty(&print_config).unwrap(); println!("{string_config}"); + string_config } fn extract_toml_paths(mut config: toml::Value, paths: &[String]) -> toml::Value { @@ -176,15 +175,15 @@ fn extract_toml_paths(mut config: toml::Value, paths: &[String]) -> toml::Value toml::Value::Table(subset) } -pub fn toggle_configuration(name: &str, key: &str) { - let mut doc = get_configuration_edit(); +pub fn toggle_configuration(context: &Context, name: &str, key: &str) { + let mut doc = get_configuration_edit(context); match handle_toggle_configuration(&mut doc, name, key) { Err(e) => { eprintln!("{e}"); process::exit(1); } - _ => write_configuration(&doc), + _ => write_configuration(context, &doc), } } @@ -217,32 +216,15 @@ fn handle_toggle_configuration(doc: &mut Document, name: &str, key: &str) -> Res Ok(()) } -pub fn get_configuration() -> toml::Table { - let starship_config = StarshipConfig::initialize(); +pub fn get_configuration(context: &Context) -> toml::Table { + let starship_config = StarshipConfig::initialize(&context.get_config_path_os()); - starship_config - .config - .expect("Failed to load starship config") + starship_config.config.unwrap_or(toml::Table::new()) } -pub fn get_configuration_edit() -> Document { - let file_path = get_config_path(); - let toml_content = match utils::read_file(file_path) { - Ok(content) => { - log::trace!("Config file content: \"\n{}\"", &content); - Some(content) - } - Err(e) => { - let level = if e.kind() == ErrorKind::NotFound { - log::Level::Debug - } else { - log::Level::Error - }; - - log::log!(level, "Unable to read config file content: {}", &e); - None - } - }; +pub fn get_configuration_edit(context: &Context) -> Document { + let config_file_path = context.get_config_path_os(); + let toml_content = StarshipConfig::read_config_content_as_str(&config_file_path); toml_content .unwrap_or_default() @@ -250,8 +232,11 @@ pub fn get_configuration_edit() -> Document { .expect("Failed to load starship config") } -pub fn write_configuration(doc: &Document) { - let config_path = get_config_path(); +pub fn write_configuration(context: &Context, doc: &Document) { + let config_path = context.get_config_path_os().unwrap_or_else(|| { + eprintln!("config path required to write configuration"); + process::exit(1); + }); let config_str = doc.to_string(); @@ -260,10 +245,16 @@ pub fn write_configuration(doc: &Document) { .expect("Error writing starship config"); } -pub fn edit_configuration(editor_override: Option<&str>) -> Result<(), Box> { +pub fn edit_configuration( + context: &Context, + editor_override: Option<&str>, +) -> Result<(), Box> { // Argument currently only used for testing, but could be used to specify // an editor override on the command line. - let config_path = get_config_path(); + let config_path = context.get_config_path_os().unwrap_or_else(|| { + eprintln!("config path required to edit configuration"); + process::exit(1); + }); let editor_cmd = shell_words::split(&get_editor(editor_override))?; let mut command = match utils::create_command(&editor_cmd[0]) { @@ -313,19 +304,18 @@ fn get_editor_internal(visual: Option, editor: Option) -> String STD_EDITOR.into() } -fn get_config_path() -> OsString { - if let Some(config_path) = env::var_os("STARSHIP_CONFIG") { - return config_path; - } - utils::home_dir() - .expect("couldn't find home directory") - .join(".config") - .join("starship.toml") - .into() -} - #[cfg(test)] mod tests { + use std::{fs::create_dir, io}; + + use tempfile::TempDir; + use toml_edit::Item; + + use crate::{ + context::{Shell, Target}, + context_env::Env, + }; + use super::*; // This is every possible permutation, 3² = 9. @@ -379,13 +369,13 @@ mod tests { #[test] fn no_panic_when_editor_unparsable() { - let outcome = edit_configuration(Some("\"vim")); + let outcome = edit_configuration(&Default::default(), Some("\"vim")); assert!(outcome.is_err()); } #[test] fn no_panic_when_editor_not_found() { - let outcome = edit_configuration(Some("this_editor_does_not_exist")); + let outcome = edit_configuration(&Default::default(), Some("this_editor_does_not_exist")); assert!(outcome.is_err()); } @@ -581,4 +571,115 @@ mod tests { .as_bool() .unwrap()) } + + #[test] + fn write_and_get_configuration_test() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let context = setup_config(&dir, true, StarshipConfigEnvScenario::NotSpecified)?; + let mut doc = get_configuration_edit(&context); + doc["directory"]["format"] = Item::Value("myformat".into()); + write_configuration(&context, &doc); + let doc_reloaded = get_configuration_edit(&context); + assert_eq!( + "myformat", + doc_reloaded["directory"]["format"].as_str().unwrap() + ); + dir.close() + } + + const PRINT_CONFIG_DEFAULT: &str = "[custom]"; + const PRINT_CONFIG_HOME: &str = "[custom.home]"; + const PRINT_CONFIG_ENV: &str = "[custom.STARSHIP_CONFIG]"; + + #[test] + fn print_configuration_scenarios() -> io::Result<()> { + run_print_configuration_test( + "~/.config/starship.toml, no STARSHIP_CONFIG uses home", + true, + StarshipConfigEnvScenario::NotSpecified, + PRINT_CONFIG_HOME, + )?; + run_print_configuration_test( + "no ~/.config/starship.toml, no STARSHIP_CONFIG uses default", + false, + StarshipConfigEnvScenario::NotSpecified, + PRINT_CONFIG_DEFAULT, + )?; + run_print_configuration_test( + "~/.config/starship.toml, STARSHIP_CONFIG nonexisting file uses default", + true, + StarshipConfigEnvScenario::NonExistingFile, + PRINT_CONFIG_DEFAULT, + )?; + run_print_configuration_test( + "~/.config/starship.toml, STARSHIP_CONFIG existing file uses STARSHIP_CONFIG file", + true, + StarshipConfigEnvScenario::ExistingFile, + PRINT_CONFIG_ENV, + )?; + Ok(()) + } + + enum StarshipConfigEnvScenario { + NotSpecified, + NonExistingFile, + ExistingFile, + } + + fn run_print_configuration_test( + message: &str, + home_file_exists: bool, + starship_config_env_scenario: StarshipConfigEnvScenario, + expected_first_line: &str, + ) -> io::Result<()> { + let dir = tempfile::tempdir()?; + let context = setup_config(&dir, home_file_exists, starship_config_env_scenario)?; + let config = print_configuration(&context, false, &["custom".to_string()]); + let first_line = config.split('\n').next().unwrap(); + assert_eq!(expected_first_line, first_line, "{message}"); + dir.close() + } + + fn setup_config( + dir: &TempDir, + home_file_exists: bool, + starship_config_env_scenario: StarshipConfigEnvScenario, + ) -> io::Result { + let config_path = dir.path().to_path_buf().join(".config"); + create_dir(&config_path)?; + let home_starship_toml = config_path.join("starship.toml"); + let env_toml = dir.path().join("env.toml"); + if home_file_exists { + let mut home_file = File::create(home_starship_toml)?; + home_file.write_all(PRINT_CONFIG_HOME.as_bytes())?; + } + + let env_starship_config = match starship_config_env_scenario { + StarshipConfigEnvScenario::NotSpecified => None, + StarshipConfigEnvScenario::NonExistingFile => Some(env_toml), + StarshipConfigEnvScenario::ExistingFile => { + let mut env_toml_file = File::create(&env_toml)?; + env_toml_file.write_all(PRINT_CONFIG_ENV.as_bytes())?; + Some(env_toml) + } + }; + + let mut env = Env::default(); + if let Some(v) = env_starship_config { + env.insert("STARSHIP_CONFIG", v.to_string_lossy().to_string()); + } + env.insert( + "HOME", + dir.path().to_path_buf().to_string_lossy().to_string(), + ); + + Ok(Context::new_with_shell_and_path( + Default::default(), + Shell::Unknown, + Target::Main, + Default::default(), + Default::default(), + env, + )) + } } diff --git a/src/context.rs b/src/context.rs index b0bfc0fd..4ece08dd 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,10 +1,11 @@ use crate::config::{ModuleConfig, StarshipConfig}; use crate::configs::StarshipRootConfig; +use crate::context_env::Env; use crate::module::Module; use crate::utils::{create_command, exec_timeout, read_file, CommandOutput, PathExt}; use crate::modules; -use crate::utils::{self, home_dir}; +use crate::utils; use clap::Parser; use gix::{ sec::{self as git_sec, trust::DefaultForLevel}, @@ -60,8 +61,7 @@ pub struct Context<'a> { pub width: usize, /// A HashMap of environment variable mocks - #[cfg(test)] - pub env: HashMap<&'a str, String>, + pub env: Env<'a>, /// A HashMap of command mocks #[cfg(test)] @@ -107,7 +107,14 @@ impl<'a> Context<'a> { .or_else(|| env::var("PWD").map(PathBuf::from).ok()) .unwrap_or_else(|| path.clone()); - Context::new_with_shell_and_path(arguments, shell, target, path, logical_path) + Context::new_with_shell_and_path( + arguments, + shell, + target, + path, + logical_path, + Default::default(), + ) } /// Create a new instance of Context for the provided directory @@ -117,8 +124,9 @@ impl<'a> Context<'a> { target: Target, path: PathBuf, logical_path: PathBuf, + env: Env<'a>, ) -> Context<'a> { - let config = StarshipConfig::initialize(); + let config = StarshipConfig::initialize(&get_config_path_os(&env)); // If the vector is zero-length, we should pretend that we didn't get a // pipestatus at all (since this is the input `--pipestatus=""`) @@ -162,11 +170,10 @@ impl<'a> Context<'a> { shell, target, width, + env, #[cfg(test)] root_dir: tempfile::TempDir::new().unwrap(), #[cfg(test)] - env: HashMap::new(), - #[cfg(test)] cmd: HashMap::new(), #[cfg(feature = "battery")] battery_info_provider: &crate::modules::BatteryInfoProviderImpl, @@ -177,37 +184,19 @@ impl<'a> Context<'a> { // Tries to retrieve home directory from a table in testing mode or else retrieves it from the os pub fn get_home(&self) -> Option { - if cfg!(test) { - return self.get_env("HOME").map(PathBuf::from).or_else(home_dir); - } - - home_dir() + home_dir(&self.env) } // Retrieves a environment variable from the os or from a table if in testing mode - #[cfg(test)] - pub fn get_env>(&self, key: K) -> Option { - self.env - .get(key.as_ref()) - .map(std::string::ToString::to_string) - } - - #[cfg(not(test))] #[inline] pub fn get_env>(&self, key: K) -> Option { - env::var(key.as_ref()).ok() + self.env.get_env(key) } // Retrieves a environment variable from the os or from a table if in testing mode (os version) - #[cfg(test)] - pub fn get_env_os>(&self, key: K) -> Option { - self.env.get(key.as_ref()).map(OsString::from) - } - - #[cfg(not(test))] #[inline] pub fn get_env_os>(&self, key: K) -> Option { - env::var_os(key.as_ref()) + self.env.get_env_os(key) } /// Convert a `~` in a path to the home directory @@ -401,6 +390,32 @@ impl<'a> Context<'a> { read_file(self.current_dir.join(file_name)).ok() } + + pub fn get_config_path_os(&self) -> Option { + get_config_path_os(&self.env) + } +} + +impl Default for Context<'_> { + fn default() -> Self { + Context::new(Default::default(), Target::Main) + } +} + +fn home_dir(env: &Env) -> Option { + if cfg!(test) { + if let Some(home) = env.get_env("HOME") { + return Some(PathBuf::from(home)); + } + } + utils::home_dir() +} + +fn get_config_path_os(env: &Env) -> Option { + if let Some(config_path) = env.get_env_os("STARSHIP_CONFIG") { + return Some(config_path); + } + Some(home_dir(env)?.join(".config").join("starship.toml").into()) } #[derive(Debug)] @@ -907,6 +922,7 @@ mod tests { Target::Main, test_path.clone(), test_path.clone(), + Default::default(), ); assert_ne!(context.current_dir, context.logical_dir); @@ -931,6 +947,7 @@ mod tests { Target::Main, test_path.clone(), test_path.clone(), + Default::default(), ); let expected_current_dir = &test_path; @@ -952,6 +969,7 @@ mod tests { Target::Main, test_path.clone(), test_path.clone(), + Default::default(), ); let expected_current_dir = home_dir() @@ -973,6 +991,7 @@ mod tests { Target::Main, test_path.clone(), test_path, + Default::default(), ); let expected_path = Path::new(r"C:\"); diff --git a/src/context_env.rs b/src/context_env.rs new file mode 100644 index 00000000..5e881951 --- /dev/null +++ b/src/context_env.rs @@ -0,0 +1,48 @@ +#[cfg(test)] +use std::collections::HashMap; +#[cfg(not(test))] +use std::env; +use std::ffi::OsString; + +#[derive(Default)] +pub struct Env<'a> { + /// A HashMap of environment variable mocks + #[cfg(test)] + pub env: HashMap<&'a str, String>, + + #[cfg(not(test))] + _marker: std::marker::PhantomData<&'a ()>, +} + +impl<'a> Env<'a> { + // Retrieves a environment variable from the os or from a table if in testing mode + #[cfg(test)] + pub fn get_env>(&self, key: K) -> Option { + self.env + .get(key.as_ref()) + .map(std::string::ToString::to_string) + } + + #[cfg(not(test))] + #[inline] + pub fn get_env>(&self, key: K) -> Option { + env::var(key.as_ref()).ok() + } + + // Retrieves a environment variable from the os or from a table if in testing mode (os version) + #[cfg(test)] + pub fn get_env_os>(&self, key: K) -> Option { + self.env.get(key.as_ref()).map(OsString::from) + } + + #[cfg(not(test))] + #[inline] + pub fn get_env_os>(&self, key: K) -> Option { + env::var_os(key.as_ref()) + } + + #[cfg(test)] + pub fn insert(&mut self, k: &'a str, v: String) -> Option { + self.env.insert(k, v) + } +} diff --git a/src/lib.rs b/src/lib.rs index cfacfa0a..50586637 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub mod config; pub mod configs; pub mod configure; pub mod context; +pub mod context_env; pub mod formatter; pub mod init; pub mod logger; diff --git a/src/main.rs b/src/main.rs index dc01666b..cc30950a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use clap::{CommandFactory, Parser, Subcommand}; use clap_complete::{generate, Shell as CompletionShell}; use rand::distributions::Alphanumeric; use rand::Rng; -use starship::context::{Properties, Target}; +use starship::context::{Context, Properties, Target}; use starship::module::ALL_MODULES; use starship::*; @@ -208,17 +208,22 @@ fn main() { } Commands::Preset { name, list, output } => print::preset_command(name, output, list), Commands::Config { name, value } => { + let context = Context::default(); if let Some(name) = name { if let Some(value) = value { - configure::update_configuration(&name, &value) + configure::update_configuration(&context, &name, &value) } - } else if let Err(reason) = configure::edit_configuration(None) { + } else if let Err(reason) = configure::edit_configuration(&context, None) { eprintln!("Could not edit configuration: {reason}"); std::process::exit(1); } } - Commands::PrintConfig { default, name } => configure::print_configuration(default, &name), - Commands::Toggle { name, value } => configure::toggle_configuration(&name, &value), + Commands::PrintConfig { default, name } => { + configure::print_configuration(&Context::default(), default, &name); + } + Commands::Toggle { name, value } => { + configure::toggle_configuration(&Context::default(), &name, &value) + } Commands::BugReport => bug_report::create(), Commands::Time => { match SystemTime::now() diff --git a/src/modules/git_status.rs b/src/modules/git_status.rs index 852f1753..451c1e07 100644 --- a/src/modules/git_status.rs +++ b/src/modules/git_status.rs @@ -386,6 +386,7 @@ fn git_status_wsl(context: &Context, conf: &GitStatusConfig) -> Option { use crate::utils::create_command; use nix::sys::utsname::uname; use std::env; + use std::ffi::OsString; use std::io::ErrorKind; let starship_exe = conf.windows_starship?; @@ -454,7 +455,9 @@ fn git_status_wsl(context: &Context, conf: &GitStatusConfig) -> Option { .map(|mut c| { c.env( "STARSHIP_CONFIG", - crate::config::get_config_path().unwrap_or_else(|| "/dev/null".to_string()), + context + .get_config_path_os() + .unwrap_or_else(|| OsString::from("/dev/null")), ) .env("WSLENV", wslenv) .args(["module", "git_status", "--path", winpath]); diff --git a/src/modules/rust.rs b/src/modules/rust.rs index b266f6f0..221111ea 100644 --- a/src/modules/rust.rs +++ b/src/modules/rust.rs @@ -735,6 +735,7 @@ version = "12" Target::Main, dir.path().into(), dir.path().into(), + Default::default(), ); assert_eq!( @@ -756,6 +757,7 @@ version = "12" Target::Main, dir.path().into(), dir.path().into(), + Default::default(), ); assert_eq!( @@ -777,6 +779,7 @@ version = "12" Target::Main, dir.path().into(), dir.path().into(), + Default::default(), ); assert_eq!( @@ -800,6 +803,7 @@ version = "12" Target::Main, child_dir_path.clone(), child_dir_path, + Default::default(), ); assert_eq!( @@ -820,6 +824,7 @@ version = "12" Target::Main, dir.path().into(), dir.path().into(), + Default::default(), ); assert_eq!(find_rust_toolchain_file(&context), None); @@ -838,6 +843,7 @@ version = "12" Target::Main, dir.path().into(), dir.path().into(), + Default::default(), ); assert_eq!( @@ -859,6 +865,7 @@ version = "12" Target::Main, dir.path().into(), dir.path().into(), + Default::default(), ); assert_eq!( @@ -882,6 +889,7 @@ version = "12" Target::Main, child_dir_path.clone(), child_dir_path, + Default::default(), ); assert_eq!( diff --git a/src/test/mod.rs b/src/test/mod.rs index 23b0d6fb..dd25c8f0 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -43,6 +43,7 @@ pub fn default_context() -> Context<'static> { Target::Main, PathBuf::new(), PathBuf::new(), + Default::default(), ); context.config = StarshipConfig { config: None }; context