use std::env;
use std::process;
use crate::file_operations;
use crate::open;
use crate::plot;
use crate::Network;
pub struct Config {}
impl Config {
pub fn run(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 2 {
return Err("not enough arguments");
}
if args.len() > 2 && args[1] != "cascade" {
return Err("too many arguments, expecting only 2, such as `touchstone filepath`");
}
match args[1].as_str() {
"--version" | "-v" => {
print_version();
process::exit(0);
}
"--help" | "-h" => {
print_help();
process::exit(0);
}
"cascade" => {
let mut output_name: Option<String> = None;
let mut file_paths = Vec::new();
let mut i = 2;
while i < args.len() {
match args[i].as_str() {
"--name" | "-n" => {
if i + 1 < args.len() {
output_name = Some(args[i + 1].clone());
i += 2;
} else {
return Err("missing argument for --name");
}
}
_ => {
file_paths.push(args[i].clone());
i += 1;
}
}
}
if file_paths.len() < 2 {
return Err(
"cascade requires at least 2 files, e.g. `touchstone cascade file1 file2`",
);
}
let mut networks = Vec::new();
for path in file_paths.iter() {
networks.push(Network::new(path.clone()));
}
let mut result = networks[0].clone();
for network in networks.iter().skip(1) {
result = result * network.clone();
}
let output_s2p_path = if let Some(name) = output_name {
name
} else {
let first_file = &file_paths[0];
let path = std::path::Path::new(first_file);
let parent = path.parent().unwrap_or(std::path::Path::new("."));
parent
.join("cascaded_result.s2p")
.to_string_lossy()
.to_string()
};
if let Err(e) = result.save(&output_s2p_path) {
tracing::error!("Failed to save S2P file: {}", e);
return Err("Failed to save S2P file");
}
tracing::info!("Saved cascaded network to {}", output_s2p_path);
let output_html_path = format!("{}.html", output_s2p_path);
generate_plot(&[result], output_html_path.clone());
open::plot(output_html_path);
return Ok(Config {});
}
_ => {
if args.len() > 2 {
return Err(
"too many arguments, expecting only 2, such as `touchstone filepath`",
);
}
}
}
let file_argument = args[1].clone();
parse_plot_open_in_browser(file_argument.clone());
Ok(Config {})
}
}
pub fn print_version() {
println!("touchstone {}", env!("CARGO_PKG_VERSION"));
}
pub fn print_error(error: &str) {
const RED: &str = "\x1b[31m";
const RESET: &str = "\x1b[0m";
println!("{}Problem parsing arguments: {error}{}", RED, RESET);
}
pub fn print_help() {
const BOLD: &str = "\x1b[1m";
const CYAN: &str = "\x1b[36m";
const GREEN: &str = "\x1b[32m";
const YELLOW: &str = "\x1b[33m";
const RESET: &str = "\x1b[0m";
println!(
"📡 Touchstone (s2p, etc.) file parser, plotter, and more - https://github.com/iancleary/touchstone{}",
RESET
);
println!();
println!("{}{}VERSION:{}", BOLD, YELLOW, RESET);
println!(" {}{}{}", GREEN, env!("CARGO_PKG_VERSION"), RESET);
println!();
println!("{}{}USAGE:{}", BOLD, YELLOW, RESET);
println!(" {} touchstone <FILE_PATH>{}", GREEN, RESET);
println!(" {} touchstone <DIRECTORY_PATH>{}", GREEN, RESET);
println!(
" {} touchstone cascade <FILE_1> <FILE_2> ... [--name <OUTPUT_NAME>]{}",
GREEN, RESET
);
println!();
println!(" FILE_PATH: path to a s2p file");
println!(" DIRECTORY_PATH: path to a directory containing s2p files");
println!();
println!(" The s2p file(s) are parsed and an interactive plot (html file and js/ folder) ");
println!(" is created next to the source file(s).");
println!();
println!("{}{}OPTIONS:{}", BOLD, YELLOW, RESET);
println!(
" {} -v, --version{}{} Print version information",
GREEN, RESET, RESET
);
println!(
" {} -h, --help{}{} Print help information",
GREEN, RESET, RESET
);
println!();
println!("{}{}EXAMPLES:{}", BOLD, YELLOW, RESET);
println!(" {} # Single file (Relative path){}", CYAN, RESET);
println!(" {} touchstone files/measurements.s2p{}", GREEN, RESET);
println!();
println!(
" {} # Directory (Plot all files in folder){}",
CYAN, RESET
);
println!(" {} touchstone files/data_folder{}", GREEN, RESET);
println!();
println!(" {} # Cascade two networks{}", CYAN, RESET);
println!(
" {} touchstone cascade ntwk1.s2p ntwk2.s2p{}",
GREEN, RESET
);
println!();
println!(" {} # Cascade with custom output name{}", CYAN, RESET);
println!(
" {} touchstone cascade ntwk1.s2p ntwk2.s2p --name result.s2p{}",
GREEN, RESET
);
println!();
println!(" {} # Bare filename{}", CYAN, RESET);
println!(" {} touchstone measurement.s2p{}", GREEN, RESET);
println!();
println!(" {} # Windows absolute path{}", CYAN, RESET);
println!(
" {} touchstone C:\\Users\\data\\measurements.s2p{}",
GREEN, RESET
);
println!();
println!(" {} # Windows UNC path (network path){}", CYAN, RESET);
println!(
" {} touchstone \\\\server\\mount\\folder\\measurement.s2p{}",
GREEN, RESET
);
println!();
println!(" {} # Unix absolute path{}", CYAN, RESET);
println!(
" {} touchstone /home/user/measurements.s2p{}",
GREEN, RESET
);
}
fn generate_plot(networks: &[Network], file_path_plot: String) {
plot::generate_plot_from_networks(networks, &file_path_plot).unwrap();
tracing::info!("Plot HTML generated at {}", file_path_plot);
}
fn parse_plot_open_in_browser(file_path: String) {
let path = std::path::Path::new(&file_path);
let mut networks = Vec::new();
if path.is_dir() {
tracing::info!("Directory detected: {}", file_path);
tracing::debug!("Plotting all valid network files in directory");
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(extension) = path.extension() {
let ext_str = extension.to_string_lossy().to_lowercase();
if ext_str.starts_with('s') && ext_str.ends_with('p') && ext_str.len() == 3
{
tracing::debug!("Found network file: {:?}", path);
networks.push(Network::new(path.to_string_lossy().to_string()));
}
}
}
}
}
if networks.is_empty() {
tracing::warn!("No valid network files found in directory.");
}
let output_html_path = path
.join("combined_plot.html")
.to_string_lossy()
.to_string();
generate_plot(&networks, output_html_path.clone());
open::plot(output_html_path.clone());
} else {
tracing::info!("Single file detected: {}", file_path);
let s2p = Network::new(file_path.clone());
networks.push(s2p);
let file_path_config: file_operations::FilePathConfig =
file_operations::get_file_path_config(&file_path);
let output_html_path = if file_path_config.absolute_path {
let mut file_path_html = format!("{}.html", &file_path);
if cfg!(target_os = "windows") && file_path_html.starts_with(r"\\?\") {
file_path_html = file_path_html[4..].to_string();
}
file_path_html
} else if file_path_config.relative_path_with_separators {
format!("{}.html", &file_path)
} else if file_path_config.bare_filename {
format!("./{}.html", &file_path)
} else {
panic!(
"file_path_config must have one true value: {:?}",
file_path_config
);
};
generate_plot(&networks, output_html_path.clone());
open::plot(output_html_path.clone());
}
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
use std::path::PathBuf;
fn setup_test_dir(name: &str) -> PathBuf {
let mut path = std::env::temp_dir();
path.push("touchstone_tests");
path.push(name);
path.push(format!(
"{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&path).unwrap();
path
}
#[test]
fn test_config_build() {
let test_dir = setup_test_dir("test_config_build");
let s2p_path = test_dir.join("test_cli_config_build.s2p");
fs::copy("files/test_cli/test_cli_config_build.s2p", &s2p_path).unwrap();
let args = vec![
String::from("program_name"),
s2p_path.to_str().unwrap().to_string(),
];
let _cli_run = Config::run(&args).unwrap();
}
#[test]
fn test_config_build_not_enough_args() {
let args = vec![String::from("program_name")];
let result = Config::run(&args);
assert!(result.is_err());
}
#[test]
fn test_help_flag() {
let help_flags = vec!["--help", "-h"];
for flag in help_flags {
assert!(flag == "--help" || flag == "-h");
}
}
#[test]
fn test_version_flag() {
let version_flags = vec!["--version", "-v"];
for flag in version_flags {
assert!(flag == "--version" || flag == "-v");
}
}
#[test]
fn test_version_output_format() {
let version = env!("CARGO_PKG_VERSION");
assert!(!version.is_empty());
let parts: Vec<&str> = version.split('.').collect();
assert_eq!(parts.len(), 3, "Version should be in X.Y.Z format");
}
#[test]
fn test_run_function() {
let test_dir_rel = setup_test_dir("test_run_function_rel");
let s2p_path_rel = test_dir_rel.join("test_cli_run_relative_path.s2p");
fs::copy(
"files/test_cli/test_cli_run_relative_path.s2p",
&s2p_path_rel,
)
.unwrap();
parse_plot_open_in_browser(s2p_path_rel.to_str().unwrap().to_string());
assert!(s2p_path_rel.with_extension("s2p.html").exists());
assert!(test_dir_rel.join("js").exists());
let _bare_filename_copy = fs::copy(
"files/test_cli/test_cli_run_bare_filename.s2p",
"test_cli_run_bare_filename.s2p",
);
let bare_filename = String::from("test_cli_run_bare_filename.s2p");
parse_plot_open_in_browser(bare_filename);
let _bare_filename_remove_file_s2p = fs::remove_file("test_cli_run_bare_filename.s2p");
let _bare_filename_remove_file_html =
fs::remove_file("test_cli_run_bare_filename.s2p.html");
let _bare_filename_remove_dir = fs::remove_dir_all("js");
let test_dir_abs = setup_test_dir("test_run_function_abs");
let s2p_path_abs = test_dir_abs.join("test_cli_run_absolute_path.s2p");
fs::copy(
"files/test_cli/test_cli_run_absolute_path.s2p",
&s2p_path_abs,
)
.unwrap();
let path_buf = std::fs::canonicalize(&s2p_path_abs).unwrap();
let absolute_path: String = path_buf.to_string_lossy().to_string();
parse_plot_open_in_browser(absolute_path);
assert!(s2p_path_abs.with_extension("s2p.html").exists());
assert!(test_dir_abs.join("js").exists());
}
#[test]
fn test_cascade_command() {
let test_dir = setup_test_dir("test_cascade_command");
let s2p1 = test_dir.join("ntwk1.s2p");
let s2p2 = test_dir.join("ntwk2.s2p");
fs::copy("files/ntwk1.s2p", &s2p1).unwrap();
fs::copy("files/ntwk2.s2p", &s2p2).unwrap();
let args = vec![
String::from("program_name"),
String::from("cascade"),
s2p1.to_str().unwrap().to_string(),
s2p2.to_str().unwrap().to_string(),
];
let _cli_run = Config::run(&args).unwrap();
let expected_output_s2p = test_dir.join("cascaded_result.s2p");
let expected_output_html = test_dir.join("cascaded_result.s2p.html");
assert!(expected_output_s2p.exists());
assert!(expected_output_html.exists());
assert!(test_dir.join("js").exists());
let output_name = test_dir.join("custom_output.s2p");
let args_named = vec![
String::from("program_name"),
String::from("cascade"),
s2p1.to_str().unwrap().to_string(),
s2p2.to_str().unwrap().to_string(),
String::from("--name"),
output_name.to_str().unwrap().to_string(),
];
let _cli_run_named = Config::run(&args_named).unwrap();
assert!(output_name.exists());
assert!(output_name.with_extension("s2p.html").exists());
}
#[test]
fn test_cascade_not_enough_args() {
let args = vec![
String::from("program_name"),
String::from("cascade"),
String::from("file1"),
];
let result = Config::run(&args);
assert!(result.is_err());
}
#[test]
fn test_directory_plot() {
let test_dir = setup_test_dir("test_directory_plot");
let s2p1 = test_dir.join("ntwk1.s2p");
let s2p2 = test_dir.join("ntwk2.s2p");
let txt_file = test_dir.join("readme.txt");
fs::copy("files/ntwk1.s2p", &s2p1).unwrap();
fs::copy("files/ntwk2.s2p", &s2p2).unwrap();
fs::write(&txt_file, "ignore me").unwrap();
parse_plot_open_in_browser(test_dir.to_str().unwrap().to_string());
let expected_output = test_dir.join("combined_plot.html");
assert!(expected_output.exists());
assert!(test_dir.join("js").exists());
let content = fs::read_to_string(&expected_output).unwrap();
assert!(content.contains("ntwk1.s2p"));
assert!(content.contains("ntwk2.s2p"));
}
}