TUI #1
15
src/tui.rs
15
src/tui.rs
@ -7,7 +7,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 +28,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 +48,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 +57,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 +74,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(),
|
||||||
|
|||||||
@ -6,7 +6,7 @@ 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, warn};
|
||||||
use ratatui::DefaultTerminal;
|
use ratatui::DefaultTerminal;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::crossterm::event;
|
use ratatui::crossterm::event;
|
||||||
@ -22,7 +22,7 @@ use std::time::Duration;
|
|||||||
// TODO: Need a way to dynamically run Widget::render on all widgets.
|
// TODO: Need a way to dynamically run Widget::render on all widgets.
|
||||||
// TODO: + Need a way to map Rect to Component::id() to position each widget?
|
// 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.
|
// TODO: Need a good way to dynamically run Component methods on all widgets.
|
||||||
#[derive(PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum AppComponent {
|
pub enum AppComponent {
|
||||||
AppEditor,
|
AppEditor,
|
||||||
AppExplorer,
|
AppExplorer,
|
||||||
@ -44,6 +44,7 @@ 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(&root_path),
|
||||||
explorer: Explorer::new(&root_path)?,
|
explorer: Explorer::new(&root_path)?,
|
||||||
@ -56,6 +57,7 @@ impl<'a> App<'a> {
|
|||||||
|
|
||||||
/// 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<()> {
|
||||||
|
trace!(target:Self::id(), "Starting App");
|
||||||
let root_path = self.explorer.root_path.clone();
|
let root_path = self.explorer.root_path.clone();
|
||||||
let editor = self
|
let editor = self
|
||||||
.editor_tabs
|
.editor_tabs
|
||||||
@ -72,10 +74,8 @@ impl<'a> App<'a> {
|
|||||||
|
|
||||||
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());
|
||||||
})?;
|
})?;
|
||||||
@ -122,6 +122,7 @@ impl<'a> App<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn change_focus(&mut self, focus: AppComponent) {
|
fn change_focus(&mut self, focus: AppComponent) {
|
||||||
|
info!(target:Self::id(), "Changing widget focus to {:?}", 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 +139,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.
|
||||||
@ -242,8 +244,8 @@ impl<'a> Component for App<'a> {
|
|||||||
Action::Quit | Action::Handled => Ok(action),
|
Action::Quit | Action::Handled => Ok(action),
|
||||||
Action::Save => match editor.save() {
|
Action::Save => match editor.save() {
|
||||||
Ok(_) => Ok(Action::Handled),
|
Ok(_) => Ok(Action::Handled),
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
error!(target:Self::id(), "Failed to save editor contents");
|
error!(target:Self::id(), "Failed to save editor contents: {e}");
|
||||||
Ok(Action::Noop)
|
Ok(Action::Noop)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#![allow(dead_code, unused_variables)]
|
#![allow(dead_code, unused_variables)]
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use log::trace;
|
||||||
use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent};
|
use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent};
|
||||||
|
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
@ -55,7 +56,12 @@ pub struct ComponentState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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: Focus::Active,
|
||||||
help_text: String::new(),
|
help_text: String::new(),
|
||||||
|
|||||||
@ -3,6 +3,7 @@ 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};
|
||||||
@ -25,6 +26,7 @@ impl Editor {
|
|||||||
|
|
||||||
// TODO: You shouldnt be able to construct the editor without a path?
|
// TODO: You shouldnt be able to construct the editor without a path?
|
||||||
pub fn new() -> 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(),
|
||||||
@ -38,6 +40,7 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 +56,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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use crate::tui::component::{Action, Component};
|
use crate::tui::component::{Action, Component};
|
||||||
use crate::tui::editor::Editor;
|
use crate::tui::editor::Editor;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use log::trace;
|
use log::{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;
|
||||||
@ -24,7 +24,7 @@ impl EditorTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(path: &std::path::PathBuf) -> Self {
|
pub fn new(path: &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()];
|
let tab_order = vec![path.to_string_lossy().to_string()];
|
||||||
Self {
|
Self {
|
||||||
editors: HashMap::from([(tab_order.first().unwrap().to_owned(), Editor::new())]),
|
editors: HashMap::from([(tab_order.first().unwrap().to_owned(), Editor::new())]),
|
||||||
@ -34,14 +34,18 @@ impl EditorTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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.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.current_editor = prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_editor(&self) -> Option<&Editor> {
|
pub fn current_editor(&self) -> Option<&Editor> {
|
||||||
@ -53,10 +57,12 @@ impl EditorTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
use crate::tui::component::{Action, Component, ComponentState, Focus};
|
use crate::tui::component::{Action, Component, ComponentState, Focus};
|
||||||
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,6 +25,7 @@ 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())?,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use crate::tui::component::{Action, Component, ComponentState, Focus};
|
use crate::tui::component::{Action, Component, ComponentState, Focus};
|
||||||
|
use log::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,6 +20,7 @@ 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 {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ use crate::tui::component::{Action, Component, ComponentState};
|
|||||||
use crate::tui::menu_bar::MenuBarItemOption::{
|
use crate::tui::menu_bar::MenuBarItemOption::{
|
||||||
About, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger,
|
About, 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;
|
||||||
@ -80,8 +81,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,
|
||||||
@ -136,12 +142,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.
|
// TODO: X offset for item option? It's fine as-is, but it might look nicer.
|
||||||
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);
|
||||||
|
rect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user