TUI #1

Merged
shaunrd0 merged 73 commits from ui into master 2026-01-25 20:57:37 +00:00
3 changed files with 135 additions and 46 deletions
Showing only changes of commit e65eb20048 - Show all commits

View File

@ -129,6 +129,16 @@ impl<'a> Widget for &mut App<'a> {
self.explorer.render(horizontal[0], buf); self.explorer.render(horizontal[0], buf);
self.draw_tabs(editor_layout[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); self.editor.render(editor_layout[1], buf);
} }
} }
@ -150,13 +160,14 @@ impl<'a> Component for App<'a> {
match self.handle_key_events(key_event) { match self.handle_key_events(key_event) {
Action::Quit => return Action::Quit, Action::Quit => return Action::Quit,
Action::Handled => { Action::Handled => {
dbg!(format!("Handled event: {:?}", self.id())); // dbg!(format!("Handled event: {:?}", self.id()));
return Action::Handled; 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. // Handle events for all components.
// for component in &mut self.components { // for component in &mut self.components {

View File

@ -17,7 +17,7 @@ use ratatui::widgets::{Block, Borders, Padding, Widget};
pub struct Editor { pub struct Editor {
pub state: EditorState, pub state: EditorState,
pub event_handler: EditorEventHandler, pub event_handler: EditorEventHandler,
file_path: Option<std::path::PathBuf>, pub file_path: Option<std::path::PathBuf>,
} }
impl Editor { impl Editor {
@ -30,15 +30,16 @@ impl Editor {
} }
pub fn set_contents(&mut self, path: &std::path::PathBuf) -> Result<()> { pub fn set_contents(&mut self, path: &std::path::PathBuf) -> Result<()> {
let contents = std::fs::read_to_string(path) if let Ok(contents) = std::fs::read_to_string(path) {
.expect(&format!("Failed to read file contents: {}", path.display()));
let lines: Vec<_> = contents let lines: Vec<_> = contents
.lines() .lines()
.map(|line| line.chars().collect::<Vec<char>>()) .map(|line| line.chars().collect::<Vec<char>>())
.collect(); .collect();
self.file_path = Some(path.clone()); self.file_path = Some(path.clone());
self.state.lines = Lines::new(lines); self.state.lines = Lines::new(lines);
Ok(()) return Ok(());
}
Err(anyhow::Error::msg("Failed to set editor file contents"))
} }
pub fn save(&self) -> Result<()> { 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 anyhow::Result;
use ratatui::buffer::Buffer; 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::prelude::Style;
use ratatui::style::Color; use ratatui::style::{Color, Modifier};
use ratatui::widgets::{Block, Borders, Widget}; use ratatui::widgets::{Block, Borders, StatefulWidget};
use std::fs; use std::fs;
use tui_tree_widget::{Tree, TreeItem}; use tui_tree_widget::{Tree, TreeItem, TreeState};
use uuid::Uuid;
#[derive(Clone, Debug)] #[derive(Debug)]
pub struct Explorer<'a> { pub struct Explorer<'a> {
root_path: std::path::PathBuf, root_path: std::path::PathBuf,
tree_items: TreeItem<'a, String>, tree_items: TreeItem<'a, String>,
tree_state: TreeState<String>,
} }
impl<'a> Explorer<'a> { impl<'a> Explorer<'a> {
@ -20,6 +21,7 @@ impl<'a> Explorer<'a> {
let explorer = Explorer { let explorer = Explorer {
root_path: path.to_owned(), root_path: path.to_owned(),
tree_items: Self::build_tree_from_path(path.to_owned()), tree_items: Self::build_tree_from_path(path.to_owned()),
tree_state: TreeState::default(),
}; };
explorer explorer
} }
@ -36,8 +38,10 @@ impl<'a> Explorer<'a> {
if path.is_dir() { if path.is_dir() {
children.push(Self::build_tree_from_path(path)); children.push(Self::build_tree_from_path(path));
} else { } else {
if let Ok(path) = std::path::absolute(&path) {
let path_str = path.to_string_lossy().to_string();
children.push(TreeItem::new_leaf( children.push(TreeItem::new_leaf(
Uuid::new_v4().to_string(), path_str,
path.file_name() path.file_name()
.expect("Failed to get file name from path.") .expect("Failed to get file name from path.")
.to_string_lossy() .to_string_lossy()
@ -46,9 +50,20 @@ impl<'a> Explorer<'a> {
} }
} }
} }
}
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( TreeItem::new(
Uuid::new_v4().to_string(), abs,
path.file_name() path.file_name()
.expect("Failed to get file name from path.") .expect("Failed to get file name from path.")
.to_string_lossy() .to_string_lossy()
@ -57,10 +72,9 @@ impl<'a> Explorer<'a> {
) )
.expect("Failed to build tree from path.") .expect("Failed to build tree from path.")
} }
}
impl<'a> Widget for &Explorer<'a> { pub fn render(&mut self, area: Rect, buf: &mut Buffer) {
fn render(self, area: Rect, buf: &mut Buffer) { StatefulWidget::render(
Tree::new(&self.tree_items.children()) Tree::new(&self.tree_items.children())
.expect("Failed to build tree.") .expect("Failed to build tree.")
.style(Style::default()) .style(Style::default())
@ -76,7 +90,20 @@ impl<'a> Widget for &Explorer<'a> {
.title_style(Style::default().fg(Color::Green)) .title_style(Style::default().fg(Color::Green))
.title_alignment(Alignment::Center), .title_alignment(Alignment::Center),
) )
.render(area, buf); .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 { fn id(&self) -> &str {
"explorer" "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
}
} }