diff --git a/crates/nu-cli/src/commands/history/history_.rs b/crates/nu-cli/src/commands/history/history_.rs index 40d951966e..cd4affe015 100644 --- a/crates/nu-cli/src/commands/history/history_.rs +++ b/crates/nu-cli/src/commands/history/history_.rs @@ -45,8 +45,10 @@ impl Command for History { return Ok(PipelineData::empty()); }; // todo for sqlite history this command should be an alias to `open ~/.config/nushell/history.sqlite3 | get history` - let Some(history_path) = history.file_path() else { - return Err(ShellError::ConfigDirNotFound { span: Some(head) }); + let result = history.file_path(Some(call.head)); + let history_path = match result { + Ok(path) => path, + Err(err) => return Err(err), }; if call.has_flag(engine_state, stack, "clear")? { diff --git a/crates/nu-cli/src/commands/history/history_import.rs b/crates/nu-cli/src/commands/history/history_import.rs index d56bc1da03..e2128025eb 100644 --- a/crates/nu-cli/src/commands/history/history_import.rs +++ b/crates/nu-cli/src/commands/history/history_import.rs @@ -73,10 +73,10 @@ Note that history item IDs are ignored when importing from file."# let Some(history) = engine_state.history_config() else { return ok; }; - let Some(current_history_path) = history.file_path() else { - return Err(ShellError::ConfigDirNotFound { - span: Some(call.head), - }); + let result = history.file_path(Some(call.head)); + let current_history_path = match result { + Ok(path) => path, + Err(err) => return Err(err), }; if let Some(bak_path) = backup(¤t_history_path)? { println!("Backed history to {}", bak_path.display()); diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index 5440f9caa2..2c4ef5c103 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -1126,7 +1126,7 @@ fn setup_history( None }; - if let Some(path) = history.file_path() { + if let Ok(path) = history.file_path(None) { return update_line_editor_history( engine_state, path, @@ -1401,7 +1401,7 @@ fn trailing_slash_looks_like_path() { fn are_session_ids_in_sync() { let engine_state = &mut EngineState::new(); let history = engine_state.history_config().unwrap(); - let history_path = history.file_path().unwrap(); + let history_path = history.file_path(None).unwrap(); let line_editor = reedline::Reedline::create(); let history_session_id = reedline::Reedline::create_history_session_id(); let line_editor = update_line_editor_history( diff --git a/crates/nu-protocol/src/config/history.rs b/crates/nu-protocol/src/config/history.rs index 8f7af7c4a5..288bfda6f2 100644 --- a/crates/nu-protocol/src/config/history.rs +++ b/crates/nu-protocol/src/config/history.rs @@ -37,20 +37,54 @@ impl UpdateFromValue for HistoryFileFormat { } } -#[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)] pub struct HistoryConfig { pub max_size: i64, pub sync_on_enter: bool, pub file_format: HistoryFileFormat, pub isolation: bool, + pub path: Option, } impl HistoryConfig { - pub fn file_path(&self) -> Option { - nu_path::nu_config_dir().map(|mut history_path| { + pub fn file_path(&self, call_head: Option) -> Result { + match self.path.clone() { + None => self.system_defined_file_path(call_head), + Some(path) => self.user_defined_file_path(path, call_head), + } + } + + fn system_defined_file_path( + &self, + call_head: Option, + ) -> Result { + let system_path = nu_path::nu_config_dir().map(|mut history_path| { history_path.push(self.file_format.default_file_name()); history_path.into() - }) + }); + match system_path { + Some(path) => Ok(path), + None => Err(ShellError::ConfigDirNotFound { span: call_head }), + } + } + + fn user_defined_file_path( + &self, + path_from_config: String, + call_head: Option, + ) -> Result { + let error = Err(ShellError::HistoryDirNotFound { span: call_head }); + + let user_path = std::path::Path::new(&path_from_config); + let dir_path = match user_path.parent() { + Some(path) => path, + None => return error, + }; + + match dir_path.exists() { + true => Ok(user_path.into()), + false => error, + } } } @@ -61,6 +95,7 @@ impl Default for HistoryConfig { sync_on_enter: true, file_format: HistoryFileFormat::Plaintext, isolation: false, + path: None, } } } @@ -84,6 +119,11 @@ impl UpdateFromValue for HistoryConfig { "sync_on_enter" => self.sync_on_enter.update(val, path, errors), "max_size" => self.max_size.update(val, path, errors), "file_format" => self.file_format.update(val, path, errors), + "path" => match val { + Value::Nothing { .. } => self.path = None, + Value::String { val, .. } => self.path = Some(val.clone()), + _ => errors.type_mismatch(path, Type::custom("path or nothing"), val), + }, _ => errors.unknown_option(path, val), } } diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 626033f915..ab1888d3cd 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -762,7 +762,7 @@ impl EngineState { /// Returns the configuration settings for command history or `None` if history is disabled pub fn history_config(&self) -> Option { - self.history_enabled.then(|| self.config.history) + self.history_enabled.then(|| self.config.history.clone()) } pub fn get_var(&self, var_id: VarId) -> &Variable { diff --git a/crates/nu-protocol/src/errors/shell_error.rs b/crates/nu-protocol/src/errors/shell_error.rs index b4b6c29cee..ed1fcf14e4 100644 --- a/crates/nu-protocol/src/errors/shell_error.rs +++ b/crates/nu-protocol/src/errors/shell_error.rs @@ -1434,6 +1434,20 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"# span: Option, }, + /// The config directory could not be found + #[error("The base directory for the history file could not be found")] + #[diagnostic( + code(nu::shell::history_dir_not_found), + help( + r#"A user-defined path for the history file was given in the config, but the base directory for the path does not exist. +Please create it."# + ) + )] + HistoryDirNotFound { + #[label = "Could not find history 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( diff --git a/crates/nu-utils/src/default_files/sample_config.nu b/crates/nu-utils/src/default_files/sample_config.nu index 5a8181506c..dd1f398e5e 100644 --- a/crates/nu-utils/src/default_files/sample_config.nu +++ b/crates/nu-utils/src/default_files/sample_config.nu @@ -63,6 +63,19 @@ $env.config.history.sync_on_enter = true # This setting only applies to SQLite-backed history $env.config.history.isolation = true +# path (string): +# The absolute path to the history file. +# +# When this config doesn't exist or is set to null, then a default path is used +# based on the OS and ENV. +# +# You will likely want to match the file extension to the `file_format` setting, +# therefore ".txt" or ".sqlite". +# +# Nushell will create the file if it doesn't exist. However it won't create the +# directory path, and will error if it doesn't exist. +$env.config.history.path = null + # ---------------------- # Miscellaneous Settings # ----------------------