TUI #1

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

View File

@ -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(),

View File

@ -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)
} }
}, },

View File

@ -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(),

View File

@ -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.")
} }
} }

View File

@ -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(());
} }

View File

@ -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())?,

View File

@ -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 {

View File

@ -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
} }
} }