h5inspect 1.3.3

A terminal based HDF5 file inspector
use crate::app::App;
use clap;
use color_eyre::Result;
use serde_json;
use std::error::Error;

use ratatui;
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>> {
    // crate::h5_utils::generate_dummy_file()?;
    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(true),
        )
        .arg(
            clap::Arg::new("analyze")
                .long("analyze")
                .help("Run analysis on a specific dataset and output JSON result")
                .action(clap::ArgAction::SetTrue),
        )
        .arg(
            clap::Arg::new("dataset")
                .value_name("DATASET")
                .help("Dataset path to analyze (required when using --analyze)")
                .required(false),
        )
        .version(env!("CARGO_PKG_VERSION"))
        .get_matches();

    let h5_file_name: &String = matches.get_one("h5file").expect("h5file is required");
    let h5_file_path = std::path::PathBuf::from(h5_file_name);

    // Check if we're in analysis mode
    if matches.get_flag("analyze") {
        let dataset_path: Option<&String> = matches.get_one("dataset");

        if dataset_path.is_none() {
            eprintln!("Error: --analyze requires a dataset path as the second argument");
            std::process::exit(1);
        }

        let dataset_path = dataset_path.unwrap();

        // Run analysis and output JSON
        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);
            }
        }
    }

    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);
    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 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
    }
}