use crate::tui::component::{Action, Component}; use anyhow::Result; use ratatui::buffer::Buffer; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind}; use ratatui::layout::{Alignment, Position, Rect}; use ratatui::prelude::Style; use ratatui::style::{Color, Modifier}; use ratatui::widgets::{Block, Borders, StatefulWidget}; use std::fs; use tui_tree_widget::{Tree, TreeItem, TreeState}; #[derive(Debug)] pub struct Explorer<'a> { root_path: std::path::PathBuf, tree_items: TreeItem<'a, String>, tree_state: TreeState, } impl<'a> Explorer<'a> { pub fn new(path: &std::path::PathBuf) -> Self { let explorer = Explorer { root_path: path.to_owned(), tree_items: Self::build_tree_from_path(path.to_owned()), tree_state: TreeState::default(), }; explorer } fn build_tree_from_path(path: std::path::PathBuf) -> TreeItem<'static, String> { let mut children = vec![]; if let Ok(entries) = fs::read_dir(&path) { let mut paths = entries .map(|res| res.map(|e| e.path())) .collect::, std::io::Error>>() .expect(""); paths.sort(); for path in paths { if path.is_dir() { children.push(Self::build_tree_from_path(path)); } else { if let Ok(path) = std::path::absolute(&path) { let path_str = path.to_string_lossy().to_string(); children.push(TreeItem::new_leaf( path_str, path.file_name() .expect("Failed to get file name from path.") .to_string_lossy() .to_string(), )); } } } } let abs = std::path::absolute(&path) .expect( format!( "Failed to find absolute path for TreeItem: {}", path.to_string_lossy().to_string() ) .as_str(), ) .to_string_lossy() .to_string(); TreeItem::new( abs, path.file_name() .expect("Failed to get file name from path.") .to_string_lossy() .to_string(), children, ) .expect("Failed to build tree from path.") } pub fn render(&mut self, area: Rect, buf: &mut Buffer) { StatefulWidget::render( Tree::new(&self.tree_items.children()) .expect("Failed to build tree.") .style(Style::default()) .block( Block::default() .borders(Borders::ALL) .title( self.root_path .file_name() .expect("Failed to get file name from path.") .to_string_lossy(), ) .title_style(Style::default().fg(Color::Green)) .title_alignment(Alignment::Center), ) .highlight_style( Style::new() .fg(Color::Black) .bg(Color::Rgb(57, 59, 64)) .add_modifier(Modifier::BOLD), ), area, buf, &mut self.tree_state, ) } pub fn selected(&self) -> Result { if let Some(path) = self.tree_state.selected().last() { return Ok(std::path::absolute(path)?.to_str().unwrap().to_string()); } Err(anyhow::anyhow!("Failed to get selected TreeItem")) } } impl<'a> Component for Explorer<'a> { fn id(&self) -> &str { "explorer" } fn handle_event(&mut self, event: Event) -> Action { if let Some(key_event) = event.as_key_event() { // Handle events here that should not be passed on to the vim emulation handler. match self.handle_key_events(key_event) { Action::Handled => return Action::Handled, _ => {} } } if let Some(mouse_event) = event.as_mouse_event() { match self.handle_mouse_events(mouse_event) { Action::Handled => return Action::Handled, _ => {} } } Action::Pass } fn handle_key_events(&mut self, key: KeyEvent) -> Action { let changed = match key.code { KeyCode::Up => self.tree_state.key_up(), KeyCode::Char('k') => self.tree_state.key_up(), KeyCode::Down => self.tree_state.key_down(), KeyCode::Char('j') => self.tree_state.key_down(), KeyCode::Left => self.tree_state.key_left(), KeyCode::Char('h') => self.tree_state.key_left(), KeyCode::Right => self.tree_state.key_right(), KeyCode::Char('l') => self.tree_state.key_right(), KeyCode::Enter => self.tree_state.toggle_selected(), _ => false, }; if changed { return Action::Handled; } Action::Noop } fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Action { let changed = match mouse.kind { MouseEventKind::ScrollDown => self.tree_state.scroll_down(1), MouseEventKind::ScrollUp => self.tree_state.scroll_up(1), MouseEventKind::Down(_button) => self .tree_state .click_at(Position::new(mouse.column, mouse.row)), _ => false, }; if changed { return Action::Handled; } Action::Noop } }