From b4e14f7f27d8e0053c154e9e01aa0daadb06f553 Mon Sep 17 00:00:00 2001 From: Shaun Reed Date: Sat, 21 Feb 2026 14:21:04 -0500 Subject: [PATCH] Fix bug preventing TUI editors from opening. Also fix bugs building file tree for paths including `../`. --- Cargo.lock | 1 - Cargo.toml | 1 - src/tui/explorer.rs | 95 ++++++++++++++++++++++++++++++++------------- 3 files changed, 69 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55a5491..d77a4e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,7 +304,6 @@ dependencies = [ "syntect", "tui-logger", "tui-tree-widget", - "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 237e11d..320aeab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ tui-tree-widget = "0.24.0" tui-logger = "0.18.1" edtui = "0.11.1" strum = "0.27.2" -uuid = { version = "1.19.0", features = ["v4"] } devicons = "0.6.12" [build-dependencies] diff --git a/src/tui/explorer.rs b/src/tui/explorer.rs index cf295bb..cf03cae 100644 --- a/src/tui/explorer.rs +++ b/src/tui/explorer.rs @@ -4,7 +4,7 @@ use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState}; use anyhow::{Context, Result, bail}; -use log::trace; +use log::{info, trace}; use ratatui::buffer::Buffer; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind}; use ratatui::layout::{Alignment, Position, Rect}; @@ -18,12 +18,55 @@ use tui_tree_widget::{Tree, TreeItem, TreeState}; #[derive(Debug)] pub struct Explorer<'a> { - pub(crate) root_path: PathBuf, + root_path: PathBuf, tree_items: TreeItem<'a, String>, tree_state: TreeState, pub(crate) component_state: ComponentState, } +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: 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: P) -> Result { + 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, + }) + } +} + impl<'a> Explorer<'a> { pub const ID: &'static str = "Explorer"; @@ -41,46 +84,46 @@ impl<'a> Explorer<'a> { Ok(explorer) } - fn build_tree_from_path(path: PathBuf) -> Result> { + /// 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: P) -> Result> { + let path = p.as_ref(); let mut children = vec![]; - let clean_path = fs::canonicalize(path)?; - if let Ok(entries) = fs::read_dir(&clean_path) { + let path_meta = EntryMeta::new(path)?; + if let Ok(entries) = fs::read_dir(&path_meta.abs_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: {:?}", - clean_path + &path_meta.abs_path ))?; paths.sort(); - for path in paths { - if path.is_dir() { - children.push(Self::build_tree_from_path(path)?); + for entry_path in paths { + let entry_meta = EntryMeta::new(&entry_path)?; + if entry_meta.is_dir { + children.push(Self::build_tree_from_path(&entry_meta.abs_path)?); } else { - if let Ok(path) = fs::canonicalize(&path) { - let path_str = path.to_string_lossy().to_string(); - children.push(TreeItem::new_leaf( - 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(), - )); - } + children.push(TreeItem::new_leaf( + entry_meta.abs_path.clone(), + entry_meta.file_name.clone(), + )); } } } + // 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( - clean_path.to_string_lossy().to_string() + uuid::Uuid::new_v4().to_string().as_str(), - clean_path - .file_name() - .context(format!("Failed to get file name from path: {clean_path:?}"))? - .to_string_lossy() - .to_string(), + path_meta.abs_path.clone(), + path_meta.file_name.clone(), 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 {