[tui] Add File MenuBar options Reload and Close.
This commit is contained in:
parent
6c2f3f9005
commit
fa36a633ee
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user