muse2/
commands.rs

1//! The command line interface for the simulation.
2use crate::input::load_model;
3use crate::log;
4use crate::output::{create_output_directory, get_output_dir};
5use crate::settings::Settings;
6use ::log::{error, info};
7use anyhow::{ensure, Context, Result};
8use clap::{Parser, Subcommand};
9use include_dir::{include_dir, Dir, DirEntry};
10use std::fs;
11use std::path::{Path, PathBuf};
12use tempfile::TempDir;
13
14/// The directory containing the example models.
15pub const EXAMPLES_DIR: Dir = include_dir!("examples");
16
17/// The command line interface for the simulation.
18#[derive(Parser)]
19#[command(version, about)]
20pub struct Cli {
21    /// The available commands.
22    #[command(subcommand)]
23    pub command: Commands,
24}
25
26/// The available commands.
27#[derive(Subcommand)]
28pub enum Commands {
29    /// Run a simulation model.
30    Run {
31        /// Path to the model directory.
32        model_dir: PathBuf,
33        /// Directory for output files
34        #[arg(short, long)]
35        output_dir: Option<PathBuf>,
36    },
37    /// Manage example models.
38    Example {
39        /// The available subcommands for managing example models.
40        #[command(subcommand)]
41        subcommand: ExampleSubcommands,
42    },
43}
44
45/// The available subcommands for managing example models.
46#[derive(Subcommand)]
47pub enum ExampleSubcommands {
48    /// List available examples.
49    List,
50    /// Extract an example model configuration to a new directory.
51    Extract {
52        /// The name of the example to extract.
53        name: String,
54        /// The destination folder for the example.
55        new_path: Option<PathBuf>,
56    },
57    /// Run an example.
58    Run {
59        /// The name of the example to run.
60        name: String,
61        /// Directory for output files
62        #[arg(short, long)]
63        output_dir: Option<PathBuf>,
64    },
65}
66
67/// Handle the `run` command.
68pub fn handle_run_command(model_path: &Path, output_path: Option<&Path>) -> Result<()> {
69    // Load program settings
70    let settings = Settings::from_path(model_path).context("Failed to load settings.")?;
71
72    // Create output folder
73    let output_path = match output_path {
74        Some(p) => p.to_owned(),
75        None => get_output_dir(model_path)?,
76    };
77    create_output_directory(&output_path).context("Failed to create output directory.")?;
78
79    // Initialise program logger
80    log::init(settings.log_level.as_deref(), &output_path)
81        .context("Failed to initialise logging.")?;
82
83    let load_and_run_model = || {
84        // Load the model to run
85        let (model, assets) = load_model(model_path).context("Failed to load model.")?;
86        info!("Loaded model from {}", model_path.display());
87        info!("Output data will be written to {}", output_path.display());
88
89        // Run the simulation
90        crate::simulation::run(model, assets, &output_path)
91    };
92
93    // Once the logger is initialised, we can write fatal errors to log
94    if let Err(err) = load_and_run_model() {
95        error!("{err:?}");
96    }
97
98    Ok(())
99}
100
101/// Handle the `example list` command.
102pub fn handle_example_list_command() {
103    for entry in EXAMPLES_DIR.dirs() {
104        println!("{}", entry.path().display());
105    }
106}
107
108/// Handle the `example extract` command
109pub fn handle_example_extract_command(name: &str, dest: Option<&Path>) -> Result<()> {
110    let dest = dest.unwrap_or(Path::new(name));
111    extract_example(name, dest)
112}
113
114/// Extract the specified example to a new directory
115fn extract_example(name: &str, new_path: &Path) -> Result<()> {
116    // Find the subdirectory in EXAMPLES_DIR whose name matches `name`.
117    let sub_dir = EXAMPLES_DIR.get_dir(name).context("Example not found.")?;
118
119    ensure!(
120        !new_path.exists(),
121        "Destination directory {} already exists",
122        new_path.display()
123    );
124
125    // Copy the contents of the subdirectory to the destination
126    fs::create_dir(new_path)?;
127    for entry in sub_dir.entries() {
128        match entry {
129            DirEntry::Dir(_) => panic!("Subdirectories in examples not supported"),
130            DirEntry::File(f) => {
131                let file_name = f.path().file_name().unwrap();
132                let file_path = new_path.join(file_name);
133                fs::write(&file_path, f.contents())?;
134            }
135        }
136    }
137
138    Ok(())
139}
140
141/// Handle the `example run` command.
142pub fn handle_example_run_command(name: &str, output_path: Option<&Path>) -> Result<()> {
143    let temp_dir = TempDir::new().context("Failed to create temporary directory.")?;
144    let model_path = temp_dir.path().join(name);
145    extract_example(name, &model_path)?;
146    handle_run_command(&model_path, output_path)
147}