[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:
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
76
src/tui/logger.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user