use crate::analysis;
use crate::analysis::AnalysisResult;
use crate::events;
use crate::h5_utils;
use crate::num_utils;
use crate::tree::TreeNode;
use crate::ui::ui;
use crossterm::event::{MouseButton, MouseEventKind};
use hdf5_metno as hdf5;
use std::io::{stdout, Write};
use std::time::Instant;
use chrono::{DateTime, Local};
use core::panic;
use crossterm::event::{KeyCode, KeyModifiers};
use dirs;
use log;
use nix::sys::wait::waitpid;
use nix::unistd::{fork, ForkResult};
use ratatui::layout::{Position, Rect};
use std::collections::HashMap;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::vec;
use tokio;
use tokio::sync::Semaphore;
#[derive(Debug, Clone)]
pub enum Hdf5Object {
Group(hdf5::Group),
Dataset(Arc<hdf5::Dataset>),
}
impl PartialEq for Hdf5Object {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self, other)
}
}
#[derive(Debug)]
enum AsyncDataAnalysis {
Loading,
Ready(analysis::AnalysisResult),
}
pub type NodeIdT = hdf5_metno_sys::h5i::hid_t;
#[derive(Debug, PartialEq)]
pub enum AppFinishingState {
Continue,
Quit,
}
pub enum KeyPressResult {
Redraw,
DontRedraw,
RunPostCommand(Option<String>, String),
}
pub struct App {
running: AppFinishingState,
pub h5_file_path: PathBuf,
pub tree_state: tui_tree_widget::TreeState<NodeIdT>,
pub tree_state_last_rendered_selected: Option<Vec<NodeIdT>>,
pub tree: Option<TreeNode<NodeIdT>>,
pub filtered_tree: Option<TreeNode<NodeIdT>>,
pub search_query_left: String,
pub search_query_right: String,
pub search_query_view_offset: u16,
pub mode: SelectionMode,
pub show_logs: bool,
pub object_info_scroll_state: u16,
last_object_info_area: Rect,
pub last_object_info_table_area: Rect,
pub object_info_row_keys: Vec<String>,
last_tree_area: Rect,
last_search_query_area: Rect,
last_help_screen_area: Rect,
pub animation_state: u8,
node_id_to_analysis: Arc<Mutex<HashMap<NodeIdT, AsyncDataAnalysis>>>,
pub help_screen_scroll_state: u16,
process_semaphore: Arc<Semaphore>,
pub last_time_had_analysis_tasks: Option<std::time::Instant>,
pub tree_width_percentage: u16,
pub is_dragging_divider: bool,
last_redraw_from_scroll: Instant,
pub hovered_node: Option<Vec<NodeIdT>>,
pub last_click_time: Option<Instant>,
pub last_click_position: Option<Position>,
pub copied_indicator: Option<(Vec<NodeIdT>, std::time::Instant)>,
pub copied_object_info_indicator: Option<(String, std::time::Instant)>,
pub hovered_object_info_key: Option<String>,
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum SelectionMode {
TreeBrowsing,
SearchQueryEditing,
ObjectInfoInspecting,
HelpScreen,
}
fn get_text_for_dataset(tree_node: &TreeNode<NodeIdT>) -> Vec<(String, String)> {
let dataset = match tree_node.hdf5_object.as_ref() {
Some(Hdf5Object::Dataset(dataset)) => dataset,
_ => panic!("Expected a Dataset, found a Group or None"),
};
let shape = dataset.shape();
let datatype: String = dataset
.dtype()
.and_then(|dt| dt.to_descriptor())
.map(|desc| h5_utils::type_descriptor_to_text(desc))
.unwrap_or("unknown".to_string());
let space = dataset
.space()
.map(|s| format!("{:?}", s))
.unwrap_or("unknown".to_string());
let chunks = dataset.chunk();
let chunk_info = match chunks {
Some(chunks) => format!("Chunked ({:?})", chunks),
None => "Contiguous".to_string(),
};
let compression = dataset.filters();
let compression_info = format!("Filter pipeline: {:?}", compression);
let storage_size = dataset.storage_size();
let data_size = dataset.size() * dataset.dtype().map_or(0, |dt| dt.size());
let data_size: u64 = data_size.try_into().unwrap_or(0);
let compression_ratio = if storage_size > 0 {
data_size as f64 / storage_size as f64
} else {
f64::NAN
};
let mut res = vec![];
res.push(("Path".to_string(), dataset.name().to_string()));
res.push(("Shape".to_string(), format!("{:?}", shape)));
res.push(("Space".to_string(), space));
res.push(("Chunk info".to_string(), chunk_info));
res.push(("Compression".to_string(), compression_info));
res.push((
"Storage size".to_string(),
format!(
"{} ({})",
num_utils::file_size_fmt(storage_size),
num_utils::file_size_fmt_no_scale(storage_size)
),
));
res.push((
"Data size".to_string(),
format!(
"{} ({})",
num_utils::file_size_fmt(data_size),
num_utils::file_size_fmt_no_scale(data_size)
),
));
res.push((
"Compression ratio".to_string(),
format!("{:.2}", compression_ratio),
));
res.push(("Datatype".to_string(), datatype));
res
}
fn get_text_for_group(tree_node: &TreeNode<NodeIdT>) -> Vec<(String, String)> {
let group = match tree_node.hdf5_object.as_ref() {
Some(Hdf5Object::Group(group)) => group,
_ => panic!("Expected a Group, found a Dataset or None"),
};
let num_groups = group.groups().unwrap_or(vec![]).len();
let num_datasets = group.datasets().unwrap_or(vec![]).len();
let attrs = group.attr_names().unwrap_or(vec![]);
let num_attrs = attrs.len();
let mut res = vec![];
res.push(("Path".to_string(), group.name().to_string()));
res.push((
"Number of groups direct".to_string(),
num_groups.to_string(),
));
res.push((
"Number of groups total".to_string(),
format!("{}", tree_node.recursive_num_groups),
));
res.push((
"Number of datasets direct".to_string(),
num_datasets.to_string(),
));
res.push((
"Number of datasets total".to_string(),
tree_node.recursive_num_datasets.to_string(),
));
res.push(("Number of attributes".to_string(), num_attrs.to_string()));
res.push(("Attribute names".to_string(), format!("{:?}", attrs)));
res.push((
"Storage size".to_string(),
format!(
"{} ({})",
num_utils::file_size_fmt(tree_node.recursive_storage_data_size),
num_utils::file_size_fmt_no_scale(tree_node.recursive_storage_data_size)
),
));
res
}
impl App {
pub const NUM_ANALYSIS_PERMITS: usize = 64;
pub fn new(h5_file_path: PathBuf) -> App {
let mut starting_mode = SelectionMode::HelpScreen;
if let Some(config_dir) = dirs::config_local_dir() {
let app_config_dir = config_dir.join("h5inspect");
if app_config_dir.exists() {
starting_mode = SelectionMode::TreeBrowsing;
} else {
if let Err(e) = std::fs::create_dir(&app_config_dir) {
log::error!("Failed to create config dir {:?}: {}", app_config_dir, e);
} else {
log::info!("Created config dir {:?}", app_config_dir);
}
}
}
App {
running: AppFinishingState::Continue,
h5_file_path,
tree_state: tui_tree_widget::TreeState::default(),
tree_state_last_rendered_selected: None,
tree: None,
filtered_tree: None,
search_query_left: String::new(),
search_query_right: String::new(),
search_query_view_offset: 0,
mode: starting_mode,
show_logs: false, object_info_scroll_state: 0,
last_object_info_area: Rect::new(0, 0, 0, 0),
last_object_info_table_area: Rect::new(0, 0, 0, 0),
object_info_row_keys: Vec::new(),
last_tree_area: Rect::new(0, 0, 0, 0),
last_search_query_area: Rect::new(0, 0, 0, 0),
last_help_screen_area: Rect::new(0, 0, 0, 0),
animation_state: 0,
node_id_to_analysis: Arc::new(Mutex::new(HashMap::new())),
help_screen_scroll_state: 0,
process_semaphore: Arc::new(Semaphore::new(App::NUM_ANALYSIS_PERMITS)), last_time_had_analysis_tasks: None,
tree_width_percentage: 50,
is_dragging_divider: false,
last_redraw_from_scroll: Instant::now(),
hovered_node: None,
last_click_time: None,
last_click_position: None,
copied_indicator: None,
copied_object_info_indicator: None,
hovered_object_info_key: None,
}
}
fn tree_from_h5(h5_file: &hdf5::File) -> Result<TreeNode<NodeIdT>, std::io::Error> {
fn tree_from_group(group_name: &str, group: hdf5::Group) -> TreeNode<NodeIdT> {
let mut children: Vec<_> = h5_utils::groups(&group)
.unwrap_or(vec![])
.into_iter()
.map(|(name, child)| tree_from_group(&name, child))
.collect();
let datasets = h5_utils::datasets(&group).unwrap_or(vec![]);
for (dataset_name, dataset) in datasets.into_iter() {
let text = dataset_name.clone();
let node_id = dataset.id();
children.push(
TreeNode::new(node_id, text, vec![])
.set_storage_dataset_size(dataset.storage_size())
.set_hdf5_object(Hdf5Object::Dataset(Arc::new(dataset))),
);
}
TreeNode::new(group.id(), group_name, children)
.set_hdf5_object(Hdf5Object::Group(group))
}
let root_name = "/";
let root_group = h5_file.group(root_name).expect("Couldn't open root group");
Ok(tree_from_group(root_name, root_group))
}
pub fn get_num_active_data_analysis_tasks(&self) -> usize {
let info_dict = self.node_id_to_analysis.lock().unwrap();
info_dict
.values()
.filter(|&v| matches!(v, AsyncDataAnalysis::Loading))
.count()
}
pub fn get_text_for(
&self,
path: &[NodeIdT],
) -> Option<(Vec<(String, String)>, Option<analysis::HistogramData>)> {
let tree = match self.tree.as_ref() {
Some(tree) => tree,
None => return None,
};
let tree_node = match tree.get_selected_node(path) {
Some(node) => node,
None => return None,
};
let obj = match tree_node.hdf5_object.as_ref() {
Some(obj) => obj,
None => return None,
};
match obj {
Hdf5Object::Dataset(_) => {
let mut info = get_text_for_dataset(&tree_node);
let key = tree_node.id().clone();
let mut stats_text: Vec<(String, String)> = vec![];
let mut hist_data: Option<analysis::HistogramData> = None;
let info_dict = self.node_id_to_analysis.lock().unwrap();
if let Some(node_info) = info_dict.get(&key) {
match node_info {
AsyncDataAnalysis::Loading => {
stats_text = vec![(
"Stats".into(),
"Loading".to_owned()
+ &".".repeat((self.animation_state / 3 % 4).into()),
)]
}
AsyncDataAnalysis::Ready(val) => match val {
analysis::AnalysisResult::Failed(s) => {
stats_text = vec![("Stats".into(), format!("Failed! ({})", s))];
}
analysis::AnalysisResult::NotAvailable => {
stats_text = vec![("Stats".into(), "Not available".into())];
}
analysis::AnalysisResult::Stats(stats, h) => {
stats_text = stats.to_vec();
hist_data = h.to_owned();
}
},
}
}
info.extend(stats_text);
Some((info, hist_data))
}
Hdf5Object::Group(_) => {
let mut info = get_text_for_group(&tree_node);
if let Some(Hdf5Object::Group(group)) = tree_node.hdf5_object.as_ref() {
if group.name() == "/" {
let path = &self.h5_file_path;
if let Ok(metadata) = fs::metadata(path) {
if let Ok(modified) = metadata.modified() {
let datetime: DateTime<Local> = DateTime::<Local>::from(modified);
info.push((
"File last modified".to_string(),
datetime.format("%Y-%m-%d %H:%M:%S").to_string(),
));
}
let perms = metadata.permissions();
#[cfg(unix)]
{
info.push((
"File permissions".to_string(),
format!("{:o}", perms.mode()),
));
}
}
}
}
Some((info, None))
}
}
}
fn copy_to_clipboard(&self, text: &str) {
use crossterm::{clipboard::CopyToClipboard, QueueableCommand};
use std::io::Write;
let mut stdout = std::io::stdout();
if let Err(e) = stdout.queue(CopyToClipboard::to_clipboard_from(text.to_string())) {
log::error!("Failed to copy to clipboard via crossterm: {:?}", e);
} else if let Err(e) = stdout.flush() {
log::error!("Failed to flush stdout after clipboard copy: {:?}", e);
} else {
log::info!(
"Copied content to clipboard via OSC 52 (length: {})",
text.len()
);
}
}
fn on_click(&mut self, column: u16, row: u16) {
let position = Position::new(column, row);
log::debug!("clicked at {:?}", position);
let is_double_click = if let (Some(last_pos), Some(last_time)) =
(self.last_click_position, self.last_click_time)
{
last_pos.y == row && last_pos.x == column && last_time.elapsed().as_millis() < 300
} else {
false
};
self.last_click_position = Some(position);
self.last_click_time = Some(std::time::Instant::now());
if self.mode == SelectionMode::HelpScreen && self.last_help_screen_area.contains(position) {
return;
}
let divider_column = self.last_tree_area.right();
if column == divider_column || column == divider_column.saturating_sub(1) {
self.is_dragging_divider = true;
return;
}
if self.last_tree_area.contains(position) {
self.mode = SelectionMode::TreeBrowsing;
if let Some(id) = self.tree_state.rendered_at(position) {
let arg = id.to_vec();
if is_double_click {
let path_str = self
.tree
.as_ref()
.and_then(|tree| tree.get_selected_node(&arg))
.and_then(|node| match &node.hdf5_object {
Some(Hdf5Object::Dataset(dataset)) => Some(dataset.name().to_string()),
Some(Hdf5Object::Group(group)) => Some(group.name().to_string()),
_ => None,
});
if let Some(path) = path_str {
self.copy_to_clipboard(&path);
self.copied_indicator = Some((arg.clone(), std::time::Instant::now()));
}
} else {
self.tree_state.toggle(arg.clone());
self.tree_state.select(arg);
}
}
return;
}
if self.last_object_info_area.contains(position) {
self.mode = SelectionMode::ObjectInfoInspecting;
if is_double_click {
let area = self.last_object_info_area;
let is_on_border = column == area.x
|| column == area.x + area.width.saturating_sub(1)
|| row == area.y
|| row == area.y + area.height.saturating_sub(1);
let selected = self.tree_state.selected().to_vec();
if !selected.is_empty() {
if let Some((info, _histogram_data)) = self.get_text_for(&selected) {
if is_on_border {
let mut text_to_copy = String::new();
for (k, v) in info {
text_to_copy.push_str(&format!("{}: {}\n", k, v));
}
self.copy_to_clipboard(&text_to_copy);
self.copied_object_info_indicator =
Some(("_all".to_string(), std::time::Instant::now()));
} else {
let table_area = self.last_object_info_table_area;
if table_area.contains(position) {
let clicked_row_idx = (row - table_area.y) as usize
+ self.object_info_scroll_state as usize;
if let Some(k) = self.object_info_row_keys.get(clicked_row_idx) {
if let Some(v) =
info.iter().find(|(key, _)| key == k).map(|(_, val)| val)
{
self.copy_to_clipboard(v);
self.copied_object_info_indicator =
Some((k.clone(), std::time::Instant::now()));
}
}
}
}
}
}
}
return;
}
if self.last_search_query_area.contains(position) {
self.mode = SelectionMode::SearchQueryEditing;
}
}
fn on_keypress_tree_mode(&mut self, keycode: crossterm::event::KeyCode) -> KeyPressResult {
match keycode {
KeyCode::Left => {
if self.filtered_tree.is_some() {
self.tree_state.key_left();
}
}
KeyCode::Char('h') => {
if self.filtered_tree.is_some() {
self.tree_state.key_left();
}
}
KeyCode::Up | KeyCode::Char('k') => {
if self.filtered_tree.is_some() {
self.tree_state.key_up();
}
}
KeyCode::Down | KeyCode::Char('j') => {
if self.filtered_tree.is_some() {
self.tree_state.key_down();
}
}
KeyCode::Right | KeyCode::Char('l') => {
self.mode = SelectionMode::ObjectInfoInspecting;
}
KeyCode::Home => {
if self.filtered_tree.is_some() {
self.tree_state.select_first();
}
}
KeyCode::End => {
if self.filtered_tree.is_some() {
self.tree_state.select_last();
}
}
KeyCode::Char('i') => {
let post_cmd = std::env::var("H5INSPECT_POST").ok();
let last_path = self
.tree
.as_ref()
.and_then(|tree| tree.get_selected_node(self.tree_state.selected()))
.and_then(|node| match &node.hdf5_object {
Some(Hdf5Object::Dataset(dataset)) => Some(dataset.name().to_string()),
Some(Hdf5Object::Group(group)) => Some(group.name().to_string()),
_ => None,
});
match last_path {
Some(p) => {
return KeyPressResult::RunPostCommand(post_cmd, p);
}
None => {
log::debug!(
"Problem finding selected dataset or group, not running H5INSPECT_POST"
);
}
}
}
KeyCode::Enter => {
if self.filtered_tree.is_some() {
self.tree_state.toggle_selected();
}
}
KeyCode::Char('c') => {
if self.filtered_tree.is_some() {
self.tree_state.toggle_selected();
}
}
KeyCode::Tab => {
if self.filtered_tree.is_some() {
self.tree_state
.select_relative(|x| x.map_or(0, |current| current.saturating_add(1)));
}
}
KeyCode::BackTab => {
if self.filtered_tree.is_some() {
self.tree_state
.select_relative(|x| x.map_or(0, |current| current.saturating_sub(1)));
}
}
KeyCode::Char('f') => {
self.open_all_tree_nodes();
}
KeyCode::Char('y') => {
let last_path = self
.tree
.as_ref()
.and_then(|tree| tree.get_selected_node(self.tree_state.selected()))
.and_then(|node| match &node.hdf5_object {
Some(Hdf5Object::Dataset(dataset)) => Some(dataset.name().to_string()),
Some(Hdf5Object::Group(group)) => Some(group.name().to_string()),
_ => None,
});
if let Some(path) = last_path {
self.copy_to_clipboard(&path);
self.copied_indicator = Some((
self.tree_state.selected().to_vec(),
std::time::Instant::now(),
));
}
}
KeyCode::Char('g') => {
if self.filtered_tree.is_some() {
self.tree_state.select_first();
}
}
KeyCode::Char('G') => {
if self.filtered_tree.is_some() {
self.tree_state.select_last();
}
}
KeyCode::Char('L') => {
self.show_logs = !self.show_logs;
}
KeyCode::Char('?') => {
self.mode = SelectionMode::HelpScreen;
}
KeyCode::PageDown => {
if self.filtered_tree.is_some() {
self.tree_state.select_relative(|current| {
current.map_or(0, |current| current.saturating_add(50))
});
}
}
KeyCode::PageUp => {
if self.filtered_tree.is_some() {
self.tree_state.select_relative(|current| {
current.map_or(0, |current| current.saturating_sub(50))
});
}
}
_ => {}
}
KeyPressResult::Redraw
}
pub fn search_query_and_cursor(&self) -> (String, u16) {
let rev_right: String = self.search_query_right.chars().rev().collect();
let text = self.search_query_left.clone() + &rev_right;
let cursor_pos = self.search_query_left.len();
(text, cursor_pos.try_into().unwrap())
}
fn on_keypress_search_mode(&mut self, key: crossterm::event::KeyEvent) -> KeyPressResult {
let keycode = key.code;
let mut refresh_filtered_tree = true;
let result = match keycode {
KeyCode::Char(to_insert) => {
self.search_query_left.push(to_insert);
KeyPressResult::Redraw
}
KeyCode::Left => {
if key.modifiers.contains(KeyModifiers::CONTROL) {
while let Some(c) = self.search_query_left.pop() {
self.search_query_right.push(c);
if c.is_whitespace() {
break;
}
}
} else {
self.search_query_left
.pop()
.map(|c| self.search_query_right.push(c));
}
KeyPressResult::Redraw
}
KeyCode::Right => {
if key.modifiers.contains(KeyModifiers::CONTROL) {
while let Some(c) = self.search_query_right.pop() {
self.search_query_left.push(c);
if c.is_whitespace() {
break;
}
}
} else {
self.search_query_right
.pop()
.map(|c| self.search_query_left.push(c));
}
KeyPressResult::Redraw
}
KeyCode::Home => {
self.search_query_right
.extend(self.search_query_left.drain(..).rev());
KeyPressResult::Redraw
}
KeyCode::End => {
self.search_query_left
.extend(self.search_query_right.drain(..).rev());
KeyPressResult::Redraw
}
KeyCode::Backspace => {
self.search_query_left.pop();
KeyPressResult::Redraw
}
KeyCode::Delete => {
self.search_query_right.pop();
KeyPressResult::Redraw
}
other => {
refresh_filtered_tree = false;
self.on_keypress_tree_mode(other)
}
};
if refresh_filtered_tree {
self.update_filtered_tree();
}
result
}
fn on_keypress_object_info_mode(
&mut self,
keycode: crossterm::event::KeyCode,
) -> KeyPressResult {
match keycode {
KeyCode::Up | KeyCode::Char('k') => {
self.object_info_scroll_state = self.object_info_scroll_state.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') => {
self.object_info_scroll_state = self.object_info_scroll_state.saturating_add(1);
}
KeyCode::Left | KeyCode::Char('h') => {
self.mode = SelectionMode::TreeBrowsing;
}
KeyCode::PageDown => {
self.object_info_scroll_state = self.object_info_scroll_state.saturating_add(50);
}
KeyCode::PageUp => {
self.object_info_scroll_state = self.object_info_scroll_state.saturating_sub(50);
}
KeyCode::End => {
self.object_info_scroll_state = u16::MAX;
}
KeyCode::Home => {
self.object_info_scroll_state = 0;
}
KeyCode::Char('?') => {
self.mode = SelectionMode::HelpScreen;
}
KeyCode::Char('L') => {
self.show_logs = !self.show_logs;
}
KeyCode::Char('i') => {
return self.on_keypress_tree_mode(keycode);
}
_ => {}
};
KeyPressResult::Redraw
}
fn on_keypress_help_screen_mode(&mut self, keycode: crossterm::event::KeyCode) {
match keycode {
KeyCode::Up | KeyCode::Char('k') => {
self.help_screen_scroll_state = self.help_screen_scroll_state.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') => {
self.help_screen_scroll_state = self.help_screen_scroll_state.saturating_add(1);
}
KeyCode::PageDown => {
self.help_screen_scroll_state = self.help_screen_scroll_state.saturating_add(50);
}
KeyCode::PageUp => {
self.help_screen_scroll_state = self.help_screen_scroll_state.saturating_sub(50);
}
KeyCode::End => {
self.help_screen_scroll_state = u16::MAX;
}
KeyCode::Home => {
self.help_screen_scroll_state = 0;
}
_ => {}
};
}
fn update_filtered_tree(&mut self) {
let query = &self.search_query_and_cursor().0;
match &self.tree {
Some(tree) => {
self.filtered_tree = tree.filter(query);
self.update_selected_tree_item();
}
None => {
self.filtered_tree = None;
}
}
}
fn update_selected_tree_item(&mut self) {
match &self.filtered_tree {
Some(filtered_tree) => {
let nothing_selected = self.tree_state.selected().is_empty();
let selected_item = filtered_tree.get_selected_node(&self.tree_state.selected());
let selected_item_is_in_tree = selected_item.is_some();
let selected_item_is_direct_match =
selected_item.map_or(false, |t| t.is_direct_match);
if nothing_selected || !selected_item_is_in_tree || !selected_item_is_direct_match {
let first_match = filtered_tree.path_to_first_match();
self.tree_state.select(first_match.clone());
for i in 0..first_match.len() {
self.tree_state.open(first_match[0..i].to_vec());
}
}
self.tree_state.scroll_selected_into_view();
}
None => {
self.tree_state.select(vec![]);
}
}
}
pub fn set_last_object_info_area(&mut self, area: Rect) {
self.last_object_info_area = area;
}
pub fn set_last_object_info_table_area(&mut self, area: Rect) {
self.last_object_info_table_area = area;
}
pub fn set_last_tree_area(&mut self, area: Rect) {
self.last_tree_area = area;
}
pub fn set_last_search_query_area(&mut self, area: Rect) {
self.last_search_query_area = area;
}
pub fn set_last_help_screen_area(&mut self, area: Rect) {
self.last_help_screen_area = area;
}
fn open_all_tree_nodes(&mut self) {
if let Some(tree) = &self.tree {
let mut to_visit = vec![(tree, vec![tree.id()])];
while let Some((current, id_path)) = to_visit.pop() {
self.tree_state.open(id_path.clone());
to_visit.extend(current.children().iter().map(|c| {
let mut id_path = id_path.clone();
id_path.push(c.id());
(c, id_path)
}));
}
}
}
fn start_analysis_task(&self, tree_node: &TreeNode<NodeIdT>) {
if let Some(Hdf5Object::Dataset(d)) = &tree_node.hdf5_object {
let key = tree_node.id().clone();
{
let mut info_dict = self.node_id_to_analysis.lock().unwrap();
if info_dict.get(&key).is_some() {
return;
}
info_dict.insert(key.clone(), AsyncDataAnalysis::Loading);
}
let thread_arc: Arc<Mutex<HashMap<NodeIdT, AsyncDataAnalysis>>> =
Arc::clone(&self.node_id_to_analysis);
let semaphore = Arc::clone(&self.process_semaphore);
let file_path = self.h5_file_path.to_string_lossy().to_string();
let dataset_path = d.name().to_string();
tokio::spawn(async move {
let _permit = semaphore
.acquire()
.await
.expect("Semaphore should not be closed");
log::debug!("Forking analysis process for dataset {}", &dataset_path);
let (tx, rx) = ipc_channel::ipc::channel::<AnalysisResult>()
.expect("Failed to create ipc-channel");
let file_path_buf = std::path::PathBuf::from(&file_path);
let dataset_path_clone = dataset_path.clone();
let result = tokio::task::spawn_blocking(move || {
match unsafe { fork() } {
Ok(ForkResult::Parent { child }) => {
drop(tx);
let msg_res = rx.recv();
let _ = waitpid(child, None);
match msg_res {
Ok(analysis) => Ok(analysis),
Err(e) => Err(format!("IPC receive failed: {:?}", e)),
}
}
Ok(ForkResult::Child) => {
log::set_max_level(log::LevelFilter::Off);
drop(rx);
let res = crate::analysis::hdf5_dataset_analysis_from_path(
&file_path_buf,
&dataset_path_clone,
);
let processed_analysis = match res {
Ok(analysis) => analysis,
Err(e) => AnalysisResult::Failed(e.to_string()),
};
let _ = tx.send(processed_analysis);
std::process::exit(0);
}
Err(e) => Err(format!("Fork failed: {}", e)),
}
})
.await;
let processed_analysis = match result {
Ok(Ok(analysis)) => analysis,
Ok(Err(err_msg)) => AnalysisResult::Failed(err_msg),
Err(join_err) => AnalysisResult::Failed(format!("Task panic: {}", join_err)),
};
if let Ok(mut info_dict) = thread_arc.lock() {
info_dict.insert(key, AsyncDataAnalysis::Ready(processed_analysis));
}
});
}
}
pub async fn run(mut self) -> Result<AppFinishingState, Box<dyn std::error::Error>> {
let h5_file = h5_utils::open_file(&self.h5_file_path)?;
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
let mut events = events::EventHandler::new(receiver);
tokio::task::spawn_blocking(move || {
let tree = App::tree_from_h5(&h5_file).expect("Failed to parse HDF5 structure");
let tree_update = events::Event::TreeUpdate(tree);
sender.clone().send(tree_update).unwrap();
});
let mut redraw = true;
let mut terminal = ratatui::init();
crossterm::execute!(
std::io::stdout(),
crossterm::event::EnableMouseCapture,
crossterm::event::EnableBracketedPaste
)?;
while self.running == AppFinishingState::Continue {
if let Some(last_selected) = &self.tree_state_last_rendered_selected {
if last_selected != self.tree_state.selected() {
self.object_info_scroll_state = 0;
let path_to_selected_node = self.tree_state.selected();
if let Some(tree_node) = self
.tree
.as_ref()
.unwrap()
.get_selected_node(path_to_selected_node)
{
self.start_analysis_task(tree_node);
}
}
}
if redraw {
terminal.draw(|frame| ui(frame, &mut self))?;
self.tree_state_last_rendered_selected = Some(self.tree_state.selected().to_vec());
}
redraw = match events.next_event() {
events::Event::AnimationTick => {
self.animation_state = self.animation_state.wrapping_add(1);
true
}
events::Event::Key(key) => {
match self.handle_keypress(key) {
KeyPressResult::Redraw => true,
KeyPressResult::DontRedraw => false,
KeyPressResult::RunPostCommand(post_cmd, ds_path) => {
crossterm::execute!(
std::io::stdout(),
crossterm::event::DisableMouseCapture,
crossterm::event::DisableBracketedPaste
)?;
ratatui::restore();
let h5_file_path_str = self.h5_file_path.to_string_lossy().to_string();
match &post_cmd {
Some(cmd) => {
println!(
"H5INSPECT_POST running: {} {} {}",
cmd, h5_file_path_str, ds_path
);
match std::process::Command::new(cmd)
.arg(&h5_file_path_str)
.arg(&ds_path)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.spawn()
.and_then(|mut child| child.wait())
{
Ok(status) if !status.success() => {
eprintln!(
"H5INSPECT_POST script exited with status: {}",
status
);
}
Err(e) => {
eprintln!("Failed to run H5INSPECT_POST: {}", e);
}
_ => {}
}
}
None => {
println!("H5INSPECT_POST not set. See https://github.com/HalFrgrd/h5inspect/blob/master/h5inspect_post/README.md");
for i in 1..=5 {
print!("Continuing in {} seconds...", 6 - i);
stdout().flush().ok();
std::thread::sleep(std::time::Duration::from_secs(1));
print!("\r");
}
}
}
terminal = ratatui::init();
crossterm::execute!(
std::io::stdout(),
crossterm::event::EnableMouseCapture,
crossterm::event::EnableBracketedPaste
)?;
true
}
}
}
events::Event::Mouse(mouse) => self.handle_mouse(mouse),
events::Event::Paste(text) => self.handle_paste(text),
events::Event::Resize => true,
events::Event::TreeUpdate(tree) => {
self.tree = Some(tree);
self.open_all_tree_nodes();
self.update_filtered_tree();
true
}
}
}
crossterm::execute!(
std::io::stdout(),
crossterm::event::DisableMouseCapture,
crossterm::event::DisableBracketedPaste
)?;
ratatui::restore();
Ok(self.running)
}
fn handle_keypress(&mut self, key: crossterm::event::KeyEvent) -> KeyPressResult {
if key.kind == crossterm::event::KeyEventKind::Press {
if key.code == KeyCode::Char('c') && key.modifiers == KeyModifiers::CONTROL {
self.running = AppFinishingState::Quit;
return KeyPressResult::Redraw;
}
return match self.mode {
SelectionMode::TreeBrowsing => match key.code {
KeyCode::Char('q') => {
self.running = AppFinishingState::Quit;
KeyPressResult::Redraw
}
KeyCode::Char('/') => {
self.mode = SelectionMode::SearchQueryEditing;
KeyPressResult::Redraw
}
other => self.on_keypress_tree_mode(other),
},
SelectionMode::SearchQueryEditing => match key.code {
KeyCode::Esc | KeyCode::Enter => {
self.mode = SelectionMode::TreeBrowsing;
KeyPressResult::Redraw
}
_ => self.on_keypress_search_mode(key),
},
SelectionMode::ObjectInfoInspecting => match key.code {
KeyCode::Char('q') => {
self.running = AppFinishingState::Quit;
KeyPressResult::Redraw
}
KeyCode::Char('/') => {
self.mode = SelectionMode::SearchQueryEditing;
KeyPressResult::Redraw
}
other => self.on_keypress_object_info_mode(other),
},
SelectionMode::HelpScreen => match key.code {
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') | KeyCode::Char('?') => {
self.mode = SelectionMode::TreeBrowsing;
KeyPressResult::Redraw
}
other => {
self.on_keypress_help_screen_mode(other);
KeyPressResult::Redraw
}
},
};
}
KeyPressResult::DontRedraw
}
fn handle_mouse(&mut self, mouse: crossterm::event::MouseEvent) -> bool {
log::debug!("mouse event: {:?}", mouse);
let position = Position::new(mouse.column, mouse.row);
let new_hover = if self.last_tree_area.contains(position) {
self.tree_state.rendered_at(position).map(|id| id.to_vec())
} else {
None
};
let hover_changed = self.hovered_node != new_hover;
self.hovered_node = new_hover;
let mut hovered_key = None;
if self.last_object_info_area.contains(position) {
let area = self.last_object_info_area;
let is_on_border = mouse.column == area.x
|| mouse.column == area.x + area.width.saturating_sub(1)
|| mouse.row == area.y
|| mouse.row == area.y + area.height.saturating_sub(1);
if !is_on_border {
let table_area = self.last_object_info_table_area;
if table_area.contains(position) {
let clicked_row_idx = (mouse.row - table_area.y) as usize
+ self.object_info_scroll_state as usize;
hovered_key = self.object_info_row_keys.get(clicked_row_idx).cloned();
}
}
}
let obj_info_hover_changed = self.hovered_object_info_key != hovered_key;
self.hovered_object_info_key = hovered_key;
let needs_redraw = hover_changed || obj_info_hover_changed;
let match_result = match mouse.kind {
MouseEventKind::Down(MouseButton::Left) => {
self.on_click(mouse.column, mouse.row);
true
}
MouseEventKind::Up(MouseButton::Left) => {
self.is_dragging_divider = false;
true
}
MouseEventKind::Drag(MouseButton::Left) => {
if self.is_dragging_divider {
let total_width = self.last_tree_area.width + self.last_object_info_area.width;
if total_width > 0 {
let new_percentage =
((mouse.column as u32 * 100) / total_width as u32) as u16;
self.tree_width_percentage = new_percentage.clamp(10, 90);
}
}
true
}
MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => {
let is_scroll_down = matches!(mouse.kind, MouseEventKind::ScrollDown);
if self.mode == SelectionMode::HelpScreen
&& self
.last_help_screen_area
.contains(Position::new(mouse.column, mouse.row))
{
if is_scroll_down {
self.help_screen_scroll_state =
self.help_screen_scroll_state.saturating_add(1);
} else {
self.help_screen_scroll_state =
self.help_screen_scroll_state.saturating_sub(1);
}
return true;
}
if self
.last_object_info_area
.contains(Position::new(mouse.column, mouse.row))
{
if is_scroll_down {
self.object_info_scroll_state =
self.object_info_scroll_state.saturating_add(1);
} else {
self.object_info_scroll_state =
self.object_info_scroll_state.saturating_sub(1);
}
} else if self
.last_tree_area
.contains(Position::new(mouse.column, mouse.row))
{
if is_scroll_down {
self.tree_state.scroll_down(1);
} else {
self.tree_state.scroll_up(1);
}
}
const SCROLL_COOLDOWN_MS: u128 = 40;
if self.last_redraw_from_scroll.elapsed().as_millis() > SCROLL_COOLDOWN_MS {
self.last_redraw_from_scroll = Instant::now();
true
} else {
false
}
}
_ => false,
};
needs_redraw || match_result
}
fn handle_paste(&mut self, text: String) -> bool {
if self.mode == SelectionMode::SearchQueryEditing {
for c in text.chars() {
if !c.is_control() {
self.search_query_left.push(c);
}
}
self.update_filtered_tree();
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::h5_utils;
use std::path::PathBuf;
#[test]
fn test_fork_analysis() {
let path = PathBuf::from("dummy.h5");
if !path.exists() {
h5_utils::generate_dummy_file().unwrap();
}
let (tx, rx) =
ipc_channel::ipc::channel::<AnalysisResult>().expect("Failed to create ipc-channel");
let dataset_path = "sums_of_bernoulli".to_string();
let file_path = path.clone();
match unsafe { fork() } {
Ok(ForkResult::Parent { child }) => {
drop(tx);
let msg_res = rx.recv().expect("Failed to receive from child");
let _ = waitpid(child, None);
match msg_res {
AnalysisResult::Stats(stats, _) => {
assert!(!stats.is_empty());
let mean_stat = stats.iter().find(|(k, _)| k == "Mean");
assert!(mean_stat.is_some());
}
other => std::panic!("Expected Stats, got {:?}", other),
}
}
Ok(ForkResult::Child) => {
log::set_max_level(log::LevelFilter::Off);
drop(rx);
let res =
crate::analysis::hdf5_dataset_analysis_from_path(&file_path, &dataset_path);
let processed_analysis = match res {
Ok(analysis) => analysis,
Err(e) => AnalysisResult::Failed(e.to_string()),
};
let _ = tx.send(processed_analysis);
std::process::exit(0);
}
Err(e) => std::panic!("Fork failed: {}", e),
}
}
}