TUI #1

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

View File

@ -99,7 +99,9 @@ impl<'a> App<'a> {
AppEditor => match self.editor_tabs.current_editor() { AppEditor => match self.editor_tabs.current_editor() {
Some(editor) => editor.component_state.help_text.clone(), Some(editor) => editor.component_state.help_text.clone(),
None => { None => {
error!(target:Self::id(), "Failed to get Editor while drawing bottom status bar"); if !self.editor_tabs.is_empty() {
error!(target:Self::id(), "Failed to get Editor while drawing bottom status bar");
}
"Failed to get current Editor while getting widget help text".to_string() "Failed to get current Editor while getting widget help text".to_string()
} }
}, },
@ -242,14 +244,12 @@ impl<'a> Component for App<'a> {
AppMenuBar => self.menu_bar.handle_event(event.clone())?, AppMenuBar => self.menu_bar.handle_event(event.clone())?,
}; };
let editor = self
.editor_tabs
.current_editor_mut()
.context("Failed to get current editor while handling App events")?;
// Components should always handle mouse events for click interaction. // Components should always handle mouse events for click interaction.
if let Some(mouse) = event.as_mouse_event() { if let Some(mouse) = event.as_mouse_event() {
if mouse.kind == MouseEventKind::Down(MouseButton::Left) { if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
editor.handle_mouse_events(mouse)?; if let Some(editor) = self.editor_tabs.current_editor_mut() {
editor.handle_mouse_events(mouse)?;
}
self.explorer.handle_mouse_events(mouse)?; self.explorer.handle_mouse_events(mouse)?;
self.logger.handle_mouse_events(mouse)?; self.logger.handle_mouse_events(mouse)?;
} }
@ -257,13 +257,20 @@ impl<'a> Component for App<'a> {
match action { match action {
Action::Quit | Action::Handled => Ok(action), Action::Quit | Action::Handled => Ok(action),
Action::Save => match editor.save() { Action::Save => match self.editor_tabs.current_editor_mut() {
Ok(_) => Ok(Action::Handled), None => {
Err(e) => { error!(target:Self::id(), "Failed to get current editor while handling App Action::Save");
error!(target:Self::id(), "Failed to save editor contents: {e}");
Ok(Action::Noop) Ok(Action::Noop)
} }
Some(editor) => match editor.save() {
Ok(_) => Ok(Action::Handled),
Err(e) => {
error!(target:Self::id(), "Failed to save editor contents: {e}");
Ok(Action::Noop)
}
},
}, },
Action::OpenTab => { Action::OpenTab => {
if let Ok(path) = self.explorer.selected() { if let Ok(path) = self.explorer.selected() {
let path_buf = PathBuf::from(path); let path_buf = PathBuf::from(path);
@ -273,6 +280,22 @@ impl<'a> Component for App<'a> {
Ok(Action::Noop) Ok(Action::Noop)
} }
} }
Action::CloseTab => match self.editor_tabs.close_current_tab() {
Ok(_) => Ok(Action::Handled),
Err(_) => Ok(Action::Noop),
},
Action::ReloadFile => {
trace!(target:Self::id(), "Reloading file for current editor");
if let Some(editor) = self.editor_tabs.current_editor_mut() {
editor
.reload_contents()
.map(|_| Action::Handled)
.context("Failed to handle Action::ReloadFile")
} else {
error!(target:Self::id(), "Failed to get current editor while handling App Action::ReloadFile");
Ok(Action::Noop)
}
}
_ => Ok(Action::Noop), _ => Ok(Action::Noop),
} }
} }

View File

@ -22,6 +22,11 @@ pub enum Action {
/// The input was handled by a Component and should not be passed to the next component. /// The input was handled by a Component and should not be passed to the next component.
Handled, Handled,
OpenTab, OpenTab,
ReloadFile,
ShowHideExplorer,
ShowHideLogger,
About,
CloseTab,
} }
pub trait Component { pub trait Component {

View File

@ -38,6 +38,17 @@ impl Editor {
} }
} }
pub fn reload_contents(&mut self) -> Result<()> {
trace!(target:Self::id(), "Reloading editor file contents {:?}", self.file_path);
match self.file_path.clone() {
None => {
error!(target:Self::id(), "Failed to reload editor contents with None file_path");
bail!("Failed to reload editor contents with None file_path")
}
Some(path) => self.set_contents(&path),
}
}
pub fn set_contents(&mut self, path: &std::path::PathBuf) -> Result<()> { pub fn set_contents(&mut self, path: &std::path::PathBuf) -> Result<()> {
trace!(target:Self::id(), "Setting Editor contents from path {:?}", path); trace!(target:Self::id(), "Setting Editor contents from path {:?}", path);
if let Ok(contents) = std::fs::read_to_string(path) { if let Ok(contents) = std::fs::read_to_string(path) {

View File

@ -1,7 +1,7 @@
use crate::tui::component::{Action, Component}; use crate::tui::component::{Action, Component, Focus, FocusState};
use crate::tui::editor::Editor; use crate::tui::editor::Editor;
use anyhow::{Context, Result}; use anyhow::{Context, Result, anyhow};
use log::{trace, warn}; use log::{error, info, trace, warn};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use ratatui::layout::Rect; use ratatui::layout::Rect;
@ -36,6 +36,7 @@ impl EditorTab {
pub fn next_editor(&mut self) { pub fn next_editor(&mut self) {
let next = (self.current_editor + 1) % self.tab_order.len(); let next = (self.current_editor + 1) % self.tab_order.len();
trace!(target:Self::id(), "Moving from {} to next editor tab at {}", self.current_editor, next); trace!(target:Self::id(), "Moving from {} to next editor tab at {}", self.current_editor, next);
self.set_tab_focus(Focus::Active, next);
self.current_editor = next; self.current_editor = next;
} }
@ -45,15 +46,64 @@ impl EditorTab {
.checked_sub(1) .checked_sub(1)
.unwrap_or(self.tab_order.len() - 1); .unwrap_or(self.tab_order.len() - 1);
trace!(target:Self::id(), "Moving from {} to previous editor tab at {}", self.current_editor, prev); trace!(target:Self::id(), "Moving from {} to previous editor tab at {}", self.current_editor, prev);
self.set_tab_focus(Focus::Active, prev);
self.current_editor = prev; self.current_editor = prev;
} }
pub fn get_editor_key(&self, index: usize) -> Option<String> {
match self.tab_order.get(index) {
None => {
if !self.tab_order.is_empty() {
error!(target:Self::id(), "Failed to get editor tab key with invalid index {index}");
}
None
}
Some(key) => Some(key.to_owned()),
}
}
pub fn current_editor(&self) -> Option<&Editor> { pub fn current_editor(&self) -> Option<&Editor> {
self.editors.get(&self.tab_order[self.current_editor]) self.editors.get(&self.get_editor_key(self.current_editor)?)
} }
pub fn current_editor_mut(&mut self) -> Option<&mut Editor> { pub fn current_editor_mut(&mut self) -> Option<&mut Editor> {
self.editors.get_mut(&self.tab_order[self.current_editor]) self.editors
.get_mut(&self.get_editor_key(self.current_editor)?)
}
pub fn set_current_tab_focus(&mut self, focus: Focus) {
trace!(target:Self::id(), "Setting current tab {} focus to {:?}", self.current_editor, focus);
self.set_tab_focus(focus, self.current_editor)
}
pub fn set_tab_focus(&mut self, focus: Focus, index: usize) {
trace!(target:Self::id(), "Setting tab {} focus to {:?}", index, focus);
if focus == Focus::Active && index != self.current_editor {
// If we are setting another tab to active, disable the current one.
trace!(
target:Self::id(),
"New tab {} focus set to Active; Setting current tab {} to Inactive",
index,
self.current_editor
);
self.set_current_tab_focus(Focus::Inactive);
}
match self.get_editor_key(index) {
None => {
error!(target:Self::id(), "Failed setting tab focus for invalid key {index}");
}
Some(key) => match self.editors.get_mut(&key) {
None => {
error!(
target:Self::id(),
"Failed to update tab focus at index {} with invalid key: {}",
self.current_editor,
self.tab_order[self.current_editor]
)
}
Some(editor) => editor.component_state.set_focus(focus),
},
}
} }
pub fn open_tab(&mut self, path: &std::path::PathBuf) -> Result<()> { pub fn open_tab(&mut self, path: &std::path::PathBuf) -> Result<()> {
@ -75,6 +125,35 @@ impl EditorTab {
Ok(()) Ok(())
} }
pub fn close_current_tab(&mut self) -> Result<()> {
self.close_tab(self.current_editor)
}
pub fn close_tab(&mut self, index: usize) -> Result<()> {
let key = self
.tab_order
.get(index)
.ok_or(anyhow!(
"Failed to get tab order with invalid index {index}"
))?
.to_owned();
match self.editors.remove(&key) {
None => {
error!(target:Self::id(), "Failed to remove editor tab {key} with invalid index {index}")
}
Some(_) => {
self.prev_editor();
self.tab_order.remove(index);
info!(target:Self::id(), "Closed editor tab {key} at index {index}")
}
}
Ok(())
}
pub fn is_empty(&self) -> bool {
self.editors.is_empty()
}
pub fn render(&mut self, tabs_area: Rect, editor_area: Rect, buf: &mut Buffer) { pub fn render(&mut self, tabs_area: Rect, editor_area: Rect, buf: &mut Buffer) {
// TODO: Only file name is displayed in tab title, so files with the same name in different // TODO: Only file name is displayed in tab title, so files with the same name in different
// directories will appear confusing. // directories will appear confusing.
@ -119,9 +198,10 @@ impl Component for EditorTab {
_ => {} _ => {}
} }
} }
self.current_editor_mut() if let Some(editor) = self.current_editor_mut() {
.context("Failed to get current editor")? return editor.handle_event(event);
.handle_event(event) }
Ok(Action::Noop)
} }
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> { fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> {

View File

@ -1,6 +1,6 @@
use crate::tui::component::{Action, Component, ComponentState, FocusState}; use crate::tui::component::{Action, Component, ComponentState, FocusState};
use crate::tui::menu_bar::MenuBarItemOption::{ use crate::tui::menu_bar::MenuBarItemOption::{
About, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger, About, CloseTab, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger,
}; };
use log::trace; use log::trace;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
@ -23,6 +23,7 @@ enum MenuBarItem {
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr, EnumIter)] #[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr, EnumIter)]
enum MenuBarItemOption { enum MenuBarItemOption {
Save, Save,
CloseTab,
Reload, Reload,
Exit, Exit,
ShowHideExplorer, ShowHideExplorer,
@ -39,6 +40,7 @@ impl MenuBarItemOption {
ShowHideExplorer => "Show / hide explorer", ShowHideExplorer => "Show / hide explorer",
ShowHideLogger => "Show / hide logger", ShowHideLogger => "Show / hide logger",
About => "About", About => "About",
CloseTab => "Close tab",
} }
} }
} }
@ -66,7 +68,7 @@ impl MenuBarItem {
pub fn options(&self) -> &[MenuBarItemOption] { pub fn options(&self) -> &[MenuBarItemOption] {
match self { match self {
MenuBarItem::File => &[Save, Reload, Exit], MenuBarItem::File => &[Save, CloseTab, Reload, Exit],
MenuBarItem::View => &[ShowHideExplorer, ShowHideLogger], MenuBarItem::View => &[ShowHideExplorer, ShowHideLogger],
MenuBarItem::Help => &[About], MenuBarItem::Help => &[About],
} }
@ -145,14 +147,14 @@ impl MenuBar {
} }
fn rect_under_option(anchor: Rect, area: Rect, width: u16, height: u16) -> Rect { fn rect_under_option(anchor: Rect, area: Rect, width: u16, height: u16) -> Rect {
// TODO: X offset for item option? It's fine as-is, but it might look nicer.
let rect = Rect { let rect = Rect {
x: anchor.x, x: anchor.x,
y: anchor.y + anchor.height, y: anchor.y + anchor.height,
width: width.min(area.width), width: width.min(area.width),
height, height,
}; };
trace!(target:Self::id(), "Building Rect under MenuBar popup {}", rect); // TODO: X offset for item option? It's fine as-is, but it might look nicer.
// trace!(target:Self::id(), "Building Rect under MenuBar popup {}", rect);
rect rect
} }
} }
@ -190,14 +192,15 @@ impl Component for MenuBar {
} }
KeyCode::Enter => { KeyCode::Enter => {
if let Some(selected) = self.list_state.selected() { if let Some(selected) = self.list_state.selected() {
let seletion = self.selected.options()[selected]; let selection = self.selected.options()[selected];
return match seletion { return match selection {
Save => Ok(Action::Save), Save => Ok(Action::Save),
Exit => Ok(Action::Quit), Exit => Ok(Action::Quit),
Reload => Ok(Action::Noop), // TODO Reload => Ok(Action::ReloadFile),
ShowHideExplorer => Ok(Action::Noop), // TODO ShowHideExplorer => Ok(Action::ShowHideExplorer),
ShowHideLogger => Ok(Action::Noop), // TODO ShowHideLogger => Ok(Action::ShowHideLogger),
About => Ok(Action::Noop), // TODO About => Ok(Action::About),
CloseTab => Ok(Action::CloseTab),
}; };
} }
Ok(Action::Noop) Ok(Action::Noop)