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::time::Instant;
use chrono::{DateTime, Local};
use core::panic;
use crossterm::event::{KeyCode, KeyModifiers};
use dirs;
use log;
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,
ShouldRunCommand(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,
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,
}
#[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_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(),
}
}
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 on_click(&mut self, column: u16, row: u16) {
let position = Position::new(column, row);
log::debug!("clicked at {:?}", position);
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;
} else if self.last_object_info_area.contains(position) {
self.mode = SelectionMode::ObjectInfoInspecting;
} else if self.last_search_query_area.contains(position) {
self.mode = SelectionMode::SearchQueryEditing;
}
if let Some(id) = self.tree_state.rendered_at(position) {
let arg = id.to_vec();
self.tree_state.toggle(arg.clone());
self.tree_state.select(arg);
}
}
fn on_keypress_tree_mode(&mut self, keycode: crossterm::event::KeyCode) -> () {
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') => {
if let Ok(post_cmd) = std::env::var("H5INSPECT_POST") {
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()),
_ => None,
});
match last_path {
Some(p) => {
self.running = AppFinishingState::ShouldRunCommand(post_cmd, p);
}
None => {
log::debug!(
"Problem finding selected dataset, not running H5INSPECT_POST"
);
}
}
} else {
log::debug!(
"Environment variable H5INSPECT_POST not set, not running any command"
);
}
}
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('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))
});
}
}
_ => {}
}
}
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) {
let keycode = key.code;
let mut refresh_filtered_tree = true;
match keycode {
KeyCode::Char(to_insert) => {
self.search_query_left.push(to_insert);
}
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));
}
}
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));
}
}
KeyCode::Home => self
.search_query_right
.extend(self.search_query_left.drain(..).rev()),
KeyCode::End => self
.search_query_left
.extend(self.search_query_right.drain(..).rev()),
KeyCode::Backspace => {
self.search_query_left.pop();
}
KeyCode::Delete => {
self.search_query_right.pop();
}
other => {
refresh_filtered_tree = false;
self.on_keypress_tree_mode(other);
}
};
if refresh_filtered_tree {
self.update_filtered_tree();
}
}
fn on_keypress_object_info_mode(&mut self, keycode: crossterm::event::KeyCode) {
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') => {
self.on_keypress_tree_mode(keycode);
}
_ => {}
};
}
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_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!("Spawning analysis process for dataset {}", &dataset_path);
let result = tokio::process::Command::new(std::env::current_exe().unwrap())
.arg(&file_path)
.arg("--analyze-dataset")
.arg(&dataset_path)
.output()
.await;
log::debug!("Analysis process for dataset {} finished", &dataset_path);
let processed_analysis = match result {
Ok(output) => {
if output.status.success() {
match serde_json::from_slice::<AnalysisResult>(&output.stdout) {
Ok(analysis) => analysis,
Err(e) => AnalysisResult::Failed(format!(
"Failed to parse analysis result: {}",
e
)),
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
AnalysisResult::Failed(format!("Analysis process failed: {}", stderr))
}
}
Err(e) => {
AnalysisResult::Failed(format!("Failed to spawn analysis process: {}", e))
}
};
if let Ok(mut info_dict) = thread_arc.lock() {
info_dict.insert(key, AsyncDataAnalysis::Ready(processed_analysis));
}
});
}
}
pub async fn run<B: ratatui::backend::Backend>(
mut self,
mut terminal: ratatui::Terminal<B>,
) -> Result<AppFinishingState, Box<dyn std::error::Error>> {
let h5_file = h5_utils::open_file(&self.h5_file_path)?;
let mut events = events::EventHandler::new();
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);
events.sender.clone().send(tree_update).unwrap();
});
let mut redraw = true;
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());
}
if let Some(event) = events.receiver.recv().await {
redraw = match event {
events::Event::AnimationTick => {
self.animation_state = self.animation_state.wrapping_add(1);
true
}
events::Event::Key(key) => {
self.handle_keypress(key);
true
}
events::Event::Mouse(mouse) => self.handle_mouse(mouse),
events::Event::Resize => true,
events::Event::TreeUpdate(tree) => {
self.tree = Some(tree);
self.open_all_tree_nodes();
self.update_filtered_tree();
true
}
}
}
}
Ok(self.running)
}
fn handle_keypress(&mut self, key: crossterm::event::KeyEvent) {
if key.kind == crossterm::event::KeyEventKind::Press {
if key.code == KeyCode::Char('c') && key.modifiers == KeyModifiers::CONTROL {
self.running = AppFinishingState::Quit;
}
match self.mode {
SelectionMode::TreeBrowsing => match key.code {
KeyCode::Char('q') => {
self.running = AppFinishingState::Quit;
}
KeyCode::Char('/') => {
self.mode = SelectionMode::SearchQueryEditing;
}
other => {
self.on_keypress_tree_mode(other);
}
},
SelectionMode::SearchQueryEditing => match key.code {
KeyCode::Esc | KeyCode::Enter => {
self.mode = SelectionMode::TreeBrowsing;
}
_ => {
self.on_keypress_search_mode(key);
}
},
SelectionMode::ObjectInfoInspecting => match key.code {
KeyCode::Char('q') => {
self.running = AppFinishingState::Quit;
}
KeyCode::Char('/') => {
self.mode = SelectionMode::SearchQueryEditing;
}
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;
}
other => {
self.on_keypress_help_screen_mode(other);
}
},
}
}
}
fn handle_mouse(&mut self, mouse: crossterm::event::MouseEvent) -> bool {
log::debug!("mouse event: {:?}", mouse);
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,
}
}
}