aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components.rs72
-rw-r--r--src/events.rs5
-rw-r--r--src/lib.rs43
-rw-r--r--src/resources.rs38
-rw-r--r--src/systems.rs154
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,
+ });
+ }
+ }
+ }
+}