[tui] Add File MenuBar options Reload and Close.

This commit is contained in:
Shaun Reed 2026-01-25 12:04:31 -05:00
parent 6c2f3f9005
commit fa36a633ee
5 changed files with 150 additions and 28 deletions

View File

@ -99,7 +99,9 @@ impl<'a> App<'a> {
AppEditor => match self.editor_tabs.current_editor() {
Some(editor) => editor.component_state.help_text.clone(),
None => {
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()
}
},
@ -242,14 +244,12 @@ impl<'a> Component for App<'a> {
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.
if let Some(mouse) = event.as_mouse_event() {
if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
if let Some(editor) = self.editor_tabs.current_editor_mut() {
editor.handle_mouse_events(mouse)?;
}
self.explorer.handle_mouse_events(mouse)?;
self.logger.handle_mouse_events(mouse)?;
}
@ -257,13 +257,20 @@ impl<'a> Component for App<'a> {
match action {
Action::Quit | Action::Handled => Ok(action),
Action::Save => match editor.save() {
Action::Save => match self.editor_tabs.current_editor_mut() {
None => {
error!(target:Self::id(), "Failed to get current editor while handling App Action::Save");
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 => {
if let Ok(path) = self.explorer.selected() {
let path_buf = PathBuf::from(path);
@ -273,6 +280,22 @@ impl<'a> Component for App<'a> {
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),
}
}

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.
Handled,
OpenTab,
ReloadFile,
ShowHideExplorer,
ShowHideLogger,
About,
CloseTab,
}
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<()> {
trace!(target:Self::id(), "Setting Editor contents from path {:?}", 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 anyhow::{Context, Result};
use log::{trace, warn};
use anyhow::{Context, Result, anyhow};
use log::{error, info, trace, warn};
use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use ratatui::layout::Rect;
@ -36,6 +36,7 @@ impl EditorTab {
pub fn next_editor(&mut self) {
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);
self.set_tab_focus(Focus::Active, next);
self.current_editor = next;
}
@ -45,15 +46,64 @@ impl EditorTab {
.checked_sub(1)
.unwrap_or(self.tab_order.len() - 1);
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;
}
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> {
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> {
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<()> {
@ -75,6 +125,35 @@ impl EditorTab {
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) {
// TODO: Only file name is displayed in tab title, so files with the same name in different
// directories will appear confusing.
@ -119,9 +198,10 @@ impl Component for EditorTab {
_ => {}
}
}
self.current_editor_mut()
.context("Failed to get current editor")?
.handle_event(event)
if let Some(editor) = self.current_editor_mut() {
return editor.handle_event(event);
}
Ok(Action::Noop)
}
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::menu_bar::MenuBarItemOption::{
About, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger,
About, CloseTab, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger,
};
use log::trace;
use ratatui::buffer::Buffer;
@ -23,6 +23,7 @@ enum MenuBarItem {
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr, EnumIter)]
enum MenuBarItemOption {
Save,
CloseTab,
Reload,
Exit,
ShowHideExplorer,
@ -39,6 +40,7 @@ impl MenuBarItemOption {
ShowHideExplorer => "Show / hide explorer",
ShowHideLogger => "Show / hide logger",
About => "About",
CloseTab => "Close tab",
}
}
}
@ -66,7 +68,7 @@ impl MenuBarItem {
pub fn options(&self) -> &[MenuBarItemOption] {
match self {
MenuBarItem::File => &[Save, Reload, Exit],
MenuBarItem::File => &[Save, CloseTab, Reload, Exit],
MenuBarItem::View => &[ShowHideExplorer, ShowHideLogger],
MenuBarItem::Help => &[About],
}
@ -145,14 +147,14 @@ impl MenuBar {
}
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 {
x: anchor.x,
y: anchor.y + anchor.height,
width: width.min(area.width),
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
}
}
@ -190,14 +192,15 @@ impl Component for MenuBar {
}
KeyCode::Enter => {
if let Some(selected) = self.list_state.selected() {
let seletion = self.selected.options()[selected];
return match seletion {
let selection = self.selected.options()[selected];
return match selection {
Save => Ok(Action::Save),
Exit => Ok(Action::Quit),
Reload => Ok(Action::Noop), // TODO
ShowHideExplorer => Ok(Action::Noop), // TODO
ShowHideLogger => Ok(Action::Noop), // TODO
About => Ok(Action::Noop), // TODO
Reload => Ok(Action::ReloadFile),
ShowHideExplorer => Ok(Action::ShowHideExplorer),
ShowHideLogger => Ok(Action::ShowHideLogger),
About => Ok(Action::About),
CloseTab => Ok(Action::CloseTab),
};
}
Ok(Action::Noop)