use crate::app::App;
use clap;
use color_eyre::Result;
use ratatui;
use serde_json;
use std::error::Error;
use std::path::PathBuf;
use tui_logger;
mod analysis;
mod app;
mod events;
mod h5_utils;
mod hist_plot;
mod num_utils;
mod tree;
mod ui;
fn main() -> Result<(), Box<dyn Error>> {
let matches = clap::Command::new("h5inspect")
.author("Hal Frigaard")
.about("Simple TUI to inspect h5 files")
.arg(
clap::Arg::new("h5file")
.value_name("FILE")
.help("Name of hdf5 file to inspect")
.value_hint(clap::ValueHint::FilePath)
.required(false),
)
.arg(
clap::Arg::new("analyze-dataset")
.long("analyze-dataset")
.value_name("DATASET")
.help("Run analysis on a specific dataset and output JSON result")
.required(false),
)
.arg(
clap::Arg::new("logs")
.long("logs")
.value_name("FILE")
.help("File path to print logs to")
.value_hint(clap::ValueHint::FilePath)
.required(false),
)
.arg(
clap::Arg::new("generate-dummy-file")
.long("generate-dummy-file")
.help("Generate a dummy hdf5 file for testing purposes")
.action(clap::ArgAction::SetTrue),
)
.version(env!("CARGO_PKG_VERSION"))
.get_matches();
if matches.get_flag("generate-dummy-file") {
h5_utils::generate_dummy_file()?;
return Ok(());
}
let h5_file_name: &String = matches
.get_one("h5file")
.expect("h5file is required unless using --generate-dummy-file");
let h5_file_path = std::path::PathBuf::from(h5_file_name);
initialize_logger(matches.get_one::<String>("logs"))?;
if let Some(dataset_path) = matches.get_one::<String>("analyze-dataset") {
analyze_dataset(&h5_file_path, dataset_path)
} else {
log::info!("Starting app");
let app = App::new(h5_file_path);
let runtime = build_runtime();
color_eyre::install()?;
let terminal = ratatui::init();
crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture)?;
let res = runtime.block_on(app.run(terminal));
crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture)?;
ratatui::restore();
if let Ok(ref finishing_state) = res {
if let app::AppFinishingState::ShouldRunCommand(post_cmd, ds_path) = finishing_state {
println!(
"H5INSPECT_POST running: {} {} {}",
post_cmd, h5_file_name, ds_path
);
let mut child = std::process::Command::new(post_cmd)
.arg(h5_file_name)
.arg(ds_path)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.spawn()?;
let status = child.wait()?;
if !status.success() {
eprintln!("H5INSPECT_POST script exited with status: {}", status);
}
}
}
res.map(|_| ())
}
}
fn initialize_logger(log_file_path: Option<&String>) -> Result<(), Box<dyn Error>> {
tui_logger::init_logger(log::LevelFilter::Trace)?;
tui_logger::set_default_level(log::LevelFilter::Trace);
tui_logger::set_level_for_target("plotters_ratatui_backend::widget", log::LevelFilter::Off);
tui_logger::set_level_for_target("mio::poll", log::LevelFilter::Off);
if let Some(log_file_path) = log_file_path {
tui_logger::set_log_file(tui_logger::TuiLoggerFile::new(log_file_path));
}
Ok(())
}
fn analyze_dataset(h5_file_path: &PathBuf, dataset_path: &str) -> Result<(), Box<dyn Error>> {
match analysis::hdf5_dataset_analysis_from_path(&h5_file_path, dataset_path) {
Ok(result) => {
let json_output = serde_json::to_string(&result)?;
println!("{}", json_output);
return Ok(());
}
Err(e) => {
let error_result = analysis::AnalysisResult::Failed(e.to_string());
let json_output = serde_json::to_string(&error_result)?;
println!("{}", json_output);
std::process::exit(1);
}
}
}
fn build_runtime() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.max_blocking_threads(512)
.enable_all()
.build()
.unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_app_startup() -> Result<(), Box<dyn Error>> {
h5_utils::generate_dummy_file()?;
let h5_file_path = std::path::PathBuf::from("dummy.h5");
run_app(h5_file_path)
}
#[test]
#[should_panic(expected = "File path doesn't exist")]
fn run_app_on_non_existent_file() {
let h5_file_path = std::path::PathBuf::from("non_existent.h5");
run_app(h5_file_path).unwrap();
}
#[test]
#[should_panic(expected = "Couldn't open file")]
fn run_app_on_non_h5_file() {
let h5_file_path = std::path::PathBuf::from("src/main.rs");
run_app(h5_file_path).unwrap();
}
#[test]
fn run_app_on_split_file() -> Result<(), Box<dyn Error>> {
h5_utils::generate_dummy_split_file()?;
let h5_file_path = std::path::PathBuf::from("dummy_split.h5");
run_app(h5_file_path)
}
fn run_app(h5_file_path: std::path::PathBuf) -> Result<(), Box<dyn Error>> {
let app = App::new(h5_file_path);
let backend = ratatui::backend::TestBackend::new(200, 120);
let terminal = ratatui::Terminal::new(backend).unwrap();
let runtime = build_runtime();
let res = runtime.block_on(async {
tokio::select! {
res = app.run(terminal) => {
res.map(|_| ())
}
_ = tokio::time::sleep(std::time::Duration::from_secs(2)) => {
println!("Timer expired before app returned, nice.");
Ok(())
}
}
});
res
}
}