2026-01-19 17:41:46 -05:00
|
|
|
use crate::tui::component::{Action, Component};
|
2026-01-20 17:19:13 -05:00
|
|
|
use anyhow::{Context, Result};
|
2026-01-17 15:07:05 -05:00
|
|
|
use ratatui::buffer::Buffer;
|
2026-01-19 17:41:46 -05:00
|
|
|
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
|
|
|
|
|
use ratatui::layout::{Alignment, Position, Rect};
|
2026-01-17 15:07:05 -05:00
|
|
|
use ratatui::prelude::Style;
|
2026-01-19 17:41:46 -05:00
|
|
|
use ratatui::style::{Color, Modifier};
|
|
|
|
|
use ratatui::widgets::{Block, Borders, StatefulWidget};
|
2026-01-17 17:09:42 -05:00
|
|
|
use std::fs;
|
2026-01-19 17:41:46 -05:00
|
|
|
use tui_tree_widget::{Tree, TreeItem, TreeState};
|
2026-01-17 15:07:05 -05:00
|
|
|
|
2026-01-19 17:41:46 -05:00
|
|
|
#[derive(Debug)]
|
2026-01-17 15:07:05 -05:00
|
|
|
pub struct Explorer<'a> {
|
2026-01-19 09:23:12 -05:00
|
|
|
root_path: std::path::PathBuf,
|
2026-01-17 17:09:42 -05:00
|
|
|
tree_items: TreeItem<'a, String>,
|
2026-01-19 17:41:46 -05:00
|
|
|
tree_state: TreeState<String>,
|
2026-01-17 15:07:05 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> Explorer<'a> {
|
2026-01-20 17:19:13 -05:00
|
|
|
pub fn new(path: &std::path::PathBuf) -> Result<Self> {
|
2026-01-18 10:09:28 -05:00
|
|
|
let explorer = Explorer {
|
2026-01-19 09:23:12 -05:00
|
|
|
root_path: path.to_owned(),
|
2026-01-20 17:19:13 -05:00
|
|
|
tree_items: Self::build_tree_from_path(path.to_owned())?,
|
2026-01-19 17:41:46 -05:00
|
|
|
tree_state: TreeState::default(),
|
2026-01-17 17:09:42 -05:00
|
|
|
};
|
2026-01-20 17:19:13 -05:00
|
|
|
Ok(explorer)
|
2026-01-17 15:07:05 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-20 17:19:13 -05:00
|
|
|
fn build_tree_from_path(path: std::path::PathBuf) -> Result<TreeItem<'static, String>> {
|
2026-01-17 15:07:05 -05:00
|
|
|
let mut children = vec![];
|
|
|
|
|
if let Ok(entries) = fs::read_dir(&path) {
|
|
|
|
|
let mut paths = entries
|
|
|
|
|
.map(|res| res.map(|e| e.path()))
|
|
|
|
|
.collect::<Result<Vec<_>, std::io::Error>>()
|
2026-01-20 17:19:13 -05:00
|
|
|
.context(format!(
|
|
|
|
|
"Failed to build vector of paths under directory: {:?}",
|
|
|
|
|
path
|
|
|
|
|
))?;
|
2026-01-17 15:07:05 -05:00
|
|
|
paths.sort();
|
|
|
|
|
for path in paths {
|
|
|
|
|
if path.is_dir() {
|
2026-01-20 17:19:13 -05:00
|
|
|
children.push(Self::build_tree_from_path(path)?);
|
2026-01-17 15:07:05 -05:00
|
|
|
} else {
|
2026-01-19 17:41:46 -05:00
|
|
|
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()
|
2026-01-20 17:19:13 -05:00
|
|
|
.context("Failed to get file name from path.")?
|
2026-01-19 17:41:46 -05:00
|
|
|
.to_string_lossy()
|
|
|
|
|
.to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-01-17 15:07:05 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 17:41:46 -05:00
|
|
|
let abs = std::path::absolute(&path)
|
2026-01-20 17:19:13 -05:00
|
|
|
.context(format!(
|
|
|
|
|
"Failed to find absolute path for TreeItem: {:?}",
|
|
|
|
|
path
|
|
|
|
|
))?
|
2026-01-19 17:41:46 -05:00
|
|
|
.to_string_lossy()
|
|
|
|
|
.to_string();
|
2026-01-17 15:07:05 -05:00
|
|
|
TreeItem::new(
|
2026-01-19 17:41:46 -05:00
|
|
|
abs,
|
2026-01-17 15:07:05 -05:00
|
|
|
path.file_name()
|
2026-01-19 09:23:12 -05:00
|
|
|
.expect("Failed to get file name from path.")
|
2026-01-17 15:07:05 -05:00
|
|
|
.to_string_lossy()
|
|
|
|
|
.to_string(),
|
|
|
|
|
children,
|
|
|
|
|
)
|
2026-01-20 17:19:13 -05:00
|
|
|
.context("Failed to build tree from path.")
|
2026-01-17 15:07:05 -05:00
|
|
|
}
|
2026-01-17 17:09:42 -05:00
|
|
|
|
2026-01-20 17:19:13 -05:00
|
|
|
pub fn render(&mut self, area: Rect, buf: &mut Buffer) -> Result<()> {
|
2026-01-19 17:41:46 -05:00
|
|
|
StatefulWidget::render(
|
|
|
|
|
Tree::new(&self.tree_items.children())
|
2026-01-20 17:19:13 -05:00
|
|
|
.context("Failed to build file Explorer Tree.")?
|
2026-01-19 17:41:46 -05:00
|
|
|
.style(Style::default())
|
|
|
|
|
.block(
|
|
|
|
|
Block::default()
|
|
|
|
|
.borders(Borders::ALL)
|
|
|
|
|
.title(
|
|
|
|
|
self.root_path
|
|
|
|
|
.file_name()
|
2026-01-20 17:19:13 -05:00
|
|
|
.context("Failed to get file name from path.")?
|
2026-01-19 17:41:46 -05:00
|
|
|
.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,
|
2026-01-20 17:19:13 -05:00
|
|
|
);
|
|
|
|
|
Ok(())
|
2026-01-19 17:41:46 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-20 16:03:38 -05:00
|
|
|
pub fn selected(&self) -> Result<String> {
|
|
|
|
|
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"))
|
2026-01-17 17:09:42 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 15:03:50 -05:00
|
|
|
impl<'a> Component for Explorer<'a> {
|
|
|
|
|
fn id(&self) -> &str {
|
|
|
|
|
"explorer"
|
|
|
|
|
}
|
2026-01-20 17:19:13 -05:00
|
|
|
fn handle_event(&mut self, event: Event) -> Result<Action> {
|
2026-01-19 17:41:46 -05:00
|
|
|
if let Some(key_event) = event.as_key_event() {
|
|
|
|
|
// Handle events here that should not be passed on to the vim emulation handler.
|
2026-01-20 17:19:13 -05:00
|
|
|
match self.handle_key_events(key_event)? {
|
|
|
|
|
Action::Handled => return Ok(Action::Handled),
|
2026-01-19 17:41:46 -05:00
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(mouse_event) = event.as_mouse_event() {
|
2026-01-20 17:19:13 -05:00
|
|
|
match self.handle_mouse_events(mouse_event)? {
|
|
|
|
|
Action::Handled => return Ok(Action::Handled),
|
2026-01-19 17:41:46 -05:00
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-20 17:19:13 -05:00
|
|
|
Ok(Action::Pass)
|
2026-01-19 17:41:46 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-20 17:19:13 -05:00
|
|
|
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> {
|
2026-01-19 17:41:46 -05:00
|
|
|
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 {
|
2026-01-20 17:19:13 -05:00
|
|
|
return Ok(Action::Handled);
|
2026-01-19 17:41:46 -05:00
|
|
|
}
|
2026-01-20 17:19:13 -05:00
|
|
|
Ok(Action::Noop)
|
2026-01-19 17:41:46 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-20 17:19:13 -05:00
|
|
|
fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result<Action> {
|
2026-01-19 17:41:46 -05:00
|
|
|
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 {
|
2026-01-20 17:19:13 -05:00
|
|
|
return Ok(Action::Handled);
|
2026-01-19 17:41:46 -05:00
|
|
|
}
|
2026-01-20 17:19:13 -05:00
|
|
|
Ok(Action::Noop)
|
2026-01-19 17:41:46 -05:00
|
|
|
}
|
2026-01-19 15:03:50 -05:00
|
|
|
}
|