use crate::tui::app::{AppComponents, ComponentOf}; use crate::tui::component::{Action, Component, ComponentState, Focus}; use anyhow::{Context, Result, bail}; 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, pub(crate) component_state: ComponentState, } impl<'a> ComponentOf> for AppComponents<'a> { fn as_ref(&self) -> Option<&Explorer<'a>> { if let AppComponents::AppExplorer(ref e) = *self { return Some(e); } None } fn as_mut(&mut self) -> Option<&mut Explorer<'a>> { if let AppComponents::AppExplorer(ref mut e) = *self { return Some(e); } None } } impl<'a> Explorer<'a> { pub fn new(path: &std::path::PathBuf) -> Result { let explorer = Explorer { root_path: path.to_owned(), tree_items: Self::build_tree_from_path(path.to_owned())?, tree_state: TreeState::default(), component_state: Default::default(), }; Ok(explorer) } fn build_tree_from_path(path: std::path::PathBuf) -> Result> { 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>>() .context(format!( "Failed to build vector of paths under directory: {:?}", path ))?; 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() .context("Failed to get file name from path.")? .to_string_lossy() .to_string(), )); } } } } let abs = std::path::absolute(&path) .context(format!( "Failed to find absolute path for TreeItem: {:?}", path ))? .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, ) .context("Failed to build tree from path.") } pub fn render(&mut self, area: Rect, buf: &mut Buffer) -> Result<()> { StatefulWidget::render( Tree::new(&self.tree_items.children()) .context("Failed to build file Explorer Tree.")? .style(Style::default()) .block( Block::default() .borders(Borders::ALL) .title( self.root_path .file_name() .context("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, ); Ok(()) } 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()); } bail!("Failed to get selected TreeItem") } } impl<'a> Component for Explorer<'a> { fn id(&self) -> &str { "Explorer" } fn is_active(&self) -> bool { self.component_state.focus == Focus::Active } fn handle_event(&mut self, event: Event) -> Result { 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 Ok(Action::Handled), _ => {} } } if let Some(mouse_event) = event.as_mouse_event() { match self.handle_mouse_events(mouse_event)? { Action::Handled => return Ok(Action::Handled), _ => {} } } Ok(Action::Pass) } fn handle_key_events(&mut self, key: KeyEvent) -> Result { 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 Ok(Action::Handled); } Ok(Action::Noop) } fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result { 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 Ok(Action::Handled); } Ok(Action::Noop) } }