diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/components.rs | 72 | ||||
-rw-r--r-- | src/events.rs | 5 | ||||
-rw-r--r-- | src/lib.rs | 43 | ||||
-rw-r--r-- | src/resources.rs | 38 | ||||
-rw-r--r-- | src/systems.rs | 154 |
5 files changed, 312 insertions, 0 deletions
diff --git a/src/components.rs b/src/components.rs new file mode 100644 index 0000000..560ad89 --- /dev/null +++ b/src/components.rs @@ -0,0 +1,72 @@ +use bevy::{ + prelude::*, + render::render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + }, +}; +use grex_dither_post_process::components::DitherPostProcessSettings; +use grex_framebuffer_extract::{ + components::{ExtractFramebufferBundle, FramebufferExtractDestination}, + render_assets::FramebufferExtractSource, +}; + +#[derive(Component)] +pub struct TerminalDisplay; + +#[derive(Bundle)] +pub struct TerminalDisplayBundle { + _terminal_display: TerminalDisplay, + extract_framebuffer_bundle: ExtractFramebufferBundle, + dither_post_process_settings: DitherPostProcessSettings, + image_handle: Handle<Image>, +} + +impl TerminalDisplayBundle { + pub fn new(dither_level: u32, asset_server: &AssetServer) -> Self { + let terminal_size = crossterm::terminal::size().unwrap(); + let size = Extent3d { + width: (terminal_size.0 as u32) * 2, + height: (terminal_size.1 as u32) * 4, + depth_or_array_layers: 1, + }; + + let mut image = Image { + texture_descriptor: TextureDescriptor { + label: None, + size, + dimension: TextureDimension::D2, + format: TextureFormat::R8Unorm, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_SRC + | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ..default() + }; + + image.resize(size); + let image_handle = asset_server.add(image); + + let framebuffer_extract_source = + asset_server.add(FramebufferExtractSource(image_handle.clone())); + + Self { + _terminal_display: TerminalDisplay, + extract_framebuffer_bundle: ExtractFramebufferBundle { + source: framebuffer_extract_source, + dest: FramebufferExtractDestination::default(), + }, + image_handle, + dither_post_process_settings: DitherPostProcessSettings::new( + dither_level, + asset_server, + ), + } + } + + pub fn image_handle(&self) -> Handle<Image> { + self.image_handle.clone() + } +} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..3f7057b --- /dev/null +++ b/src/events.rs @@ -0,0 +1,5 @@ +use bevy::prelude::*; +use crossterm::event::Event; + +#[derive(Event)] +pub struct TerminalInputEvent(pub Event); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3ad98f4 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,43 @@ +use std::io::stdout; + +use bevy::prelude::*; +use crossterm::{ + event::PopKeyboardEnhancementFlags, terminal::disable_raw_mode, ExecutableCommand, +}; +use grex_dither_post_process::DitherPostProcessPlugin; +use grex_framebuffer_extract::FramebufferExtractPlugin; + +pub use crossterm::event::KeyCode; + +pub mod components; +pub mod events; +pub mod resources; +mod systems; + +pub struct TerminalDisplayPlugin; + +impl Plugin for TerminalDisplayPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((DitherPostProcessPlugin, FramebufferExtractPlugin)) + .add_systems(Startup, systems::setup) + .add_systems( + Update, + ( + systems::input_handling, + systems::resize_handling, + systems::print_to_terminal, + ), + ) + .insert_resource(resources::EventQueue::default()) + .insert_resource(resources::TerminalInput::default()) + .add_event::<events::TerminalInputEvent>(); + } +} + +impl Drop for TerminalDisplayPlugin { + fn drop(&mut self) { + let mut stdout = stdout(); + stdout.execute(PopKeyboardEnhancementFlags).unwrap(); + disable_raw_mode().unwrap(); + } +} diff --git a/src/resources.rs b/src/resources.rs new file mode 100644 index 0000000..343967d --- /dev/null +++ b/src/resources.rs @@ -0,0 +1,38 @@ +use std::sync::{Arc, Mutex}; + +use bevy::{prelude::*, utils::HashSet}; +use crossterm::event::{Event, KeyCode}; + +#[derive(Resource, Default)] +pub struct TerminalInput { + pressed_keys: HashSet<KeyCode>, + released_keys: HashSet<KeyCode>, +} + +impl TerminalInput { + pub fn is_pressed(&self, code: KeyCode) -> bool { + self.pressed_keys.contains(&code) + } + + pub fn is_released(&self, code: KeyCode) -> bool { + self.released_keys.contains(&code) + } + + pub(super) fn press(&mut self, code: KeyCode) { + if !self.is_pressed(code) { + self.pressed_keys.insert(code); + } + } + + pub(super) fn release(&mut self, code: KeyCode) { + if self.is_pressed(code) { + self.pressed_keys.remove(&code); + } + if !self.is_released(code) { + self.released_keys.insert(code); + } + } +} + +#[derive(Resource, Default)] +pub(super) struct EventQueue(pub(super) Arc<Mutex<Vec<Event>>>); diff --git a/src/systems.rs b/src/systems.rs new file mode 100644 index 0000000..77b8d11 --- /dev/null +++ b/src/systems.rs @@ -0,0 +1,154 @@ +use std::{ + io::{stdout, Write}, + usize, +}; + +use bevy::{ + prelude::*, + render::render_resource::{Extent3d, TextureFormat}, +}; +use crossterm::{ + cursor::{self, MoveTo}, + event::{read, Event, KeyEventKind, KeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, + terminal::enable_raw_mode, + ExecutableCommand, QueueableCommand, +}; +use grex_framebuffer_extract::{ + components::FramebufferExtractDestination, render_assets::FramebufferExtractSource, +}; + +use crate::{ + events::TerminalInputEvent, + resources::{EventQueue, TerminalInput}, +}; + +const BRAILLE_CODE_MIN: u16 = 0x2800; +const BRAILLE_CODE_MAX: u16 = 0x28FF; +const BRAILLE_DOT_BIT_POSITIONS: [u8; 8] = [0, 1, 2, 6, 3, 4, 5, 7]; + +pub fn setup(event_queue: Res<EventQueue>) { + let event_queue = event_queue.0.clone(); + std::thread::spawn(move || { + loop { + // `read()` blocks until an `Event` is available + match read() { + Ok(event) => { + event_queue.lock().unwrap().push(event); + } + Err(err) => { + panic!("Error reading input events: {:?}", err); + } + } + } + }); + + 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>) { + for image_export in image_exports.iter() { + let mut image = image_export + .0 + .lock() + .expect("Failed to get lock on output texture"); + //TODO: Find a better way of preventing first frame + if image.size() == UVec2::ONE { + continue; + } + if image.texture_descriptor.format != TextureFormat::R8Unorm { + warn_once!("Extracted framebuffer texture is not R8Unorm format. Will attempt conversion, but consider changing your render texture's format."); + info_once!("{:?}", image); + match image.convert(TextureFormat::R8Unorm) { + Some(img) => *image = img, + None => error_once!( + "Could not convert to R8Unorm texture format. Unexpected output may occur." + ), + }; + } + + let mut output_buffer = Vec::<char>::new(); + let width = image.width(); + let height = image.height(); + let data = &image.data; + for character_y in (0..height).step_by(4) { + for character_x in (0..width).step_by(2) { + let mut mask: u8 = 0; + for offset_x in 0..2 { + for offset_y in 0..4 { + let x = character_x + offset_x; + let y = character_y + offset_y; + if x < width && y < height && data[(y * width + x) as usize] == 0xFF { + mask |= 1 + << (BRAILLE_DOT_BIT_POSITIONS[(offset_x * 4 + offset_y) as usize]); + } + } + } + output_buffer.push(braille_char(mask)); + } + } + + let string = output_buffer.into_iter().collect::<String>(); + let mut stdout = stdout(); + stdout.queue(MoveTo(0, 0)).unwrap(); + stdout.write_all(string.as_bytes()).unwrap(); + stdout.flush().unwrap(); + } +} + +fn braille_char(mask: u8) -> char { + match char::from_u32((BRAILLE_CODE_MIN + mask as u16) as u32) { + Some(character) => { + if character as u16 > BRAILLE_CODE_MAX { + panic!("Number too big!") + } + character + } + None => panic!("Error converting character!"), + } +} + +pub fn input_handling( + event_queue: Res<EventQueue>, + mut input: ResMut<TerminalInput>, + mut event_writer: EventWriter<TerminalInputEvent>, +) { + let mut event_queue = event_queue.0.lock().unwrap(); + while let Some(event) = event_queue.pop() { + if let Event::Key(event) = event { + match event.kind { + KeyEventKind::Press => { + input.press(event.code); + } + KeyEventKind::Release => { + input.release(event.code); + } + _ => (), + } + } + event_writer.send(TerminalInputEvent(event)); + } +} + +pub fn resize_handling( + mut images: ResMut<Assets<Image>>, + mut sources: ResMut<Assets<FramebufferExtractSource>>, + mut event_reader: EventReader<TerminalInputEvent>, +) { + for event in event_reader.read() { + if let Event::Resize(w, h) = event.0 { + for source in sources.iter_mut() { + let image = images.get_mut(&source.1 .0).unwrap(); + image.resize(Extent3d { + width: w as u32 * 2, + height: h as u32 * 4, + depth_or_array_layers: 1, + }); + } + } + } +} |