diff --git a/Cargo.lock b/Cargo.lock index db40d7ae3..413eebdad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,6 +363,27 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.0", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -400,6 +421,16 @@ dependencies = [ "syn", ] +[[package]] +name = "edit" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f364860e764787163c8c8f58231003839be31276e821e2ad2092ddf496b1aa09" +dependencies = [ + "tempfile", + "which", +] + [[package]] name = "either" version = "1.15.0" @@ -475,6 +506,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -484,7 +526,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi", + "wasi 0.14.4+wasi-0.2.4", ] [[package]] @@ -549,6 +591,15 @@ dependencies = [ "cmake", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "human-panic" version = "2.0.3" @@ -761,7 +812,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom", + "getrandom 0.3.3", "libc", ] @@ -809,6 +860,16 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "libz-sys" version = "1.1.22" @@ -821,6 +882,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -884,7 +951,9 @@ dependencies = [ "csv", "current_dir", "derive_more", + "dirs", "documented", + "edit", "fern", "float-cmp", "highs", @@ -962,6 +1031,12 @@ dependencies = [ "syn", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "os_info" version = "3.12.0" @@ -1130,6 +1205,17 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.11.2" @@ -1213,6 +1299,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.1" @@ -1377,9 +1476,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.1.1", "windows-sys 0.61.0", ] @@ -1553,7 +1652,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom", + "getrandom 0.3.3", ] [[package]] @@ -1562,6 +1661,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasi" version = "0.14.4+wasi-0.2.4" @@ -1630,6 +1735,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 81d3d245c..9ad54fbaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,8 @@ derive_more = {version = "2.0", features = ["add", "display"]} petgraph = "0.8.2" strum = {version = "0.27.2", features = ["derive"]} documented = "0.9.2" +dirs = "6.0.0" +edit = "0.1.5" [dev-dependencies] current_dir = "0.1.2" diff --git a/docs/user_guide.md b/docs/user_guide.md index 00ca1fb63..cc1ca296d 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -5,10 +5,22 @@ Once you have installed MUSE 2.0, you should be able to run it via the `muse2` command-line program. For details of the command-line interface, [see here](./command_line_help.md). -You can also configure the behaviour of MUSE 2.0 by creating a `settings.toml` file. For more -information, see [the documentation for this file][settings.toml-docs]. +## Modifying the program settings -[settings.toml-docs]: https://energysystemsmodellinglab.github.io/MUSE_2.0/file_formats/program_settings.html +You can configure the behaviour of MUSE 2.0 with a `settings.toml` file. To edit this file, run: + +```sh +muse2 settings edit +``` + +There are also some more commands for working with the settings file; for details, run: `muse2 +settings help`. + +For information about the available settings, see [the documentation for the `settings.toml` +file][settings.toml-docs]. + +[settings.toml-docs]: +https://energysystemsmodellinglab.github.io/MUSE_2.0/file_formats/program_settings.html ## Setting the log level diff --git a/src/cli.rs b/src/cli.rs index 4676db93e..366f0ff91 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -11,6 +11,9 @@ use std::path::{Path, PathBuf}; pub mod example; use example::ExampleSubcommands; +pub mod settings; +use settings::SettingsSubcommands; + /// The command line interface for the simulation. #[derive(Parser)] #[command(version, about)] @@ -59,21 +62,23 @@ enum Commands { /// The path to the model directory. model_dir: PathBuf, }, - /// Print default settings file. - DumpDefaultSettings, + /// Manage settings file. + Settings { + /// The subcommands for managing the settings file. + #[command(subcommand)] + subcommand: SettingsSubcommands, + }, } impl Commands { /// Execute the supplied CLI command fn execute(self) -> Result<()> { match self { - Self::Run { model_dir, opts } => handle_run_command(&model_dir, &opts, None)?, - Self::Example { subcommand } => subcommand.execute()?, - Self::Validate { model_dir } => handle_validate_command(&model_dir, None)?, - Self::DumpDefaultSettings => handle_dump_default_settings(), + Self::Run { model_dir, opts } => handle_run_command(&model_dir, &opts, None), + Self::Example { subcommand } => subcommand.execute(), + Self::Validate { model_dir } => handle_validate_command(&model_dir, None), + Self::Settings { subcommand } => subcommand.execute(), } - - Ok(()) } } @@ -173,8 +178,3 @@ pub fn handle_validate_command(model_path: &Path, settings: Option) -> Ok(()) } - -/// Handle the `dump-default-settings` command -fn handle_dump_default_settings() { - print!("{}", Settings::default_file_contents()); -} diff --git a/src/cli/settings.rs b/src/cli/settings.rs new file mode 100644 index 000000000..3eeee62a6 --- /dev/null +++ b/src/cli/settings.rs @@ -0,0 +1,116 @@ +//! Code related to CLI interface for managing the settings file +use crate::settings::{Settings, get_settings_file_path}; +use anyhow::{Context, Result, bail}; +use clap::Subcommand; +use std::fs; +use std::io::{self, Write}; +use std::path::Path; + +/// Subcommands for settings +#[derive(Subcommand)] +pub enum SettingsSubcommands { + /// Edit the program settings file + Edit, + /// Delete the settings file, if any + Delete, + /// Get the path to where the settings file is read from + Path, + /// Show the contents of the `settings.toml`, if present + Show, + /// Show the default settings for `settings.toml` + ShowDefault, +} + +impl SettingsSubcommands { + /// Execute the supplied settings subcommand + pub fn execute(self) -> Result<()> { + match self { + Self::Edit => handle_edit_command()?, + Self::Delete => handle_delete_command()?, + Self::Path => handle_path_command(), + Self::Show => handle_show_command()?, + Self::ShowDefault => handle_show_default_command(), + } + + Ok(()) + } +} + +/// Get the path to the settings file, creating it if it doesn't exist +fn ensure_settings_file_exists(file_path: &Path) -> Result<()> { + if file_path.is_file() { + // File already exists + return Ok(()); + } + + if let Some(dir_path) = file_path.parent() { + // Create parent directory + fs::create_dir_all(dir_path) + .with_context(|| format!("Failed to create directory: {}", dir_path.display()))?; + } + + // Create placeholder settings file + fs::write(file_path, Settings::default_file_contents())?; + + Ok(()) +} + +/// Handle the `edit` command +fn handle_edit_command() -> Result<()> { + let file_path = get_settings_file_path(); + ensure_settings_file_exists(&file_path)?; + + // Allow user to edit in text editor + println!("Opening settings file for editing: {}", file_path.display()); + edit::edit_file(&file_path)?; + + Ok(()) +} + +/// Handle the `delete` command +fn handle_delete_command() -> Result<()> { + let file_path = get_settings_file_path(); + if file_path.exists() { + fs::remove_file(&file_path) + .with_context(|| format!("Error deleting file: {}", file_path.display()))?; + println!("Deleted settings file: {}", file_path.display()); + } else { + eprintln!("No settings file to delete"); + } + + Ok(()) +} + +/// Handle the `path` command +fn handle_path_command() { + let file_path = get_settings_file_path(); + if file_path.is_file() { + println!("{}", file_path.display()); + } else { + eprintln!("Settings file not found at: {}", file_path.display()); + } +} + +/// Handle the `show` command +fn handle_show_command() -> Result<()> { + let file_path = get_settings_file_path(); + + match fs::read(&file_path) { + // Write contents of file to stdout + Ok(ref contents) => io::stdout().write_all(contents)?, + Err(err) => { + if err.kind() == io::ErrorKind::NotFound { + bail!("Settings file not found at: {}", file_path.display()) + } + // Some other kind of IO error occurred; just return it + Err(err)?; + } + } + + Ok(()) +} + +/// Handle the `show-default` command +fn handle_show_default_command() { + print!("{}", Settings::default_file_contents()); +} diff --git a/src/lib.rs b/src/lib.rs index 0b42d1a88..4c432935f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,9 @@ //! Common functionality for MUSE 2.0. #![warn(missing_docs)] + +use dirs::config_dir; +use std::path::PathBuf; + pub mod agent; pub mod asset; pub mod cli; @@ -21,3 +25,15 @@ pub mod year; #[cfg(test)] mod fixture; + +/// Get config dir for program. +/// +/// In the unlikely event this path cannot be retrieved, the CWD will be returned. +pub fn get_muse2_config_dir() -> PathBuf { + let Some(mut config_dir) = config_dir() else { + return PathBuf::default(); + }; + + config_dir.push("muse2"); + config_dir +} diff --git a/src/settings.rs b/src/settings.rs index 1df46a849..e2ac261ed 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,18 +1,38 @@ //! Code for loading program settings. +use crate::get_muse2_config_dir; use crate::input::read_toml; use crate::log::DEFAULT_LOG_LEVEL; use anyhow::Result; use documented::DocumentedFields; use serde::{Deserialize, Serialize}; use std::fmt::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; const SETTINGS_FILE_NAME: &str = "settings.toml"; -const DEFAULT_SETTINGS_FILE_HEADER: &str = "# This file contains the program settings for MUSE 2.0 -# For more information, visit: +const DEFAULT_SETTINGS_FILE_HEADER: &str = concat!( + "# This file contains the program settings for MUSE 2.0. +# +# The default options for MUSE2 v", + env!("CARGO_PKG_VERSION"), + " are shown below, commented out. To change an option, uncomment it and set the value +# appropriately. +# +# To show the default options for the current version of MUSE2, run: +# \tmuse2 settings show-default +# +# For information about the possible settings, visit: # \thttps://energysystemsmodellinglab.github.io/MUSE_2.0/file_formats/program_settings.html -"; +" +); + +/// Get the path to where the settings file will be read from +pub fn get_settings_file_path() -> PathBuf { + let mut path = get_muse2_config_dir(); + path.push(SETTINGS_FILE_NAME); + + path +} /// Program settings from config file /// @@ -44,15 +64,15 @@ impl Settings { /// /// If the file is not present, default values for settings will be used /// - /// # Arguments - /// - /// * `model_dir` - Folder containing model configuration files - /// /// # Returns /// /// The program settings as a `Settings` struct or an error if the file is invalid pub fn load() -> Result { - let file_path = Path::new(SETTINGS_FILE_NAME); + Self::load_from_path(&get_settings_file_path()) + } + + /// Read from the specified path, returning + fn load_from_path(file_path: &Path) -> Result { if !file_path.is_file() { return Ok(Settings::default()); } @@ -92,32 +112,32 @@ impl Settings { #[cfg(test)] mod tests { use super::*; - use current_dir::Cwd; use std::fs::File; use std::io::Write; use tempfile::tempdir; #[test] - fn test_settings_from_path_no_file() { + fn test_settings_load_from_path_no_file() { let dir = tempdir().unwrap(); - let mut cwd = Cwd::mutex().lock().unwrap(); - cwd.set(dir.path()).unwrap(); - assert_eq!(Settings::load().unwrap(), Settings::default()); + let file_path = dir.path().join(SETTINGS_FILE_NAME); // NB: doesn't exist + assert_eq!( + Settings::load_from_path(&file_path).unwrap(), + Settings::default() + ); } #[test] - fn test_settings_from_path() { + fn test_settings_load_from_path() { let dir = tempdir().unwrap(); - let mut cwd = Cwd::mutex().lock().unwrap(); - cwd.set(dir.path()).unwrap(); + let file_path = dir.path().join(SETTINGS_FILE_NAME); { - let mut file = File::create(Path::new(SETTINGS_FILE_NAME)).unwrap(); + let mut file = File::create(&file_path).unwrap(); writeln!(file, "log_level = \"warn\"").unwrap(); } assert_eq!( - Settings::load().unwrap(), + Settings::load_from_path(&file_path).unwrap(), Settings { log_level: "warn".to_string(), debug_model: false,