[tui] File explorer controls editor contents.

This commit is contained in:
Shaun Reed 2026-01-19 17:41:46 -05:00
parent f10d4cd41d
commit e65eb20048
3 changed files with 135 additions and 46 deletions

View File

@ -129,6 +129,16 @@ impl<'a> Widget for &mut App<'a> {
self.explorer.render(horizontal[0], buf);
self.draw_tabs(editor_layout[0], buf);
if let Some(editor) = self.editor.file_path.clone() {
let editor_abs = std::path::absolute(editor).unwrap();
if let Some(selected) = self.explorer.selected() {
let selected_abs = std::path::absolute(selected).unwrap();
if selected_abs.is_file() && selected_abs != editor_abs {
self.editor.set_contents(&selected_abs).ok();
}
}
}
self.editor.render(editor_layout[1], buf);
}
}
@ -150,13 +160,14 @@ impl<'a> Component for App<'a> {
match self.handle_key_events(key_event) {
Action::Quit => return Action::Quit,
Action::Handled => {
dbg!(format!("Handled event: {:?}", self.id()));
// dbg!(format!("Handled event: {:?}", self.id()));
return Action::Handled;
}
_ => {}
}
}
self.editor.handle_event(event);
self.explorer.handle_event(event.clone());
self.editor.handle_event(event.clone());
// Handle events for all components.
// for component in &mut self.components {

View File

@ -17,7 +17,7 @@ use ratatui::widgets::{Block, Borders, Padding, Widget};
pub struct Editor {
pub state: EditorState,
pub event_handler: EditorEventHandler,
file_path: Option<std::path::PathBuf>,
pub file_path: Option<std::path::PathBuf>,
}
impl Editor {
@ -30,15 +30,16 @@ impl Editor {
}
pub fn set_contents(&mut self, path: &std::path::PathBuf) -> Result<()> {
let contents = std::fs::read_to_string(path)
.expect(&format!("Failed to read file contents: {}", path.display()));
let lines: Vec<_> = contents
.lines()
.map(|line| line.chars().collect::<Vec<char>>())
.collect();
self.file_path = Some(path.clone());
self.state.lines = Lines::new(lines);
Ok(())
if let Ok(contents) = std::fs::read_to_string(path) {
let lines: Vec<_> = contents
.lines()
.map(|line| line.chars().collect::<Vec<char>>())
.collect();
self.file_path = Some(path.clone());
self.state.lines = Lines::new(lines);
return Ok(());
}
Err(anyhow::Error::msg("Failed to set editor file contents"))
}
pub fn save(&self) -> Result<()> {

View File

@ -1,18 +1,19 @@
use crate::tui::component::Component;
use crate::tui::component::{Action, Component};
use anyhow::Result;
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Rect};
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
use ratatui::layout::{Alignment, Position, Rect};
use ratatui::prelude::Style;
use ratatui::style::Color;
use ratatui::widgets::{Block, Borders, Widget};
use ratatui::style::{Color, Modifier};
use ratatui::widgets::{Block, Borders, StatefulWidget};
use std::fs;
use tui_tree_widget::{Tree, TreeItem};
use uuid::Uuid;
use tui_tree_widget::{Tree, TreeItem, TreeState};
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct Explorer<'a> {
root_path: std::path::PathBuf,
tree_items: TreeItem<'a, String>,
tree_state: TreeState<String>,
}
impl<'a> Explorer<'a> {
@ -20,6 +21,7 @@ impl<'a> Explorer<'a> {
let explorer = Explorer {
root_path: path.to_owned(),
tree_items: Self::build_tree_from_path(path.to_owned()),
tree_state: TreeState::default(),
};
explorer
}
@ -36,19 +38,32 @@ impl<'a> Explorer<'a> {
if path.is_dir() {
children.push(Self::build_tree_from_path(path));
} else {
children.push(TreeItem::new_leaf(
Uuid::new_v4().to_string(),
path.file_name()
.expect("Failed to get file name from path.")
.to_string_lossy()
.to_string(),
));
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(
Uuid::new_v4().to_string(),
abs,
path.file_name()
.expect("Failed to get file name from path.")
.to_string_lossy()
@ -57,26 +72,38 @@ impl<'a> Explorer<'a> {
)
.expect("Failed to build tree from path.")
}
}
impl<'a> Widget for &Explorer<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
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),
)
.render(area, buf);
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) -> Option<&String> {
self.tree_state.selected().last()
}
}
@ -84,4 +111,54 @@ 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
}
}