[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.
This commit is contained in:
2026-01-20 20:14:25 -05:00
parent 1e635ee059
commit 7149ad0118
7 changed files with 266 additions and 29 deletions

View File

@@ -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<CrosstermBackend<Stdout>>,
@@ -21,6 +27,19 @@ pub struct Tui {
impl Tui {
pub fn new(root_path: std::path::PathBuf) -> Result<Self> {
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,

View File

@@ -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<dyn Component>),
}
@@ -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<Action> {
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,

View File

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

View File

@@ -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<Action> {
if let Some(key_event) = event.as_key_event() {

76
src/tui/logger.rs Normal file
View File

@@ -0,0 +1,76 @@
use crate::tui::component::{Action, Component};
use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent};
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::Widget;
use tui_logger::{TuiLoggerLevelOutput, TuiLoggerSmartWidget, TuiWidgetEvent, TuiWidgetState};
/// Any log written as info!(target:self.id(), "message") will work with this logger.
/// The logger is bound to info!, debug!, error!, trace! macros within Tui::new().
pub struct Logger {
state: TuiWidgetState,
}
impl Logger {
pub fn new() -> Self {
Self {
state: TuiWidgetState::new(),
}
}
}
impl Widget for &Logger {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
// TODO: Use output_file?
TuiLoggerSmartWidget::default()
.style_error(Style::default().fg(Color::Red))
.style_debug(Style::default().fg(Color::Green))
.style_warn(Style::default().fg(Color::Yellow))
.style_trace(Style::default().fg(Color::Magenta))
.style_info(Style::default().fg(Color::Cyan))
.output_separator(':')
.output_timestamp(Some("%H:%M:%S".to_string()))
.output_level(Some(TuiLoggerLevelOutput::Abbreviated))
.output_target(true)
.output_file(true)
.output_line(true)
.state(&self.state)
.render(area, buf);
}
}
impl Component for Logger {
fn id(&self) -> &str {
"Logger"
}
fn handle_event(&mut self, event: Event) -> anyhow::Result<Action> {
if let Some(key_event) = event.as_key_event() {
return self.handle_key_events(key_event);
}
Ok(Action::Noop)
}
fn handle_key_events(&mut self, key: KeyEvent) -> anyhow::Result<Action> {
match key.code {
KeyCode::Char(' ') => self.state.transition(TuiWidgetEvent::SpaceKey),
KeyCode::Esc => self.state.transition(TuiWidgetEvent::EscapeKey),
KeyCode::PageUp => self.state.transition(TuiWidgetEvent::PrevPageKey),
KeyCode::PageDown => self.state.transition(TuiWidgetEvent::NextPageKey),
KeyCode::Up => self.state.transition(TuiWidgetEvent::UpKey),
KeyCode::Down => self.state.transition(TuiWidgetEvent::DownKey),
KeyCode::Left => self.state.transition(TuiWidgetEvent::LeftKey),
KeyCode::Right => self.state.transition(TuiWidgetEvent::RightKey),
KeyCode::Char('+') => self.state.transition(TuiWidgetEvent::PlusKey),
KeyCode::Char('-') => self.state.transition(TuiWidgetEvent::MinusKey),
KeyCode::Char('h') => self.state.transition(TuiWidgetEvent::HideKey),
KeyCode::Char('f') => self.state.transition(TuiWidgetEvent::FocusKey),
_ => (),
}
Ok(Action::Pass)
}
}