From edcbea746cf787b18d9d7cdb7141ea0ac434310e Mon Sep 17 00:00:00 2001 From: Shaun Reed Date: Tue, 20 Jan 2026 20:14:25 -0500 Subject: [PATCH] [tui] Add debug console. The input will not be handled correctly until #8 is complete, but the input logic is there and was tested. Fixes #5. --- Cargo.lock | 129 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/tui.rs | 19 +++++++ src/tui/app.rs | 62 ++++++++++++--------- src/tui/editor.rs | 4 +- src/tui/explorer.rs | 4 +- 6 files changed, 190 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9fc058..795325c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -216,6 +225,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + [[package]] name = "clang-format" version = "0.3.0" @@ -280,6 +300,7 @@ dependencies = [ "log", "ratatui", "syntect", + "tui-logger", "tui-tree-widget", ] @@ -351,6 +372,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -729,6 +756,16 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -927,6 +964,30 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2151,6 +2212,21 @@ dependencies = [ "time-core", ] +[[package]] +name = "tui-logger" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9384df20a5244a6ab204bc4b6959b41f37f0ee7b5e0f2feb7a8a78f58e684d06" +dependencies = [ + "chrono", + "env_filter", + "lazy_static", + "log", + "parking_lot", + "ratatui", + "unicode-segmentation", +] + [[package]] name = "tui-tree-widget" version = "0.24.0" @@ -2421,12 +2497,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.60.2" diff --git a/Cargo.toml b/Cargo.toml index a002f91..de9de16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ clap = { version = "4.5.54", features = ["derive"] } ratatui = "0.30.0" anyhow = "1.0.100" tui-tree-widget = "0.24.0" +tui-logger = "0.18.1" edtui = "0.11.1" [build-dependencies] diff --git a/src/tui.rs b/src/tui.rs index 97721b7..83fc16e 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -2,8 +2,10 @@ mod app; mod component; mod editor; mod explorer; +mod logger; use anyhow::{Context, Result}; +use log::{LevelFilter, debug}; use ratatui::Terminal; use ratatui::backend::CrosstermBackend; use ratatui::crossterm::event::{ @@ -12,7 +14,11 @@ use ratatui::crossterm::event::{ use ratatui::crossterm::terminal::{ EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, }; +use std::env; use std::io::{Stdout, stdout}; +use tui_logger::{ + TuiLoggerFile, TuiLoggerLevelOutput, init_logger, set_default_level, set_log_file, +}; pub struct Tui { terminal: Terminal>, @@ -21,6 +27,19 @@ pub struct Tui { impl Tui { pub fn new(root_path: std::path::PathBuf) -> Result { + init_logger(LevelFilter::Trace)?; + set_default_level(LevelFilter::Trace); + debug!(target:"Tui", "Logging initialized"); + + let mut dir = env::temp_dir(); + dir.push("clide.log"); + let file_options = TuiLoggerFile::new(dir.to_str().unwrap()) + .output_level(Some(TuiLoggerLevelOutput::Abbreviated)) + .output_file(false) + .output_separator(':'); + set_log_file(file_options); + debug!(target:"Tui", "Logging to file: {}", dir.to_str().unwrap()); + Ok(Self { terminal: Terminal::new(CrosstermBackend::new(stdout()))?, root_path, diff --git a/src/tui/app.rs b/src/tui/app.rs index 574497c..f10891a 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -1,7 +1,9 @@ use crate::tui::component::{Action, Component}; use crate::tui::editor::Editor; use crate::tui::explorer::Explorer; +use crate::tui::logger::Logger; use anyhow::{Context, Result, anyhow, bail}; +use log::{debug, error, info, trace, warn}; use ratatui::buffer::Buffer; use ratatui::crossterm::event; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; @@ -12,9 +14,13 @@ use ratatui::{DefaultTerminal, symbols}; use std::path::PathBuf; use std::time::Duration; +// 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 dynamically run Component methods on all widgets. pub enum AppComponents<'a> { AppEditor(Editor), AppExplorer(Explorer<'a>), + AppLogger(Logger), AppComponent(Box), } @@ -28,6 +34,7 @@ impl<'a> App<'a> { components: vec![ AppComponents::AppExplorer(Explorer::new(&root_path)?), AppComponents::AppEditor(Editor::new()), + AppComponents::AppLogger(Logger::new()), ], }; app.get_editor_mut() @@ -133,21 +140,6 @@ impl<'a> App<'a> { .render(area, buf); } - fn draw_terminal(&self, area: Rect, buf: &mut Buffer) { - // TODO: Title should be detected shell name - // TODO: Contents should be shell output - Paragraph::new("shaun@pc:~/Code/clide$ ") - .style(Style::default()) - .block( - Block::default() - .title("Bash") - .title_style(Style::default().fg(Color::DarkGray)) - .borders(Borders::ALL), - ) - .wrap(Wrap { trim: false }) - .render(area, buf); - } - /// 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. fn refresh_editor_contents(&mut self) -> Result<()> { @@ -201,22 +193,27 @@ impl<'a> Widget for &mut App<'a> { .split(horizontal[1]); self.draw_status(vertical[0], buf); - self.draw_terminal(vertical[2], buf); - if let Ok(explorer) = self.get_explorer_mut() { - // TODO: What to do about errors during rendering? - // Once there is a debug console, maybe log it and discard? Panic isn't great. - explorer - .render(horizontal[0], buf) - .expect("Failed to render Explorer"); - } self.draw_tabs(editor_layout[0], buf); - self.get_editor_mut().unwrap().render(editor_layout[1], buf); + for component in &mut self.components { + match component { + AppComponents::AppEditor(editor) => editor.render(editor_layout[1], buf), + AppComponents::AppExplorer(explorer) => { + // TODO: What to do about errors during rendering? + // Once there is a debug console, maybe log it and discard? Panic isn't great. + explorer + .render(horizontal[0], buf) + .expect("Failed to render Explorer"); + } + AppComponents::AppLogger(logger) => logger.render(vertical[2], buf), + AppComponents::AppComponent(_) => {} + } + } } } impl<'a> Component for App<'a> { fn id(&self) -> &str { - "app" + "App" } /// TODO: Get active widget with some Component trait function helper? @@ -245,6 +242,7 @@ impl<'a> Component for App<'a> { AppComponents::AppEditor(editor) => editor.handle_event(event.clone())?, AppComponents::AppExplorer(explorer) => explorer.handle_event(event.clone())?, AppComponents::AppComponent(comp) => comp.handle_event(event.clone())?, + AppComponents::AppLogger(logger) => logger.handle_event(event.clone())?, }; // Actions returned here abort the input handling iteration. match action { @@ -258,6 +256,20 @@ impl<'a> Component for App<'a> { /// Handles key events for the App Component only. fn handle_key_events(&mut self, key: KeyEvent) -> Result { match key { + KeyEvent { + code: KeyCode::Char('l'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: _state, + } => { + // Some example logs for testing. + error!(target:self.id(), "an error"); + warn!(target:self.id(), "a warning"); + info!(target:self.id(), "a two line info\nsecond line"); + debug!(target:self.id(), "a debug"); + trace!(target:self.id(), "a trace"); + Ok(Action::Noop) + } KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, diff --git a/src/tui/editor.rs b/src/tui/editor.rs index d6dc3a7..7dbf5fb 100644 --- a/src/tui/editor.rs +++ b/src/tui/editor.rs @@ -1,6 +1,6 @@ use crate::tui::component::{Action, Component}; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use edtui::{ EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter, }; @@ -87,7 +87,7 @@ impl Widget for &mut Editor { impl Component for Editor { fn id(&self) -> &str { - "editor" + "Editor" } fn handle_event(&mut self, event: Event) -> Result { diff --git a/src/tui/explorer.rs b/src/tui/explorer.rs index c63e810..751d725 100644 --- a/src/tui/explorer.rs +++ b/src/tui/explorer.rs @@ -1,5 +1,5 @@ use crate::tui::component::{Action, Component}; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use ratatui::buffer::Buffer; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind}; use ratatui::layout::{Alignment, Position, Rect}; @@ -113,7 +113,7 @@ impl<'a> Explorer<'a> { impl<'a> Component for Explorer<'a> { fn id(&self) -> &str { - "explorer" + "Explorer" } fn handle_event(&mut self, event: Event) -> Result { if let Some(key_event) = event.as_key_event() {