1 Commits

Author SHA1 Message Date
00f9075d0f Add basic TUI support (#1) 2026-01-25 20:57:36 +00:00
13 changed files with 642 additions and 129 deletions

3
.cargo/config.toml Normal file
View File

@@ -0,0 +1,3 @@
[build]
rustflags = [ "-C", "link-arg=-fuse-ld=lld", ]

View File

@@ -1,15 +1,10 @@
# CLIDE # CLIDE
CLIDE is a barebones but extendable IDE written in Rust using the Qt UI framework that supports both full and headless Linux environments. CLIDE is an extendable command-line driven development environment written in Rust using the Qt UI framework that supports both full and headless Linux environments.
The core application will provide you with a text editor that can be extended with plugins written in Rust. The GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate.
The UI is written in QML and compiled to C++ using `cxx`, which is then linked into the Rust application. It's up to you to build your own development environment for your tools. Plugins are planned to be supported in the future for bringing your own language-specific tools or features.
Once you've created your plugin, you can submit a pull request to add a link to the git repository for your plugin to the final section in this README if you'd like to contribute.
It's up to you to build your own development environment for your tools.
This project is intended to be a light-weight core application with no language-specific tools or features.
To add tools for your purposes, create a plugin that implements the `ClidePlugin` trait. (This is currently under development and not yet available.)
Once you've created your plugin, you can submit a pull request to add your plugin to the final section in this README if you'd like to contribute.
If this section becomes too large, we may explore other options to distribute plugins.
The following packages must be installed before the application will build. The following packages must be installed before the application will build.
In the future, we may provide a minimal installation option that only includes dependencies for the headless TUI. In the future, we may provide a minimal installation option that only includes dependencies for the headless TUI.
@@ -24,6 +19,68 @@ And of course, [Rust](https://www.rust-lang.org/tools/install).
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
``` ```
## Usage
To install and run clide
```bash
git clone https://git.shaunreed.com/shaunrd0/clide
cd clide
cargo install --path .
```
After installation `clide` can be used directly
```bash
clide --help
Extendable command-line driven development environment written in Rust using the Qt UI framework.
If no flags are provided, the GUI editor is launched in a separate process.
If no path is provided, the current directory is used.
Usage: clide [OPTIONS] [PATH]
Arguments:
[PATH] The root directory for the project to open with the clide editor
Options:
-t, --tui Run clide in headless mode
-g, --gui Run the clide GUI in the current process, blocking the terminal and showing all output streams
-h, --help Print help
```
### TUI
The TUI is implemented using the ratatui crate and has the typical features you would expect from a text editor.
You can browse your project tree, open / close new editor tabs, and save / reload files.
Controls for the TUI are listed at the bottom of the window, and update depending on which widget you have focused.
For now, there are no language-specific features or plugins available for the TUI it is only a text editor.
To run the TUI, pass the `-t` or `--tui` flags.
```bash
# With cargo from the project root
cargo run -- -t
# Or via clide directly after installation
clide -t
```
![image](./resources/tui.png)
### GUI
The GUI is still in development. It is at this point a text viewer, instead of a text editor.
There are many placeholder buttons and features in the GUI that do nothing when used.
The GUI is run by default when executing the `clide` application.
```bash
# With cargo from the project root
cargo run
# Or via clide directly after installation
clide
```
## Development ## Development
It's recommended to use RustRover or Qt Creator for development. It's recommended to use RustRover or Qt Creator for development.

BIN
resources/tui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

View File

@@ -6,7 +6,7 @@ use std::process::{Command, Stdio};
pub mod gui; pub mod gui;
pub mod tui; pub mod tui;
/// Command line interface IDE with full GUI and headless modes. /// Extendable command-line driven development environment written in Rust using the Qt UI framework.
/// If no flags are provided, the GUI editor is launched in a separate process. /// If no flags are provided, the GUI editor is launched in a separate process.
/// If no path is provided, the current directory is used. /// If no path is provided, the current directory is used.
#[derive(Parser, Debug)] #[derive(Parser, Debug)]

View File

@@ -1,3 +1,4 @@
mod about;
mod app; mod app;
mod component; mod component;
mod editor; mod editor;
@@ -7,7 +8,7 @@ mod logger;
mod menu_bar; mod menu_bar;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::{LevelFilter, debug, info}; use log::{LevelFilter, debug, info, trace};
use ratatui::Terminal; use ratatui::Terminal;
use ratatui::backend::CrosstermBackend; use ratatui::backend::CrosstermBackend;
use ratatui::crossterm::event::{ use ratatui::crossterm::event::{
@@ -28,10 +29,15 @@ pub struct Tui {
} }
impl Tui { impl Tui {
pub fn id() -> &'static str {
"Tui"
}
pub fn new(root_path: std::path::PathBuf) -> Result<Self> { pub fn new(root_path: std::path::PathBuf) -> Result<Self> {
trace!(target:Self::id(), "Building {}", Self::id());
init_logger(LevelFilter::Trace)?; init_logger(LevelFilter::Trace)?;
set_default_level(LevelFilter::Trace); set_default_level(LevelFilter::Trace);
debug!(target:"Tui", "Logging initialized"); debug!(target:Self::id(), "Logging initialized");
let mut dir = env::temp_dir(); let mut dir = env::temp_dir();
dir.push("clide.log"); dir.push("clide.log");
@@ -43,7 +49,7 @@ impl Tui {
.output_file(false) .output_file(false)
.output_separator(':'); .output_separator(':');
set_log_file(file_options); set_log_file(file_options);
debug!(target:"Tui", "Logging to file: {dir:?}"); debug!(target:Self::id(), "Logging to file: {dir:?}");
Ok(Self { Ok(Self {
terminal: Terminal::new(CrosstermBackend::new(stdout()))?, terminal: Terminal::new(CrosstermBackend::new(stdout()))?,
@@ -52,7 +58,7 @@ impl Tui {
} }
pub fn start(self) -> Result<()> { pub fn start(self) -> Result<()> {
info!(target:"Tui", "Starting the TUI editor at {:?}", self.root_path); info!(target:Self::id(), "Starting the TUI editor at {:?}", self.root_path);
ratatui::crossterm::execute!( ratatui::crossterm::execute!(
stdout(), stdout(),
EnterAlternateScreen, EnterAlternateScreen,
@@ -69,7 +75,7 @@ impl Tui {
} }
fn stop() -> Result<()> { fn stop() -> Result<()> {
info!(target:"Tui", "Stopping the TUI editor"); info!(target:Self::id(), "Stopping the TUI editor");
disable_raw_mode()?; disable_raw_mode()?;
ratatui::crossterm::execute!( ratatui::crossterm::execute!(
stdout(), stdout(),

138
src/tui/about.rs Normal file
View File

@@ -0,0 +1,138 @@
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap};
pub struct About {}
impl About {
#[allow(unused)]
pub fn id() -> &'static str {
"About"
}
pub fn new() -> Self {
// trace!(target:Self::id(), "Building {}", Self::id());
Self {}
}
}
impl Widget for About {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
Clear::default().render(area, buf);
// Split main area
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Fill(2), // image column
Constraint::Fill(1), // image column
Constraint::Fill(2), // text column
])
.split(area);
let top_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Fill(1),
Constraint::Fill(3),
Constraint::Fill(1),
])
.split(chunks[1]);
let bottom_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Fill(1),
Constraint::Fill(3),
Constraint::Fill(1),
])
.split(chunks[2]);
// ---------- IMAGE ----------
let kilroy_art = [
" * ",
" |.===. ",
" {}o o{} ",
"-----------------------ooO--(_)--Ooo---------------------------",
"# #",
"# CLIDE WAS HERE #",
"# #",
"# https://git.shaunreed.com/shaunred/clide #",
"# https://shaunreed.com/shaunred/clide #",
"# #",
];
let kilroy_lines: Vec<Line> = kilroy_art
.iter()
.map(|l| Line::from(Span::raw(*l)))
.collect();
Paragraph::new(kilroy_lines)
.block(
Block::default()
.borders(Borders::NONE)
.padding(Padding::bottom(0)),
)
.wrap(Wrap { trim: false })
.centered()
.render(top_chunks[1], buf);
// ---------- TEXT ----------
let about_text = vec![
Line::from(vec![Span::styled(
"clide\n",
Style::default().add_modifier(Modifier::BOLD),
)])
.centered(),
Line::from(""),
Line::from(vec![
Span::styled("Author: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("Shaun Reed"),
])
.left_aligned(),
Line::from(vec![
Span::styled("Email: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("shaunrd0@gmail.com"),
])
.left_aligned(),
Line::from(vec![
Span::styled("URL: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("https://git.shaunreed.com/shaunrd0/clide"),
])
.left_aligned(),
Line::from(vec![
Span::styled("Blog: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("https://shaunreed.com"),
])
.left_aligned(),
Line::from(""),
Line::from(vec![Span::styled(
"Description\n",
Style::default().add_modifier(Modifier::BOLD),
)])
.left_aligned(),
Line::from(concat!(
"CLIDE is an extendable command-line driven development environment written in Rust using the Qt UI framework that supports both full and headless Linux environments. ",
"The GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate. ",
))
.style(Style::default())
.left_aligned(),
];
Block::bordered().render(area, buf);
let paragraph = Paragraph::new(about_text)
.block(
Block::default()
.title("About")
.borders(Borders::ALL)
.padding(Padding::top(0)),
)
.wrap(Wrap { trim: true });
paragraph.render(bottom_chunks[1], buf);
}
}

View File

@@ -1,12 +1,13 @@
use crate::tui::about::About;
use crate::tui::app::AppComponent::{AppEditor, AppExplorer, AppLogger}; use crate::tui::app::AppComponent::{AppEditor, AppExplorer, AppLogger};
use crate::tui::component::{Action, Component, Focus, FocusState}; use crate::tui::component::{Action, Component, Focus, FocusState, Visibility, VisibleState};
use crate::tui::editor_tab::EditorTab; use crate::tui::editor_tab::EditorTab;
use crate::tui::explorer::Explorer; use crate::tui::explorer::Explorer;
use crate::tui::logger::Logger; use crate::tui::logger::Logger;
use crate::tui::menu_bar::MenuBar; use crate::tui::menu_bar::MenuBar;
use AppComponent::AppMenuBar; use AppComponent::AppMenuBar;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::error; use log::{error, info, trace};
use ratatui::DefaultTerminal; use ratatui::DefaultTerminal;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event; use ratatui::crossterm::event;
@@ -19,10 +20,7 @@ use ratatui::widgets::{Paragraph, Wrap};
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
// TODO: Need a way to dynamically run Widget::render on all widgets. #[derive(Debug, Clone, Copy, PartialEq)]
// TODO: + Need a way to map Rect to Component::id() to position each widget?
// TODO: Need a good way to dynamically run Component methods on all widgets.
#[derive(PartialEq)]
pub enum AppComponent { pub enum AppComponent {
AppEditor, AppEditor,
AppExplorer, AppExplorer,
@@ -36,6 +34,7 @@ pub struct App<'a> {
logger: Logger, logger: Logger,
menu_bar: MenuBar, menu_bar: MenuBar,
last_active: AppComponent, last_active: AppComponent,
about: bool,
} }
impl<'a> App<'a> { impl<'a> App<'a> {
@@ -44,38 +43,28 @@ impl<'a> App<'a> {
} }
pub fn new(root_path: PathBuf) -> Result<Self> { pub fn new(root_path: PathBuf) -> Result<Self> {
trace!(target:Self::id(), "Building {}", Self::id());
let app = Self { let app = Self {
editor_tabs: EditorTab::new(&root_path), editor_tabs: EditorTab::new(None),
explorer: Explorer::new(&root_path)?, explorer: Explorer::new(&root_path)?,
logger: Logger::new(), logger: Logger::new(),
menu_bar: MenuBar::new(), menu_bar: MenuBar::new(),
last_active: AppEditor, last_active: AppEditor,
about: false,
}; };
Ok(app) Ok(app)
} }
/// Logic that should be executed once on application startup. /// Logic that should be executed once on application startup.
pub fn start(&mut self) -> Result<()> { pub fn start(&mut self) -> Result<()> {
let root_path = self.explorer.root_path.clone(); trace!(target:Self::id(), "Starting App");
let editor = self
.editor_tabs
.current_editor_mut()
.context("Failed to get current editor in App::start")?;
editor
.set_contents(&root_path.join("src/tui/app.rs"))
.context(format!(
"Failed to initialize editor contents to path: {root_path:?}"
))?;
editor.component_state.set_focus(Focus::Active);
Ok(()) Ok(())
} }
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
self.start()?; self.start()?;
trace!(target:Self::id(), "Entering App run loop");
loop { loop {
self.refresh_editor_contents()
.context("Failed to refresh editor contents.")?;
terminal.draw(|f| { terminal.draw(|f| {
f.render_widget(&mut self, f.area()); f.render_widget(&mut self, f.area());
})?; })?;
@@ -99,7 +88,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()
} }
}, },
@@ -121,7 +112,23 @@ impl<'a> App<'a> {
.render(area, buf); .render(area, buf);
} }
fn clear_focus(&mut self) {
info!(target:Self::id(), "Clearing all widget focus");
self.explorer.component_state.set_focus(Focus::Inactive);
self.explorer.component_state.set_focus(Focus::Inactive);
self.logger.component_state.set_focus(Focus::Inactive);
self.menu_bar.component_state.set_focus(Focus::Inactive);
match self.editor_tabs.current_editor_mut() {
None => {
error!(target:Self::id(), "Failed to get current Editor while clearing focus")
}
Some(editor) => editor.component_state.set_focus(Focus::Inactive),
}
}
fn change_focus(&mut self, focus: AppComponent) { fn change_focus(&mut self, focus: AppComponent) {
info!(target:Self::id(), "Changing widget focus to {:?}", focus);
self.clear_focus();
match focus { match focus {
AppEditor => match self.editor_tabs.current_editor_mut() { AppEditor => match self.editor_tabs.current_editor_mut() {
None => { None => {
@@ -138,6 +145,7 @@ impl<'a> App<'a> {
/// Refresh the contents of the editor to match the selected TreeItem in the file Explorer. /// Refresh the contents of the editor to match the selected TreeItem in the file Explorer.
/// If the selected item is not a file, this does nothing. /// If the selected item is not a file, this does nothing.
#[allow(unused)]
fn refresh_editor_contents(&mut self) -> Result<()> { fn refresh_editor_contents(&mut self) -> Result<()> {
// TODO: This may be useful for a preview mode of the selected file prior to opening a tab. // TODO: This may be useful for a preview mode of the selected file prior to opening a tab.
// Use the currently selected TreeItem or get an absolute path to this source file. // Use the currently selected TreeItem or get an absolute path to this source file.
@@ -167,40 +175,92 @@ impl<'a> Widget for &mut App<'a> {
where where
Self: Sized, Self: Sized,
{ {
let vertical_constraints = match self.logger.component_state.vis {
Visibility::Visible => {
vec![
Constraint::Length(3), // top status bar
Constraint::Percentage(70), // horizontal layout
Constraint::Fill(1), // terminal
Constraint::Length(3), // bottom status bar
]
}
Visibility::Hidden => {
vec![
Constraint::Length(3), // top status bar
Constraint::Fill(1), // horizontal layout
Constraint::Length(3), // bottom status bar
]
}
};
let vertical = Layout::default() let vertical = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints(vertical_constraints)
Constraint::Length(3), // top status bar
Constraint::Percentage(70), // horizontal layout
Constraint::Percentage(30), // terminal
Constraint::Length(3), // bottom status bar
])
.split(area); .split(area);
let horizontal_constraints = match self.explorer.component_state.vis {
Visibility::Visible => {
vec![
Constraint::Max(30), // File explorer with a max width of 30 characters.
Constraint::Fill(1), // Editor fills the remaining space.
]
}
Visibility::Hidden => {
vec![
Constraint::Fill(1), // Editor fills the remaining space.
]
}
};
// The index used for vertical here does not care if the Logger is Visible or not.
let horizontal = Layout::default() let horizontal = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([ .constraints(horizontal_constraints)
Constraint::Max(30), // File explorer with a max width of 30 characters.
Constraint::Fill(1), // Editor fills the remaining space.
])
.split(vertical[1]); .split(vertical[1]);
match self.explorer.component_state.vis {
Visibility::Visible => {
let editor_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // Editor tabs.
Constraint::Fill(1), // Editor contents.
])
.split(horizontal[1]);
self.editor_tabs
.render(editor_layout[0], editor_layout[1], buf);
self.explorer.render(horizontal[0], buf);
}
Visibility::Hidden => {
let editor_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // Editor tabs.
Constraint::Fill(1), // Editor contents.
])
.split(horizontal[0]);
self.editor_tabs
.render(editor_layout[0], editor_layout[1], buf);
}
}
let editor_layout = Layout::default() match self.logger.component_state.vis {
.direction(Direction::Vertical) // Index 1 of vertical is rendered with the horizontal layout above.
.constraints([ Visibility::Visible => {
Constraint::Length(1), // Editor tabs. self.logger.render(vertical[2], buf);
Constraint::Fill(1), // Editor contents. self.draw_bottom_status(vertical[3], buf);
]) // The title bar is rendered last to overlay any popups created for drop-down menus.
.split(horizontal[1]); self.menu_bar.render(vertical[0], buf);
}
Visibility::Hidden => {
self.draw_bottom_status(vertical[2], buf);
// The title bar is rendered last to overlay any popups created for drop-down menus.
self.menu_bar.render(vertical[0], buf);
}
}
self.draw_bottom_status(vertical[3], buf); if self.about {
self.editor_tabs let about_area = area.centered(Constraint::Percentage(50), Constraint::Percentage(45));
.render(editor_layout[0], editor_layout[1], buf); About::new().render(about_area, buf);
self.explorer.render(horizontal[0], buf); }
self.logger.render(vertical[2], buf);
// The title bar is rendered last to overlay any popups created for drop-down menus.
self.menu_bar.render(vertical[0], buf);
} }
} }
@@ -225,27 +285,32 @@ 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)?;
} }
} }
// Handle actions returned from widgets that may need context on other widgets or app state.
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(_) => { error!(target:Self::id(), "Failed to get current editor while handling App Action::Save");
error!(target:Self::id(), "Failed to save editor contents");
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() {
@@ -256,12 +321,51 @@ 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)
}
}
Action::ShowHideLogger => {
self.logger.component_state.togget_visible();
Ok(Action::Handled)
}
Action::ShowHideExplorer => {
self.explorer.component_state.togget_visible();
Ok(Action::Handled)
}
Action::ShowHideAbout => {
self.about = !self.about;
Ok(Action::Handled)
}
_ => Ok(Action::Noop), _ => Ok(Action::Noop),
} }
} }
/// Handles key events for the App Component only. /// Handles key events for the App Component only.
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> { fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> {
match key.code {
// If the ESC key is pressed with the About page open, hide it.
KeyCode::Esc | KeyCode::Char('q') => {
if self.about {
self.about = false;
return Ok(Action::Handled);
}
}
_ => {}
}
match key { match key {
KeyEvent { KeyEvent {
code: KeyCode::Char('q'), code: KeyCode::Char('q'),

View File

@@ -1,7 +1,11 @@
#![allow(dead_code, unused_variables)] #![allow(dead_code, unused_variables)]
use crate::tui::component::Focus::Inactive;
use Focus::Active;
use anyhow::Result; use anyhow::Result;
use log::trace;
use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent}; use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent};
use ratatui::style::Color;
pub enum Action { pub enum Action {
/// Exit the application. /// Exit the application.
@@ -20,6 +24,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,
ShowHideAbout,
CloseTab,
} }
pub trait Component { pub trait Component {
@@ -51,13 +60,20 @@ pub trait Component {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ComponentState { pub struct ComponentState {
pub(crate) focus: Focus, pub(crate) focus: Focus,
pub(crate) vis: Visibility,
pub(crate) help_text: String, pub(crate) help_text: String,
} }
impl ComponentState { impl ComponentState {
pub fn id() -> &'static str {
"ComponentState"
}
fn new() -> Self { fn new() -> Self {
trace!(target:Self::id(), "Building {}", Self::id());
Self { Self {
focus: Focus::Active, focus: Active,
vis: Visibility::Visible,
help_text: String::new(), help_text: String::new(),
} }
} }
@@ -75,16 +91,27 @@ pub enum Focus {
Inactive, Inactive,
} }
impl Focus {
pub(crate) fn get_active_color(&self) -> Color {
match self {
Active => Color::LightYellow,
Inactive => Color::White,
}
}
}
pub trait FocusState { pub trait FocusState {
fn with_focus(self, focus: Focus) -> Self; fn with_focus(self, focus: Focus) -> Self;
fn set_focus(&mut self, focus: Focus); fn set_focus(&mut self, focus: Focus);
fn toggle_focus(&mut self); fn toggle_focus(&mut self);
fn get_active_color(&self) -> Color;
} }
impl FocusState for ComponentState { impl FocusState for ComponentState {
fn with_focus(self, focus: Focus) -> Self { fn with_focus(self, focus: Focus) -> Self {
Self { Self {
focus, focus,
vis: Visibility::Visible,
help_text: self.help_text, help_text: self.help_text,
} }
} }
@@ -95,8 +122,46 @@ impl FocusState for ComponentState {
fn toggle_focus(&mut self) { fn toggle_focus(&mut self) {
match self.focus { match self.focus {
Focus::Active => self.set_focus(Focus::Inactive), Active => self.set_focus(Inactive),
Focus::Inactive => self.set_focus(Focus::Active), Inactive => self.set_focus(Active),
}
}
fn get_active_color(&self) -> Color {
self.focus.get_active_color()
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub enum Visibility {
#[default]
Visible,
Hidden,
}
pub trait VisibleState {
fn with_visible(self, vis: Visibility) -> Self;
fn set_visible(&mut self, vis: Visibility);
fn togget_visible(&mut self);
}
impl VisibleState for ComponentState {
fn with_visible(self, vis: Visibility) -> Self {
Self {
focus: self.focus,
vis,
help_text: self.help_text,
}
}
fn set_visible(&mut self, vis: Visibility) {
self.vis = vis;
}
fn togget_visible(&mut self) {
match self.vis {
Visibility::Visible => self.set_visible(Visibility::Hidden),
Visibility::Hidden => self.set_visible(Visibility::Visible),
} }
} }
} }

View File

@@ -1,8 +1,9 @@
use crate::tui::component::{Action, Component, ComponentState, Focus}; use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use edtui::{ use edtui::{
EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter, EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter,
}; };
use log::{error, trace};
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::{Alignment, Rect}; use ratatui::layout::{Alignment, Rect};
@@ -23,12 +24,12 @@ impl Editor {
"Editor" "Editor"
} }
// TODO: You shouldnt be able to construct the editor without a path? pub fn new(path: &std::path::PathBuf) -> Self {
pub fn new() -> Self { trace!(target:Self::id(), "Building {}", Self::id());
Editor { Editor {
state: EditorState::default(), state: EditorState::default(),
event_handler: EditorEventHandler::default(), event_handler: EditorEventHandler::default(),
file_path: None, file_path: Some(path.to_owned()),
syntax_set: SyntaxSet::load_defaults_nonewlines(), syntax_set: SyntaxSet::load_defaults_nonewlines(),
component_state: ComponentState::default().with_help_text(concat!( component_state: ComponentState::default().with_help_text(concat!(
"CTRL+S: Save file | ALT+(←/h): Previous tab | ALT+(l/→): Next tab |", "CTRL+S: Save file | ALT+(←/h): Previous tab | ALT+(l/→): Next tab |",
@@ -37,7 +38,19 @@ 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);
if let Ok(contents) = std::fs::read_to_string(path) { if let Ok(contents) = std::fs::read_to_string(path) {
let lines: Vec<_> = contents let lines: Vec<_> = contents
.lines() .lines()
@@ -53,8 +66,10 @@ impl Editor {
pub fn save(&self) -> Result<()> { pub fn save(&self) -> Result<()> {
if let Some(path) = &self.file_path { if let Some(path) = &self.file_path {
trace!(target:Self::id(), "Saving Editor contents {:?}", path);
return std::fs::write(path, self.state.lines.to_string()).map_err(|e| e.into()); return std::fs::write(path, self.state.lines.to_string()).map_err(|e| e.into());
}; };
error!(target:Self::id(), "Failed saving Editor contents; file_path was None");
bail!("File not saved. No file path set.") bail!("File not saved. No file path set.")
} }
} }
@@ -82,7 +97,8 @@ impl Widget for &mut Editor {
.title_style(Style::default().fg(Color::Yellow)) .title_style(Style::default().fg(Color::Yellow))
.title_alignment(Alignment::Right) .title_alignment(Alignment::Right)
.borders(Borders::ALL) .borders(Borders::ALL)
.padding(Padding::new(0, 0, 0, 1)), .padding(Padding::new(0, 0, 0, 1))
.style(Style::default().fg(self.component_state.get_active_color())),
), ),
) )
.syntax_highlighter(SyntaxHighlighter::new("dracula", lang).ok()) .syntax_highlighter(SyntaxHighlighter::new("dracula", lang).ok())

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; 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;
@@ -23,52 +23,149 @@ impl EditorTab {
"EditorTab" "EditorTab"
} }
pub fn new(path: &std::path::PathBuf) -> Self { pub fn new(path: Option<&std::path::PathBuf>) -> Self {
trace!(target:Self::id(), "Building EditorTab with path '{path:?}'"); trace!(target:Self::id(), "Building EditorTab with path {path:?}");
let tab_order = vec![path.to_string_lossy().to_string()]; match path {
Self { None => Self {
editors: HashMap::from([(tab_order.first().unwrap().to_owned(), Editor::new())]), editors: HashMap::new(),
tab_order, tab_order: Vec::new(),
current_editor: 0, current_editor: 0,
},
Some(path) => {
let tab_order = vec![path.to_string_lossy().to_string()];
Self {
editors: HashMap::from([(
tab_order.first().unwrap().to_owned(),
Editor::new(path),
)]),
tab_order,
current_editor: 0,
}
}
} }
} }
pub fn next_editor(&mut self) { pub fn next_editor(&mut self) {
self.current_editor = (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);
self.set_tab_focus(Focus::Active, next);
self.current_editor = next;
} }
pub fn prev_editor(&mut self) { pub fn prev_editor(&mut self) {
self.current_editor = self let prev = self
.current_editor .current_editor
.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);
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> { 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<()> {
trace!(target:Self::id(), "Opening new EditorTab with path {:?}", path);
if self if self
.editors .editors
.contains_key(&path.to_string_lossy().to_string()) .contains_key(&path.to_string_lossy().to_string())
{ {
warn!(target:Self::id(), "EditorTab already opened with this file");
return Ok(()); return Ok(());
} }
let path_str = path.to_string_lossy().to_string(); let path_str = path.to_string_lossy().to_string();
self.tab_order.push(path_str.clone()); self.tab_order.push(path_str.clone());
let mut editor = Editor::new(); let mut editor = Editor::new(path);
editor.set_contents(path).context("Failed to open tab")?; editor.set_contents(path).context("Failed to open tab")?;
self.editors.insert(path_str, editor); self.editors.insert(path_str, editor);
self.current_editor = self.tab_order.len() - 1; self.current_editor = self.tab_order.len() - 1;
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.
@@ -78,6 +175,7 @@ impl EditorTab {
.map(|f| f.to_string_lossy().to_string()) .map(|f| f.to_string_lossy().to_string())
.unwrap_or("Unknown".to_string()) .unwrap_or("Unknown".to_string())
}); });
// Don't set border color based on ComponentState::focus, the Editor renders the border.
Tabs::new(tab_titles) Tabs::new(tab_titles)
.select(self.current_editor) .select(self.current_editor)
.divider("|") .divider("|")
@@ -112,9 +210,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,5 +1,6 @@
use crate::tui::component::{Action, Component, ComponentState, Focus}; use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::trace;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
use ratatui::layout::{Alignment, Position, Rect}; use ratatui::layout::{Alignment, Position, Rect};
@@ -24,13 +25,14 @@ impl<'a> Explorer<'a> {
} }
pub fn new(path: &PathBuf) -> Result<Self> { pub fn new(path: &PathBuf) -> Result<Self> {
trace!(target:Self::id(), "Building {}", Self::id());
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(), tree_state: TreeState::default(),
component_state: ComponentState::default().with_help_text(concat!( component_state: ComponentState::default().with_help_text(concat!(
"(↑/k)/(↓/j): Select item | ←/h: Close folder | →/l: Open folder |", "(↑/k)/(↓/j): Select item | ←/h: Close folder | →/l: Open folder |",
" Enter: Open editor tab" " Space: Open / close folder | Enter: Open file in new editor tab"
)), )),
}; };
Ok(explorer) Ok(explorer)
@@ -99,20 +101,20 @@ impl<'a> Widget for &mut Explorer<'a> {
if let Ok(tree) = Tree::new(&self.tree_items.children()) { if let Ok(tree) = Tree::new(&self.tree_items.children()) {
let file_name = self.root_path.file_name().unwrap_or("Unknown".as_ref()); let file_name = self.root_path.file_name().unwrap_or("Unknown".as_ref());
StatefulWidget::render( StatefulWidget::render(
tree.style(Style::default()) tree.block(
.block( Block::default()
Block::default() .borders(Borders::ALL)
.borders(Borders::ALL) .title(file_name.to_string_lossy())
.title(file_name.to_string_lossy()) .border_style(Style::default().fg(self.component_state.get_active_color()))
.title_style(Style::default().fg(Color::Green)) .title_style(Style::default().fg(Color::Green))
.title_alignment(Alignment::Center), .title_alignment(Alignment::Center),
) )
.highlight_style( .highlight_style(
Style::new() Style::new()
.fg(Color::Black) .fg(Color::Black)
.bg(Color::Rgb(57, 59, 64)) .bg(Color::Rgb(57, 59, 64))
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
), ),
area, area,
buf, buf,
&mut self.tree_state, &mut self.tree_state,
@@ -147,7 +149,7 @@ impl<'a> Component for Explorer<'a> {
return Ok(Action::OpenTab); return Ok(Action::OpenTab);
} }
} }
return Ok(Action::Noop); // Otherwise fall through and handle Enter in the next match case.
} }
let changed = match key.code { let changed = match key.code {
@@ -158,6 +160,9 @@ impl<'a> Component for Explorer<'a> {
let key = self.tree_state.selected().to_owned(); let key = self.tree_state.selected().to_owned();
self.tree_state.close(key.as_ref()) self.tree_state.close(key.as_ref())
} }
KeyCode::Char(' ') | KeyCode::Enter => self
.tree_state
.toggle(self.tree_state.selected().to_owned()),
KeyCode::Right | KeyCode::Char('l') => self.tree_state.key_right(), KeyCode::Right | KeyCode::Char('l') => self.tree_state.key_right(),
_ => false, _ => false,
}; };

View File

@@ -1,4 +1,5 @@
use crate::tui::component::{Action, Component, ComponentState, Focus}; use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use log::{LevelFilter, trace};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent};
use ratatui::layout::Rect; use ratatui::layout::Rect;
@@ -19,10 +20,13 @@ impl Logger {
} }
pub fn new() -> Self { pub fn new() -> Self {
trace!(target:Self::id(), "Building {}", Self::id());
let state = TuiWidgetState::new(); let state = TuiWidgetState::new();
state.transition(TuiWidgetEvent::HideKey); state.transition(TuiWidgetEvent::HideKey);
Self { Self {
state, state: state
.set_level_for_target("arboard::platform::linux::x11", LevelFilter::Off)
.set_level_for_target("mio::poll", LevelFilter::Off),
component_state: ComponentState::default().with_help_text(concat!( component_state: ComponentState::default().with_help_text(concat!(
"Space: Hide/show logging target selector panel | (↑/k)/(↓/j): Select target |", "Space: Hide/show logging target selector panel | (↑/k)/(↓/j): Select target |",
" (←/h)/(→/l): Display level | f: Focus target | +/-: Filter level |", " (←/h)/(→/l): Display level | f: Focus target | +/-: Filter level |",
@@ -38,6 +42,7 @@ impl Widget for &Logger {
Self: Sized, Self: Sized,
{ {
TuiLoggerSmartWidget::default() TuiLoggerSmartWidget::default()
.border_style(Style::default().fg(self.component_state.get_active_color()))
.style_error(Style::default().fg(Color::Red)) .style_error(Style::default().fg(Color::Red))
.style_debug(Style::default().fg(Color::Green)) .style_debug(Style::default().fg(Color::Green))
.style_warn(Style::default().fg(Color::Yellow)) .style_warn(Style::default().fg(Color::Yellow))

View File

@@ -1,7 +1,8 @@
use crate::tui::component::{Action, Component, ComponentState}; 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 ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{KeyCode, KeyEvent}; use ratatui::crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::Rect; use ratatui::layout::Rect;
@@ -22,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,
@@ -38,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",
} }
} }
} }
@@ -65,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],
} }
@@ -80,8 +83,13 @@ pub struct MenuBar {
} }
impl MenuBar { impl MenuBar {
pub fn id() -> &'static str {
"MenuBar"
}
const DEFAULT_HELP: &str = "(←/h)/(→/l): Select option | Enter: Choose selection"; const DEFAULT_HELP: &str = "(←/h)/(→/l): Select option | Enter: Choose selection";
pub fn new() -> Self { pub fn new() -> Self {
trace!(target:Self::id(), "Building {}", Self::id());
Self { Self {
selected: MenuBarItem::File, selected: MenuBarItem::File,
opened: None, opened: None,
@@ -102,7 +110,11 @@ impl MenuBar {
}; };
Tabs::new(titles) Tabs::new(titles)
.style(tabs_style) .style(tabs_style)
.block(Block::default().borders(Borders::ALL)) .block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(self.component_state.get_active_color())),
)
.highlight_style(highlight_style) .highlight_style(highlight_style)
.select(self.selected as usize) .select(self.selected as usize)
.render(area, buf); .render(area, buf);
@@ -135,13 +147,15 @@ 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 {
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,
} };
// 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
} }
} }
@@ -178,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::ShowHideAbout),
CloseTab => Ok(Action::CloseTab),
}; };
} }
Ok(Action::Noop) Ok(Action::Noop)