qsym2/interfaces/cli/
mod.rs

1//! Command-line interface for QSym².
2
3use std::fmt;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use clap::{Parser, Subcommand};
8use lazy_static::lazy_static;
9#[cfg(feature = "python")]
10use pyo3::prelude::*;
11use regex::Regex;
12
13use crate::auxiliary::contributors::CONTRIBUTORS;
14use crate::io::format::{log_subtitle, log_title, qsym2_output, QSym2Output};
15
16/// The current version of QSym².
17const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
18
19// =======
20// Structs
21// =======
22
23/// Enumerated type for subcommands.
24#[derive(Subcommand, Debug)]
25pub enum Commands {
26    /// Generates a template YAML configuration file and exits.
27    Template {
28        /// The name for the generated template YAML configuration file.
29        #[arg(short, long)]
30        name: Option<PathBuf>,
31    },
32
33    /// Runs an analysis calculation and exits.
34    Run {
35        /// The configuration YAML file specifying parameters for the calculation.
36        #[arg(short, long, required = true)]
37        config: PathBuf,
38
39        /// The output filename.
40        #[arg(short, long, required = true)]
41        output: PathBuf,
42
43        /// Turn debugging information on.
44        #[arg(short, long, action = clap::ArgAction::Count)]
45        debug: u8,
46    },
47}
48
49/// Structure to handle command-line interface parsing.
50#[derive(Parser, Debug)]
51#[command(author, version, about)]
52#[command(next_line_help = true)]
53pub struct Cli {
54    /// Subcommands.
55    #[command(subcommand)]
56    pub command: Commands,
57}
58
59impl fmt::Display for Cli {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match &self.command {
62            Commands::Template { name } => {
63                writeln!(
64                    f,
65                    "Generate a template configuration YAML file: {}",
66                    name.as_ref()
67                        .map(|name| name.display().to_string())
68                        .unwrap_or("no name specified".to_string())
69                )
70            }
71            Commands::Run {
72                config,
73                output,
74                debug,
75            } => {
76                writeln!(f, "{:<11}: {}", "Config file", config.display().to_string())?;
77                writeln!(f, "{:<11}: {}", "Output file", output.display().to_string())?;
78                writeln!(f, "{:<11}: {}", "Debug level", debug)?;
79                Ok(())
80            }
81        }
82    }
83}
84
85// =========
86// Functions
87// =========
88
89/// Outputs a nicely formatted QSym2 heading to the `qsym2-output` logger.
90#[cfg_attr(feature = "python", pyfunction)]
91pub fn qsym2_output_heading() {
92    let version = if let Some(ver) = VERSION {
93        format!("v{ver}")
94    } else {
95        format!("v unknown")
96    };
97    // Banner length: 103
98    qsym2_output!("╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮");
99    qsym2_output!("│                                                                                 222222222222222     │");
100    qsym2_output!("│                                                                                2:::::::::::::::22   │");
101    qsym2_output!("│                                                                                2::::::222222:::::2  │");
102    qsym2_output!("│                                                                                2222222     2:::::2  │");
103    qsym2_output!("│                                                                                            2:::::2  │");
104    qsym2_output!("│      QQQQQQQQQ        SSSSSSSSSSSSSSS                                                      2:::::2  │");
105    qsym2_output!("│    QQ:::::::::QQ    SS:::::::::::::::S                                                  2222::::2   │");
106    qsym2_output!("│  QQ:::::::::::::QQ S:::::SSSSSS::::::S                                             22222::::::22    │");
107    qsym2_output!("│ Q:::::::QQQ:::::::QS:::::S     SSSSSSS                                           22::::::::222      │");
108    qsym2_output!("│ Q::::::O   Q::::::QS:::::S      yyyyyyy           yyyyyyy mmmmmmm    mmmmmmm    2:::::22222         │");
109    qsym2_output!("│ Q:::::O     Q:::::QS:::::S       y:::::y         y:::::ymm:::::::m  m:::::::mm 2:::::2              │");
110    qsym2_output!("│ Q:::::O     Q:::::Q S::::SSSS     y:::::y       y:::::ym::::::::::mm::::::::::m2:::::2              │");
111    qsym2_output!("│ Q:::::O     Q:::::Q  SS::::::SSSSS y:::::y     y:::::y m::::::::::::::::::::::m2:::::2       222222 │");
112    qsym2_output!("│ Q:::::O     Q:::::Q    SSS::::::::SSy:::::y   y:::::y  m:::::mmm::::::mmm:::::m2::::::2222222:::::2 │");
113    qsym2_output!("│ Q:::::O     Q:::::Q       SSSSSS::::Sy:::::y y:::::y   m::::m   m::::m   m::::m2::::::::::::::::::2 │");
114    qsym2_output!("│ Q:::::O  QQQQ:::::Q            S:::::Sy:::::y:::::y    m::::m   m::::m   m::::m22222222222222222222 │");
115    qsym2_output!("│ Q::::::O Q::::::::Q            S:::::S y:::::::::y     m::::m   m::::m   m::::m                     │");
116    qsym2_output!("│ Q:::::::QQ::::::::QSSSSSSS     S:::::S  y:::::::y      m::::m   m::::m   m::::m                     │");
117    qsym2_output!("│  QQ::::::::::::::Q S::::::SSSSSS:::::S   y:::::y       m::::m   m::::m   m::::m                     │");
118    qsym2_output!("│    QQ:::::::::::Q  S:::::::::::::::SS   y:::::y        m::::m   m::::m   m::::m                     │");
119    qsym2_output!("│      QQQQQQQQ::::QQ SSSSSSSSSSSSSSS    y:::::y         mmmmmm   mmmmmm   mmmmmm                     │");
120    qsym2_output!("│              Q:::::Q                  y:::::y                                                       │");
121    qsym2_output!("│               QQQQQQ                 y:::::y                A program for Quantum Symbolic Symmetry │");
122    qsym2_output!("│                                     y:::::y                                                         │");
123    qsym2_output!("│                                    y:::::y                                     {version:>13} (2025) │");
124    qsym2_output!("│                                   yyyyyyy                                     Author: Bang C. Huynh │");
125    qsym2_output!("╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯");
126    qsym2_output!("       developed with support from the ERC's topDFT project at the University of Nottingham, UK");
127    qsym2_output!("");
128    qsym2_output!("          If you find QSym² helpful in your research, please support us by citing our paper:");
129    qsym2_output!("                         Huynh, B. C., Wibowo-Teale, M. & Wibowo-Teale, A. M.");
130    qsym2_output!("                              *J. Chem. Theory Comput.* **20**, 114–133.");
131    qsym2_output!("                                 doi:10.1021/acs.jctc.3c01118 (2024)");
132    qsym2_output!("");
133}
134
135lazy_static! {
136    /// Regular expression pattern for lines commented out with `#`.
137    static ref COMMENT_RE: Regex = Regex::new(r"^\s*#.*?").expect("Regex pattern invalid.");
138}
139
140/// Outputs a nicely formatted list of contributors.
141#[cfg_attr(feature = "python", pyfunction)]
142pub fn qsym2_output_contributors() {
143    qsym2_output!("    Contributors (in alphabetical order):");
144    CONTRIBUTORS.iter().for_each(|contrib| {
145        qsym2_output!("        {}", contrib.trim());
146    });
147    qsym2_output!("");
148}
149
150/// Outputs a summary of the calculation.
151///
152/// # Arguments
153///
154/// * `config_path` - The path of the configuration YAML file defining the calculation parameters.
155/// * `cli` - The parsed command-line arguments.
156pub fn qsym2_output_calculation_summary<P: AsRef<Path>>(config_path: P, cli: &Cli) {
157    log_title("Calculation Summary");
158    qsym2_output!("");
159
160    log_subtitle("Command line arguments");
161    cli.log_output_display();
162    qsym2_output!("");
163
164    log_subtitle("Input YAML configuration file");
165    let config_contents =
166        fs::read_to_string(&config_path).expect("Input configuration YAML file could not be read.");
167
168    qsym2_output!("File path: {}", config_path.as_ref().display());
169    let filtered_config_contents = config_contents
170        .lines()
171        .filter_map(|line| {
172            if COMMENT_RE.is_match(line) {
173                None
174            } else {
175                Some(line.trim_end().to_string())
176            }
177        })
178        .collect::<Vec<_>>();
179    let length = filtered_config_contents
180        .iter()
181        .map(|line| line.chars().count())
182        .max()
183        .unwrap_or(20);
184    let formatted_config_contents = itertools::intersperse(
185        filtered_config_contents
186            .iter()
187            .map(|line| format!("┊ {line:<length$} ┊")),
188        "\n".to_string(),
189    )
190    .collect::<String>();
191    qsym2_output!("┌{}┐", "┄".repeat(length + 2));
192    formatted_config_contents.trim().log_output_display();
193    qsym2_output!("└{}┘", "┄".repeat(length + 2));
194    qsym2_output!("");
195    qsym2_output!("");
196}