2 Commits

Author SHA1 Message Date
7d4f23d82a Fix bug showing project name in explorer. 2026-02-21 14:24:44 -05:00
b4e14f7f27 Fix bug preventing TUI editors from opening.
Also fix bugs building file tree for paths including `../`.
2026-02-21 14:21:28 -05:00
3 changed files with 72 additions and 34 deletions

1
Cargo.lock generated
View File

@@ -304,7 +304,6 @@ dependencies = [
"syntect", "syntect",
"tui-logger", "tui-logger",
"tui-tree-widget", "tui-tree-widget",
"uuid",
] ]
[[package]] [[package]]

View File

@@ -17,7 +17,6 @@ tui-tree-widget = "0.24.0"
tui-logger = "0.18.1" tui-logger = "0.18.1"
edtui = "0.11.1" edtui = "0.11.1"
strum = "0.27.2" strum = "0.27.2"
uuid = { version = "1.19.0", features = ["v4"] }
devicons = "0.6.12" devicons = "0.6.12"
[build-dependencies] [build-dependencies]

View File

@@ -4,7 +4,7 @@
use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState}; use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::trace; use log::{info, trace};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
use ratatui::layout::{Alignment, Position, Rect}; use ratatui::layout::{Alignment, Position, Rect};
@@ -16,9 +16,53 @@ use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tui_tree_widget::{Tree, TreeItem, TreeState}; use tui_tree_widget::{Tree, TreeItem, TreeState};
#[derive(Debug)]
struct EntryMeta {
abs_path: String,
file_name: String,
is_dir: bool,
}
impl EntryMeta {
/// Normalizes a path, returning an absolute from the root of the filesystem.
/// Does not resolve symlinks and extracts `./` or `../` segments.
fn normalize<P: AsRef<Path>>(p: P) -> PathBuf {
let path = p.as_ref();
let mut buf = PathBuf::new();
for comp in path.components() {
match comp {
std::path::Component::ParentDir => {
buf.pop();
}
std::path::Component::CurDir => {}
_ => buf.push(comp),
}
}
buf
}
fn new<P: AsRef<Path>>(p: P) -> Result<Self> {
let path = p.as_ref();
let is_dir = path.is_dir();
let abs_path = Self::normalize(&path).to_string_lossy().to_string();
let file_name = Path::new(&abs_path)
.file_name()
.context(format!("Failed to get file name for path: {abs_path:?}"))?
.to_string_lossy()
.to_string();
Ok(EntryMeta {
abs_path,
file_name,
is_dir,
})
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct Explorer<'a> { pub struct Explorer<'a> {
pub(crate) root_path: PathBuf, root_path: EntryMeta,
tree_items: TreeItem<'a, String>, tree_items: TreeItem<'a, String>,
tree_state: TreeState<String>, tree_state: TreeState<String>,
pub(crate) component_state: ComponentState, pub(crate) component_state: ComponentState,
@@ -30,7 +74,7 @@ impl<'a> Explorer<'a> {
pub fn new(path: &PathBuf) -> Result<Self> { pub fn new(path: &PathBuf) -> Result<Self> {
trace!(target:Self::ID, "Building {}", Self::ID); trace!(target:Self::ID, "Building {}", Self::ID);
let explorer = Explorer { let explorer = Explorer {
root_path: path.to_owned(), root_path: EntryMeta::new(&path)?,
tree_items: Self::build_tree_from_path(path.to_owned())?, tree_items: Self::build_tree_from_path(path.to_owned())?,
tree_state: TreeState::default(), tree_state: TreeState::default(),
component_state: ComponentState::default().with_help_text(concat!( component_state: ComponentState::default().with_help_text(concat!(
@@ -41,46 +85,46 @@ impl<'a> Explorer<'a> {
Ok(explorer) Ok(explorer)
} }
fn build_tree_from_path(path: PathBuf) -> Result<TreeItem<'static, String>> { /// Builds the file tree from a path using recursion.
/// The identifiers used for the TreeItems are normalized. Symlinks are not resolved.
/// Resolving symlinks would cause collisions on the TreeItem unique identifiers within the set.
fn build_tree_from_path<P: AsRef<Path>>(p: P) -> Result<TreeItem<'static, String>> {
let path = p.as_ref();
let mut children = vec![]; let mut children = vec![];
let clean_path = fs::canonicalize(path)?; let path_meta = EntryMeta::new(path)?;
if let Ok(entries) = fs::read_dir(&clean_path) { if let Ok(entries) = fs::read_dir(&path_meta.abs_path) {
let mut paths = entries let mut paths = entries
.map(|res| res.map(|e| e.path())) .map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, std::io::Error>>() .collect::<Result<Vec<_>, std::io::Error>>()
.context(format!( .context(format!(
"Failed to build vector of paths under directory: {:?}", "Failed to build vector of paths under directory: {:?}",
clean_path &path_meta.abs_path
))?; ))?;
paths.sort(); paths.sort();
for path in paths { for entry_path in paths {
if path.is_dir() { let entry_meta = EntryMeta::new(&entry_path)?;
children.push(Self::build_tree_from_path(path)?); if entry_meta.is_dir {
children.push(Self::build_tree_from_path(&entry_meta.abs_path)?);
} else { } else {
if let Ok(path) = fs::canonicalize(&path) { children.push(TreeItem::new_leaf(
let path_str = path.to_string_lossy().to_string(); entry_meta.abs_path.clone(),
children.push(TreeItem::new_leaf( entry_meta.file_name.clone(),
path_str + uuid::Uuid::new_v4().to_string().as_str(), ));
path.file_name()
.context("Failed to get file name from path.")?
.to_string_lossy()
.to_string(),
));
}
} }
} }
} }
// Note: The first argument is a unique identifier, where no 2 TreeItems may share the same.
// For a file tree this is fine because we shouldn't list the same object twice.
TreeItem::new( TreeItem::new(
clean_path.to_string_lossy().to_string() + uuid::Uuid::new_v4().to_string().as_str(), path_meta.abs_path.clone(),
clean_path path_meta.file_name.clone(),
.file_name()
.context(format!("Failed to get file name from path: {clean_path:?}"))?
.to_string_lossy()
.to_string(),
children, children,
) )
.context(format!("Failed to build tree from path: {clean_path:?}")) .context(format!(
"Failed to build tree from path: {:?}",
path_meta.abs_path
))
} }
pub fn selected(&self) -> Result<String> { pub fn selected(&self) -> Result<String> {
@@ -97,15 +141,11 @@ impl<'a> Explorer<'a> {
impl<'a> Widget for &mut Explorer<'a> { impl<'a> Widget for &mut Explorer<'a> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
if let Ok(tree) = Tree::new(&self.tree_items.children()) { if let Ok(tree) = Tree::new(&self.tree_items.children()) {
let file_name = self
.root_path
.file_name()
.unwrap_or_else(|| OsStr::new("Unknown"));
StatefulWidget::render( StatefulWidget::render(
tree.block( tree.block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(file_name.to_string_lossy()) .title(self.root_path.file_name.clone())
.border_style(Style::default().fg(self.component_state.get_active_color())) .border_style(Style::default().fg(self.component_state.get_active_color()))
.title_style(Style::default().fg(Color::Green)) .title_style(Style::default().fg(Color::Green))
.title_alignment(Alignment::Center), .title_alignment(Alignment::Center),