Added ratatui integration + logger redirect

This commit is contained in:
Silas Bartha 2024-05-02 02:53:51 -04:00
parent 646db83286
commit 6834194b9e
Signed by: soaos
GPG Key ID: 9BD3DCC0D56A09B2
7 changed files with 155 additions and 53 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "grex_terminal_display" name = "grex_terminal_display"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -17,3 +17,8 @@ tag = "v0.1.0"
[dependencies.grex_dither_post_process] [dependencies.grex_dither_post_process]
git = "https://github.com/exvacuum/grex_dither_post_process" git = "https://github.com/exvacuum/grex_dither_post_process"
tag = "v0.1.2" tag = "v0.1.2"
[dependencies.ratatui]
version = "0.26.2"
features = ["unstable-widget-ref"]

View File

@ -10,11 +10,9 @@ Features Include:
- Post-process dithers colors to pure black and white, which are then printed as braille characters to the terminal - Post-process dithers colors to pure black and white, which are then printed as braille characters to the terminal
- Responsiveness to terminal window resizing - Responsiveness to terminal window resizing
- `TerminalInput` resource which keeps track of pressed & released keys - `TerminalInput` resource which keeps track of pressed & released keys
- Keyboard input enhancements using kitty protocol - `TerminalUI` resource for rendering ratatui TUI widgets
- `TerminalWidget` trait for creating custom TUI widget components
Future Goals: - Logging redirected to `output.log`
- Find a way to integrate into a TUI library like ratatui for more interaction options.
- Move kitty enhancements to a feature maybe
## Screenshots ## Screenshots
![](./doc/screenshot.png) ![](./doc/screenshot.png)
@ -42,7 +40,7 @@ use grex_terminal_display;
fn main() { fn main() {
App::new() App::new()
.add_plugins(( .add_plugins((
DefaultPlugins.build().disable::<WinitPlugin>(), DefaultPlugins.build().disable::<WinitPlugin>().disable::<LogPlugin>,
ScheduleRunnerPlugin::run_loop(Duration::from_secs_f32(1.0 / 60.0)), ScheduleRunnerPlugin::run_loop(Duration::from_secs_f32(1.0 / 60.0)),
grex_terminal_display::TerminalDisplayPlugin, grex_terminal_display::TerminalDisplayPlugin,
)) ))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -1,3 +1,4 @@
use bevy::{ use bevy::{
prelude::*, prelude::*,
render::render_resource::{ render::render_resource::{
@ -70,3 +71,4 @@ impl TerminalDisplayBundle {
self.image_handle.clone() self.image_handle.clone()
} }
} }

View File

@ -1,13 +1,22 @@
use std::io::stdout; use std::{io::stdout, fs::OpenOptions};
use bevy::prelude::*; use bevy::{
log::{
tracing_subscriber::{self, Registry, prelude::*},
LogPlugin, Level,
},
prelude::*, utils::tracing::level_filters::LevelFilter,
};
use crossterm::{ use crossterm::{
event::PopKeyboardEnhancementFlags, terminal::disable_raw_mode, ExecutableCommand, event::DisableMouseCapture,
terminal::{disable_raw_mode, LeaveAlternateScreen},
ExecutableCommand,
}; };
use grex_dither_post_process::DitherPostProcessPlugin; use grex_dither_post_process::DitherPostProcessPlugin;
use grex_framebuffer_extract::FramebufferExtractPlugin; use grex_framebuffer_extract::FramebufferExtractPlugin;
pub use crossterm::event::KeyCode; pub use crossterm;
pub use ratatui;
pub mod components; pub mod components;
pub mod events; pub mod events;
@ -18,26 +27,47 @@ pub struct TerminalDisplayPlugin;
impl Plugin for TerminalDisplayPlugin { impl Plugin for TerminalDisplayPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_plugins((DitherPostProcessPlugin, FramebufferExtractPlugin)) app.add_plugins((
.add_systems(Startup, systems::setup) DitherPostProcessPlugin,
.add_systems( FramebufferExtractPlugin,
Update, LogPlugin {
( update_subscriber: Some(|_| {
systems::input_handling, let log_file = OpenOptions::new()
systems::resize_handling, .write(true)
systems::print_to_terminal, .create(true)
), .open("debug.log")
) .unwrap();
.insert_resource(resources::EventQueue::default()) let file_layer = tracing_subscriber::fmt::Layer::new()
.insert_resource(resources::TerminalInput::default()) .with_writer(log_file)
.add_event::<events::TerminalInputEvent>(); .with_filter(LevelFilter::from_level(Level::INFO));
Box::new(Registry::default().with(file_layer))
}),
..Default::default()
},
))
.add_systems(Startup, systems::setup)
.add_systems(
Update,
(
systems::input_handling,
systems::resize_handling,
systems::print_to_terminal,
systems::widget_input_handling,
),
)
.insert_resource(resources::Terminal::default())
.insert_resource(resources::EventQueue::default())
.insert_resource(resources::TerminalInput::default())
.insert_resource(resources::TerminalUI::default())
.add_event::<events::TerminalInputEvent>();
} }
} }
impl Drop for TerminalDisplayPlugin { impl Drop for TerminalDisplayPlugin {
fn drop(&mut self) { fn drop(&mut self) {
let mut stdout = stdout(); let mut stdout = stdout();
stdout.execute(PopKeyboardEnhancementFlags).unwrap(); let _ = stdout.execute(DisableMouseCapture);
disable_raw_mode().unwrap(); let _ = stdout.execute(LeaveAlternateScreen);
let _ = disable_raw_mode();
} }
} }

View File

@ -1,7 +1,10 @@
use std::sync::{Arc, Mutex}; use std::{sync::{Arc, Mutex}, io::{stdout, Stdout}, fs::{File, OpenOptions}};
use bevy::{prelude::*, utils::HashSet}; use bevy::{prelude::*, utils::{HashSet, Uuid, HashMap, tracing::{subscriber, level_filters::LevelFilter}}, log::tracing_subscriber};
use crossterm::event::{Event, KeyCode}; use crossterm::{event::{Event, KeyCode, EnableMouseCapture}, terminal::{EnterAlternateScreen, enable_raw_mode}, ExecutableCommand};
use ratatui::{backend::CrosstermBackend, Frame, layout::Rect};
use crate::events::TerminalInputEvent;
#[derive(Resource, Default)] #[derive(Resource, Default)]
pub struct TerminalInput { pub struct TerminalInput {
@ -36,3 +39,52 @@ impl TerminalInput {
#[derive(Resource, Default)] #[derive(Resource, Default)]
pub(super) struct EventQueue(pub(super) Arc<Mutex<Vec<Event>>>); pub(super) struct EventQueue(pub(super) Arc<Mutex<Vec<Event>>>);
#[derive(Resource)]
pub struct Terminal(pub ratatui::Terminal<CrosstermBackend<Stdout>>);
impl Default for Terminal {
fn default() -> Self {
stdout().execute(EnterAlternateScreen).unwrap();
stdout().execute(EnableMouseCapture).unwrap();
enable_raw_mode().unwrap();
let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout())).expect("Failed to create terminal");
terminal.clear().expect("Failed to clear terminal");
Self(terminal)
}
}
#[derive(Resource, Default)]
pub struct TerminalUI {
widgets: HashMap<Uuid, Box<dyn TerminalWidget + Sync + Send>>
}
impl TerminalUI {
pub fn insert_widget(&mut self, widget: Box<dyn TerminalWidget + Sync + Send>) -> Uuid {
let id = Uuid::new_v4();
self.widgets.insert(id, widget);
id
}
pub fn get_widget(&mut self, id: Uuid) -> Option<&mut Box<dyn TerminalWidget + Sync + Send>> {
self.widgets.get_mut(&id)
}
pub fn destroy_widget(&mut self, id: Uuid) {
self.widgets.remove(&id);
}
pub fn widgets(&mut self) -> Vec<&mut Box<dyn TerminalWidget + Sync + Send>> {
let mut vec = self.widgets.values_mut().collect::<Vec<_>>();
vec.sort_by(|a, b| { a.depth().cmp(&b.depth()).reverse() });
vec
}
}
pub trait TerminalWidget {
fn init(&mut self) {}
fn update(&mut self) {}
fn render(&mut self, frame: &mut Frame, rect: Rect);
fn handle_events(&mut self, _event: &TerminalInputEvent) {}
fn depth(&self) -> u32 { 0 }
}

View File

@ -1,25 +1,20 @@
use std::{
io::{stdout, Write},
usize,
};
use bevy::{ use bevy::{
prelude::*, prelude::*,
render::render_resource::{Extent3d, TextureFormat}, render::render_resource::{Extent3d, TextureFormat},
}; };
use crossterm::{ use crossterm::event::{read, Event, KeyEventKind};
cursor::{self, MoveTo},
event::{read, Event, KeyEventKind, KeyboardEnhancementFlags, PushKeyboardEnhancementFlags},
terminal::enable_raw_mode,
ExecutableCommand, QueueableCommand,
};
use grex_framebuffer_extract::{ use grex_framebuffer_extract::{
components::FramebufferExtractDestination, render_assets::FramebufferExtractSource, components::FramebufferExtractDestination, render_assets::FramebufferExtractSource,
}; };
use crate::{ use crate::{
events::TerminalInputEvent, events::TerminalInputEvent,
resources::{EventQueue, TerminalInput}, resources::{EventQueue, Terminal, TerminalInput, TerminalUI},
};
use ratatui::{
prelude::*,
widgets::{Paragraph, Wrap},
}; };
const BRAILLE_CODE_MIN: u16 = 0x2800; const BRAILLE_CODE_MIN: u16 = 0x2800;
@ -41,16 +36,13 @@ pub fn setup(event_queue: Res<EventQueue>) {
} }
} }
}); });
let mut stdout = stdout();
enable_raw_mode().expect("Failed to put terminal into raw mode");
let _ = stdout.execute(PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::REPORT_EVENT_TYPES,
));
let _ = stdout.execute(cursor::Hide);
} }
pub fn print_to_terminal(image_exports: Query<&FramebufferExtractDestination>) { pub fn print_to_terminal(
mut terminal: ResMut<Terminal>,
mut terminal_ui: ResMut<TerminalUI>,
image_exports: Query<&FramebufferExtractDestination>,
) {
for image_export in image_exports.iter() { for image_export in image_exports.iter() {
let mut image = image_export let mut image = image_export
.0 .0
@ -93,10 +85,22 @@ pub fn print_to_terminal(image_exports: Query<&FramebufferExtractDestination>) {
} }
let string = output_buffer.into_iter().collect::<String>(); let string = output_buffer.into_iter().collect::<String>();
let mut stdout = stdout(); terminal
stdout.queue(MoveTo(0, 0)).unwrap(); .0
stdout.write_all(string.as_bytes()).unwrap(); .draw(|frame| {
stdout.flush().unwrap(); let area = frame.size();
frame.render_widget(
Paragraph::new(string)
.white()
.bold()
.wrap(Wrap { trim: true }),
area,
);
for widget in terminal_ui.widgets().iter_mut() {
widget.render(frame, area);
}
})
.expect("Failed to draw terminal frame");
} }
} }
@ -112,6 +116,17 @@ fn braille_char(mask: u8) -> char {
} }
} }
pub fn widget_input_handling(
mut terminal_ui: ResMut<TerminalUI>,
mut event_reader: EventReader<TerminalInputEvent>,
) {
for event in event_reader.read() {
for widget in terminal_ui.widgets().iter_mut() {
widget.handle_events(event);
}
}
}
pub fn input_handling( pub fn input_handling(
event_queue: Res<EventQueue>, event_queue: Res<EventQueue>,
mut input: ResMut<TerminalInput>, mut input: ResMut<TerminalInput>,