diff --git a/Cargo.lock b/Cargo.lock index 66188da692..65bc3f5fd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2703,6 +2703,7 @@ dependencies = [ "assert_cmd", "crossterm", "ctrlc", + "dirs-next", "divan", "log", "miette", diff --git a/Cargo.toml b/Cargo.toml index c0dbac1faa..4f4ed363ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,6 +123,7 @@ nix = { workspace = true, default-features = false, features = [ [dev-dependencies] nu-test-support = { path = "./crates/nu-test-support", version = "0.91.1" } assert_cmd = "2.0" +dirs-next = "2.0" divan = "0.1.14" pretty_assertions = "1.4" rstest = { workspace = true, default-features = false } diff --git a/crates/nu-path/src/helpers.rs b/crates/nu-path/src/helpers.rs index a3ce3b84cf..aaa53eab71 100644 --- a/crates/nu-path/src/helpers.rs +++ b/crates/nu-path/src/helpers.rs @@ -7,7 +7,18 @@ pub fn home_dir() -> Option { } pub fn config_dir() -> Option { - dirs_next::config_dir().map(|path| canonicalize(&path).unwrap_or(path)) + match std::env::var("XDG_CONFIG_HOME").map(PathBuf::from) { + Ok(xdg_config) if xdg_config.is_absolute() => { + Some(canonicalize(&xdg_config).unwrap_or(xdg_config)) + } + _ => config_dir_old(), + } +} + +/// Get the old default config directory. Outside of Linux, this will ignore `XDG_CONFIG_HOME` +pub fn config_dir_old() -> Option { + let path = dirs_next::config_dir()?; + Some(canonicalize(&path).unwrap_or(path)) } #[cfg(windows)] diff --git a/crates/nu-path/src/lib.rs b/crates/nu-path/src/lib.rs index 0ab69b9c67..63c0091892 100644 --- a/crates/nu-path/src/lib.rs +++ b/crates/nu-path/src/lib.rs @@ -5,6 +5,6 @@ mod tilde; mod util; pub use expansions::{canonicalize_with, expand_path_with, expand_to_real_path}; -pub use helpers::{config_dir, home_dir}; +pub use helpers::{config_dir, config_dir_old, home_dir}; pub use tilde::expand_tilde; pub use util::trim_trailing_slash; diff --git a/crates/nu-protocol/src/errors/shell_error.rs b/crates/nu-protocol/src/errors/shell_error.rs index 1cc87a27cd..cf365947ed 100644 --- a/crates/nu-protocol/src/errors/shell_error.rs +++ b/crates/nu-protocol/src/errors/shell_error.rs @@ -1341,6 +1341,14 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"# #[label = "Could not find config directory"] span: Option, }, + + /// XDG_CONFIG_HOME was set to an invalid path + #[error("$env.XDG_CONFIG_HOME ({xdg}) is invalid, using default config directory instead: {default}")] + #[diagnostic( + code(nu::shell::xdg_config_home_invalid), + help("Set XDG_CONFIG_HOME to an absolute path, or set it to an empty string to ignore it") + )] + InvalidXdgConfig { xdg: String, default: String }, } // TODO: Implement as From trait diff --git a/src/main.rs b/src/main.rs index 4732058ad6..e292cbeba3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,7 @@ use nu_cmd_base::util::get_init_cwd; use nu_lsp::LanguageServer; use nu_protocol::{ engine::EngineState, eval_const::create_nu_constant, report_error_new, util::BufferedReader, - PipelineData, RawStream, Span, Value, NU_VARIABLE_ID, + PipelineData, RawStream, ShellError, Span, Value, NU_VARIABLE_ID, }; use nu_std::load_standard_library; use nu_utils::utils::perf; @@ -35,6 +35,7 @@ use run::{run_commands, run_file, run_repl}; use signals::ctrlc_protection; use std::{ io::BufReader, + path::Path, str::FromStr, sync::{atomic::AtomicBool, Arc}, }; @@ -91,6 +92,37 @@ fn main() -> Result<()> { std::path::PathBuf::new() }; + if let Ok(xdg_config_home) = std::env::var("XDG_CONFIG_HOME") { + if !xdg_config_home.is_empty() { + if nushell_config_path != Path::new(&xdg_config_home).join("nushell") { + report_error_new( + &engine_state, + &ShellError::InvalidXdgConfig { + xdg: xdg_config_home, + default: nushell_config_path.display().to_string(), + }, + ); + } else if let Some(old_config) = nu_path::config_dir_old().map(|p| p.join("nushell")) { + let xdg_config_empty = nushell_config_path + .read_dir() + .map_or(true, |mut dir| dir.next().is_none()); + let old_config_empty = old_config + .read_dir() + .map_or(true, |mut dir| dir.next().is_none()); + if !old_config_empty && xdg_config_empty { + eprintln!( + "WARNING: XDG_CONFIG_HOME has been set but {} is empty.\n", + nushell_config_path.display(), + ); + eprintln!( + "Nushell will not move your configuration files from {}", + old_config.display() + ); + } + } + } + } + let mut default_nu_lib_dirs_path = nushell_config_path.clone(); default_nu_lib_dirs_path.push("scripts"); engine_state.add_env_var( diff --git a/src/tests/test_config_path.rs b/src/tests/test_config_path.rs index 85d2e4ca4b..0b73df999e 100644 --- a/src/tests/test_config_path.rs +++ b/src/tests/test_config_path.rs @@ -1,7 +1,7 @@ use nu_test_support::nu; use nu_test_support::playground::{Executable, Playground}; use pretty_assertions::assert_eq; -use std::fs; +use std::fs::{self, File}; use std::path::{Path, PathBuf}; #[cfg(not(target_os = "windows"))] @@ -22,36 +22,16 @@ fn adjust_canonicalization>(p: P) -> String { /// Make the config directory a symlink that points to a temporary folder. /// Returns the path to the `nushell` config folder inside, via the symlink. -/// -/// Need to figure out how to change config directory on Windows. -#[cfg(any(target_os = "linux", target_os = "macos"))] fn setup_fake_config(playground: &mut Playground) -> PathBuf { - #[cfg(target_os = "linux")] - { - let config_dir = "config"; - let config_link = "config_link"; - playground.mkdir(&format!("{config_dir}/nushell")); - playground.symlink(config_dir, config_link); - playground.with_env( - "XDG_CONFIG_HOME", - &playground.cwd().join(config_link).display().to_string(), - ); - playground.cwd().join(config_link).join("nushell") - } - - #[cfg(target_os = "macos")] - { - let fake_home = "fake_home"; - let home_link = "home_link"; - let dir_end = "Library/Application Support/nushell"; - playground.mkdir(&format!("{fake_home}/{dir_end}")); - playground.symlink(fake_home, home_link); - playground.with_env( - "HOME", - &playground.cwd().join(home_link).display().to_string(), - ); - playground.cwd().join(home_link).join(dir_end) - } + let config_dir = "config"; + let config_link = "config_link"; + playground.mkdir(&format!("{config_dir}/nushell")); + playground.symlink(config_dir, config_link); + playground.with_env( + "XDG_CONFIG_HOME", + &playground.cwd().join(config_link).display().to_string(), + ); + playground.cwd().join(config_link).join("nushell") } fn run(playground: &mut Playground, command: &str) -> String { @@ -131,7 +111,6 @@ fn test_default_config_path() { /// Make the config folder a symlink to a temporary folder without any config files /// and see if the config files' paths are properly canonicalized -#[cfg(any(target_os = "linux", target_os = "macos"))] #[test] fn test_default_symlinked_config_path_empty() { Playground::setup("symlinked_empty_config_dir", |_, playground| { @@ -142,14 +121,16 @@ fn test_default_symlinked_config_path_empty() { /// Like [[test_default_symlinked_config_path_empty]], but fill the temporary folder /// with broken symlinks and see if they're properly canonicalized -#[cfg(any(target_os = "linux", target_os = "macos"))] #[test] fn test_default_symlink_config_path_broken_symlink_config_files() { Playground::setup( - "symlinked_cfg_dir_with_symlinked_cfg_files", + "symlinked_cfg_dir_with_symlinked_cfg_files_broken", |_, playground| { let fake_config_dir_nushell = setup_fake_config(playground); + let fake_dir = PathBuf::from("fake"); + playground.mkdir(&fake_dir.display().to_string()); + for config_file in [ "config.nu", "env.nu", @@ -158,12 +139,17 @@ fn test_default_symlink_config_path_broken_symlink_config_files() { "login.nu", "plugin.nu", ] { - playground.symlink( - format!("fake/{config_file}"), - fake_config_dir_nushell.join(config_file), - ); + let fake_file = fake_dir.join(config_file); + File::create(playground.cwd().join(&fake_file)).unwrap(); + + playground.symlink(&fake_file, fake_config_dir_nushell.join(config_file)); } + // Windows doesn't allow creating a symlink without the file existing, + // so we first create original files for the symlinks, then delete them + // to break the symlinks + std::fs::remove_dir_all(playground.cwd().join(&fake_dir)).unwrap(); + test_config_path_helper(playground, fake_config_dir_nushell); }, ); @@ -171,11 +157,8 @@ fn test_default_symlink_config_path_broken_symlink_config_files() { /// Like [[test_default_symlinked_config_path_empty]], but fill the temporary folder /// with working symlinks to empty files and see if they're properly canonicalized -#[cfg(any(target_os = "linux", target_os = "macos"))] #[test] fn test_default_config_path_symlinked_config_files() { - use std::fs::File; - Playground::setup( "symlinked_cfg_dir_with_symlinked_cfg_files", |_, playground| { @@ -221,3 +204,37 @@ fn test_alternate_config_path() { ); assert_eq!(actual.out, env_path.to_string_lossy().to_string()); } + +#[test] +fn test_xdg_config_empty() { + Playground::setup("xdg_config_empty", |_, playground| { + playground.with_env("XDG_CONFIG_HOME", ""); + + let actual = nu!("$nu.default-config-dir"); + assert_eq!( + actual.out, + dirs_next::config_dir() + .unwrap() + .join("nushell") + .display() + .to_string() + ); + }); +} + +#[test] +fn test_xdg_config_bad() { + Playground::setup("xdg_config_bad", |_, playground| { + playground.with_env("XDG_CONFIG_HOME", r#"mn2''6t\/k*((*&^//k//: "#); + + let actual = nu!("$nu.default-config-dir"); + assert_eq!( + actual.out, + dirs_next::config_dir() + .unwrap() + .join("nushell") + .display() + .to_string() + ); + }); +}