├── .gitignore ├── Cargo.toml ├── README.md ├── examples ├── basic.rs └── capybara.jpg └── src ├── 3270 Narrow Nerd Font Complete.ttf ├── bema.rs ├── gui_runner.rs ├── hovercraft_runner.rs ├── lib.rs ├── runner.rs └── terminal_runner.rs /.gitignore: -------------------------------------------------------------------------------- 1 | *.jpg 2 | *.png 3 | *.hc 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [profile.dev.package.'*'] 2 | opt-level = 3 3 | 4 | [package] 5 | name = "bema" 6 | version = "0.0.9" 7 | authors = ["yazgoo "] 8 | edition = "2018" 9 | license = "MIT" 10 | description = "presentation in the terminal" 11 | readme = "README.md" 12 | homepage = "https://github.com/yazgoo/bema" 13 | repository = "https://github.com/yazgoo/bema" 14 | keywords = ["terminal"] 15 | categories = [] 16 | 17 | [dependencies] 18 | crossterm = "0.18" 19 | syntect = "4.4" 20 | tempfile = "3.2" 21 | compile-time-run = "0.2" 22 | blockish = "0.0.9" 23 | macroquad = "0.3.0" 24 | image = "0.23" 25 | indoc = "1.0" 26 | 27 | [dev-dependencies] 28 | 29 | plotters = "0.3.0" 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🗣 bema 2 | 3 | [![Discord](https://img.shields.io/badge/discord--blue?logo=discord)](https://discord.gg/F684Y8rYwZ) 4 | 5 | Write your next slideshow in rust 🦀, as a self-contained binary 📦. 6 | 7 | ## 🦀 DSL 8 | 9 | See [examples/basic.rs](examples/basic.rs). 10 | 11 | ## 👀 frontends 12 | 13 | There are several ways you can display your slideshow. 14 | 15 | ### 🖥 GUI 16 | 17 | ![demo](https://raw.githubusercontent.com/yazgoo/bema/gh-pages/screenshot_gui.gif) 18 | `cargo run --example basic gui` 19 | 20 | Invoke the program with `gui` as argument. 21 | Press `escape` for help on usage keys. 22 | 23 | ### 💾 Terminal 24 | 25 | ![demo](https://raw.githubusercontent.com/yazgoo/bema/gh-pages/screenshot.gif) 26 | `cargo run --example basic` 27 | 28 | Invoke the program with no argument. 29 | For now, full definition images are only supported within [kitty](https://sw.kovidgoyal.net/kitty/), 30 | otherwise the program will fallback on [blockish](https://github.com/yazgoo/blockish/). 31 | Use arrow keys or `hjkl` to navigate, `q` to quit. 32 | 33 | ### 🕸 in browser with hovercraft 34 | 35 | `cargo run --example basic hovecraft` 36 | 37 | Just invoke the program with `hovercraft` as argument. 38 | This will output an [hovercraft](https://hovercraft.readthedocs.io) file (as well as images) that you 39 | can then interpret with hovercraft: 40 | 41 | `cargo run --example basic hovercraft > pres.hc && hovercraft pres.hc` 42 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | extern crate bema; 2 | 3 | use plotters::prelude::*; 4 | use bema::*; 5 | use indoc::indoc; 6 | use compile_time_run::run_command; 7 | use image::ImageBuffer; 8 | use image::DynamicImage; 9 | 10 | fn image_from_plot(width: usize, height: usize, f: &dyn Fn(BitMapBackend) -> Result<(), Box>) -> Result, Box> { 11 | let size = width * height * 3; 12 | let mut plot = Vec::with_capacity(size); 13 | for _ in 0..size { plot.push(0); } 14 | let backend = BitMapBackend::with_buffer(&mut plot, (width as u32, height as u32)); 15 | let _ = f(backend)?; 16 | let img = DynamicImage::ImageRgb8(ImageBuffer::from_raw(width as u32, height as u32, plot.to_vec()).unwrap()); 17 | let mut bytes: Vec = Vec::new(); 18 | img.write_to(&mut bytes, image::ImageOutputFormat::Png)?; 19 | Ok(bytes) 20 | } 21 | 22 | fn main() -> Result<(), Box> { 23 | 24 | slides(|b| { 25 | 26 | b.slide("a slide with just text", |s| { 27 | s.text("text in the first slide") 28 | .text("") 29 | .t("t is an alias for text") 30 | .text("") 31 | .text("multi line text is also supported:") 32 | .text(indoc! {" 33 | 34 | • this is very usefull 35 | • for lists 36 | • while keeping them 37 | • centered 38 | 39 | "}) 40 | }) 41 | 42 | .slide("code", |s| { 43 | s.t("").t("helloworld.c").t("") 44 | .code("c", indoc! {r#" 45 | #include 46 | int main() { 47 | printf("Hello, World!"); 48 | return 0; 49 | } 50 | "#}) 51 | }) 52 | 53 | .slide("diagram - requires dot (graphviz) at compile time", |s| { 54 | s.image(run_command!("sh", "-c", r##"echo ' 55 | digraph X { 56 | rankdir = "LR" 57 | bgcolor= "transparent" 58 | node [ style=filled,fill="#65b2ff", fontname = "helvetica", shape = "rectangle" ] 59 | edge [ color="#65b2ff" , fontname = "helvetica", fontcolor="#65b2ff"] 60 | graph [ fontname = "helvetica", color="#3f6190", fontcolor="#3f6190", nodesep="0" ]; 61 | a -> b -> c 62 | b -> d 63 | dpi=500 64 | } 65 | ' | dot -Tpng"##).to_vec(), ".png", Some(500)) 66 | }) 67 | 68 | .slide("plotting (using plotters crate)", |s| { 69 | s.image(image_from_plot(800, 500, 70 | &(|backend| -> Result<(), Box> { 71 | let root = backend.into_drawing_area(); 72 | root.fill(&WHITE)?; 73 | 74 | let mut chart = ChartBuilder::on(&root) 75 | .x_label_area_size(35) 76 | .y_label_area_size(40) 77 | .margin(5) 78 | .build_cartesian_2d((0u32..10u32).into_segmented(), 0u32..10u32)?; 79 | 80 | chart 81 | .configure_mesh() 82 | .disable_x_mesh() 83 | .bold_line_style(&WHITE.mix(0.3)) 84 | .y_desc("Count") 85 | .x_desc("Bucket") 86 | .axis_desc_style(("sans-serif", 15)) 87 | .draw()?; 88 | 89 | let data = [ 90 | 0u32, 1, 1, 1, 4, 2, 5, 7, 8, 6, 4, 2, 1, 8, 3, 3, 3, 4, 4, 3, 3, 3, 91 | ]; 92 | 93 | chart.draw_series( 94 | Histogram::vertical(&chart) 95 | .style(RED.mix(0.5).filled()) 96 | .data(data.iter().map(|x: &u32| (*x, 1))), 97 | )?; 98 | 99 | Ok(()) 100 | })).unwrap(), ".png", None) 101 | .t("a plot") 102 | }) 103 | 104 | .slide("using cols and rows", |s| { 105 | s.cols(|b| { 106 | b.rows(|r| { 107 | r.image(include_bytes!("capybara.jpg").to_vec(), ".jpg", Some(500)) 108 | .text("a capybara") 109 | }) 110 | .code("rs", indoc! {r#" 111 | // main function 112 | fn main() { 113 | 114 | // Print to the console 115 | println!("Hello World!"); 116 | } 117 | "#}) 118 | }) 119 | }) 120 | 121 | .slide("frames (can be used for tables)", |s| { 122 | s.framed(|f| { 123 | f.cols(|c| { 124 | c.rows(|r| { 125 | r.framed(|f1| { 126 | f1.framed(|f2| f2.text("ruby") ) 127 | .code("py", r#"puts "test""#) 128 | .image(include_bytes!("ruby.png").to_vec(), ".png", Some(200)) 129 | }) 130 | }) 131 | .rows(|r| { 132 | r.framed(|f1| { 133 | f1.framed(|f2| f2.text("python") ) 134 | .code("py", r#"print("test")"#) 135 | .image(include_bytes!("python.png").to_vec(), ".png", Some(200)) 136 | }) 137 | }) 138 | .rows(|r| { 139 | r.framed(|f1| { 140 | f1.framed(|f2| f2.text("rust") ) 141 | .code("rs", r#"println!("test")"#) 142 | .image(include_bytes!("rust.png").to_vec(), ".png", Some(200)) 143 | }) 144 | }) 145 | }) 146 | }) 147 | }) 148 | 149 | }).run()?; 150 | 151 | Ok(()) 152 | } 153 | -------------------------------------------------------------------------------- /examples/capybara.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yazgoo/bema/d398f27287ca1eab5537e654249ca16214e3f71c/examples/capybara.jpg -------------------------------------------------------------------------------- /src/3270 Narrow Nerd Font Complete.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yazgoo/bema/d398f27287ca1eab5537e654249ca16214e3f71c/src/3270 Narrow Nerd Font Complete.ttf -------------------------------------------------------------------------------- /src/bema.rs: -------------------------------------------------------------------------------- 1 | 2 | #[derive(Clone)] 3 | pub enum SlideItem { 4 | Code{ extension: String, source: String }, 5 | Image{ image: Vec, extension: String, width: Option }, 6 | Text{ text: String }, 7 | Rows { items: Vec }, 8 | Cols { items: Vec }, 9 | Framed { items: Vec }, 10 | } 11 | 12 | #[derive(Clone)] 13 | pub struct Slide { 14 | pub title: String, 15 | pub items: Vec, 16 | } 17 | 18 | #[derive(Clone)] 19 | pub struct Bema { 20 | pub slides: Vec 21 | } 22 | -------------------------------------------------------------------------------- /src/gui_runner.rs: -------------------------------------------------------------------------------- 1 | use crate::runner::{Runner, get_justify, fit_image_bytes}; 2 | use crate::bema::{Bema, SlideItem, Slide}; 3 | use indoc::indoc; 4 | 5 | use std::collections::HashMap; 6 | use std::time::{SystemTime, Duration}; 7 | use syntect::easy::HighlightLines; 8 | use syntect::parsing::SyntaxSet; 9 | use syntect::highlighting::{ThemeSet, Style}; 10 | use syntect::util::LinesWithEndings; 11 | 12 | use crossterm::Result; 13 | use macroquad::prelude::*; 14 | use miniquad::{BlendState, BlendValue, BlendFactor, Equation}; 15 | 16 | fn get_transition_duration() -> u128 { 17 | 200 18 | } 19 | 20 | pub struct GuiRunner { 21 | } 22 | 23 | 24 | fn get_justify_px(font_size: u16, texts: Vec<&String>, total_width: f32) -> f32 { 25 | let font_width = font_size / 2; 26 | (font_width as usize * get_justify((total_width/ font_width as f32) as usize, texts).unwrap_or(0)) as f32 27 | } 28 | 29 | fn main_draw_texture(textures: &mut HashMap<(i32, usize),Texture2D>, bytes: &[u8], width: &Option, extension: &String, pos: usize, i: i32, dx: f32, y: &mut f32, total_width: f32) { 30 | match textures.get(&(i, pos)) { 31 | Some(_) => {}, 32 | None => { 33 | let bytes = fit_image_bytes(bytes, width, extension); 34 | let texture = Texture2D::from_file_with_format(&bytes[..], None); 35 | textures.insert((i, pos), texture); 36 | } 37 | }; 38 | let texture = *textures.get(&(i, pos)).unwrap(); 39 | let w = total_width; 40 | let x = if w < texture.width() { 41 | 0.0 42 | } else { 43 | (w - texture.width()) / 2.0 44 | }; 45 | draw_texture(texture, x + dx, *y, WHITE); 46 | *y += texture.width(); 47 | } 48 | 49 | fn main_capture_input(bema: &Bema, i: &mut i32, scale: &mut f32, antibounce: &mut SystemTime, transition: &mut SystemTime, transition_direction: &mut f32, help: &mut bool, decoration: &mut bool, white_mode: &mut bool) { 50 | let mut changed = false; 51 | 52 | if antibounce.elapsed().unwrap_or(Duration::from_millis(0)).as_millis() >= get_transition_duration() { 53 | if is_key_down(miniquad::KeyCode::Right) || is_key_down(miniquad::KeyCode::Down) || is_key_down(miniquad::KeyCode::L) || is_key_down(miniquad::KeyCode::J) || is_key_down(miniquad::KeyCode::N) || is_key_down(miniquad::KeyCode::Space) || is_mouse_button_down(miniquad::MouseButton::Left) { 54 | *i += 1; 55 | *transition_direction = -1.0; 56 | changed = true; 57 | } 58 | if is_key_down(miniquad::KeyCode::Left) || is_key_down(miniquad::KeyCode::Up) || is_key_down(miniquad::KeyCode::H) || is_key_down(miniquad::KeyCode::K) || is_key_down(miniquad::KeyCode::P) || is_mouse_button_down(miniquad::MouseButton::Right) { 59 | *i -= 1; 60 | *transition_direction = 1.0; 61 | changed = true; 62 | } 63 | if is_key_down(miniquad::KeyCode::Q) { 64 | std::process::exit(0); 65 | } 66 | if is_key_down(miniquad::KeyCode::M) { 67 | *scale *= 1.1; 68 | } 69 | if is_key_down(miniquad::KeyCode::R) { 70 | *scale /= 1.1; 71 | } 72 | if is_key_down(miniquad::KeyCode::Escape) { 73 | *help = !*help; 74 | } 75 | if is_key_down(miniquad::KeyCode::D) { 76 | *decoration = !*decoration; 77 | } 78 | if is_key_down(miniquad::KeyCode::C) { 79 | *white_mode = !*white_mode; 80 | } 81 | if is_key_down(miniquad::KeyCode::S) { 82 | let png_path = format!("bema_slide_{}.png", *i); 83 | println!("export png: {}", png_path); 84 | macroquad::texture::get_screen_data().export_png(&png_path); 85 | } 86 | if is_key_down(miniquad::KeyCode::G) { 87 | *i = 0; 88 | } 89 | if *i >= bema.slides.len() as i32 { 90 | *i = 0; 91 | } 92 | else if *i < 0 { 93 | *i = bema.slides.len() as i32 - 1; 94 | } 95 | *antibounce = SystemTime::now(); 96 | if changed { 97 | *transition = SystemTime::now(); 98 | *help = false; 99 | } 100 | } 101 | } 102 | 103 | fn scalef(font_size: u16, scale: f32) -> u16 { 104 | (font_size as f32 * scale as f32) as u16 105 | } 106 | 107 | fn write_text(text_size: u16, font: Font, font_color: Color, dx: f32, y: &mut f32, text: &String, total_width: f32) { 108 | let splits = text.split("\n").map( |x| x.to_string()).collect::>(); 109 | let v2: Vec<&String> = splits.iter().map(|s| s).collect::>(); 110 | let x = get_justify_px(text_size, v2, total_width) + dx; 111 | for split in splits { 112 | draw_text_ex(&split, x, *y + text_size as f32, TextParams { font_size: text_size, font, 113 | color: font_color, 114 | ..Default::default() 115 | }); 116 | *y += text_size as f32; 117 | } 118 | } 119 | 120 | fn write_code(text_size: u16, font: Font, dx: f32, y: &mut f32, extension: &String, source: &String, total_width: f32) { 121 | let ps = SyntaxSet::load_defaults_newlines(); 122 | let ts = ThemeSet::load_defaults(); 123 | 124 | let syntax = ps.find_syntax_by_extension(extension).unwrap(); 125 | let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]); 126 | let splits = source.split("\n").map( |x| x.to_string()).collect::>(); 127 | let v2: Vec<&String> = splits.iter().map(|s| s).collect::>(); 128 | let x = get_justify_px(text_size, v2, total_width) + dx; 129 | for line in LinesWithEndings::from(source) { 130 | let ranges: Vec<(Style, &str)> = h.highlight(line, &ps); 131 | let mut dx = 0.0; 132 | for range in ranges { 133 | let c = range.0.foreground; 134 | draw_text_ex(range.1, (x + (dx * (text_size as f32 / 2.0))) as f32, *y + text_size as f32, TextParams { font_size: text_size, font, 135 | color: macroquad::color::Color::new(c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0, c.a as f32 / 255.0), 136 | ..Default::default() 137 | }); 138 | dx += range.1.len() as f32; 139 | } 140 | *y += text_size as f32; 141 | } 142 | } 143 | 144 | fn draw_item(font: Font, font_color: Color, i: i32, pos: usize, item: &SlideItem, dx: f32, y: &mut f32, total_width: f32, textures: &mut HashMap<(i32, usize), Texture2D>, scale: f32) { 145 | let text_size : u16 = scalef(60, scale); 146 | match item { 147 | SlideItem::Image { image: bytes, extension, width } => { 148 | main_draw_texture(textures, bytes, width, &extension, pos, i, dx, y, total_width); 149 | }, 150 | SlideItem::Code { extension, source } => { 151 | write_code(text_size, font, dx, y, extension, source, total_width); 152 | }, 153 | SlideItem::Text { text } => { 154 | write_text(text_size, font, font_color, dx, y, text, total_width); 155 | }, 156 | SlideItem::Cols { items } => { 157 | let w = total_width / items.len() as f32; 158 | let mut ys = vec![]; 159 | for (pos2, item2) in items.iter().enumerate() { 160 | let mut y2 = *y; 161 | draw_item(font, font_color, i, pos + pos2, item2, dx + w * pos2 as f32, &mut y2, w, textures, scale); 162 | ys.push(y2); 163 | } 164 | *y = ys.iter().cloned().fold(0.0, |a, b| { a.max(b) }) 165 | }, 166 | SlideItem::Rows { items } => { 167 | for (k, item2) in items.iter().enumerate() { 168 | draw_item(font, font_color, i, pos + k, item2, dx as f32, y, total_width, textures, scale); 169 | } 170 | }, 171 | SlideItem::Framed { items } => { 172 | let y0 = *y; 173 | for (k, item2) in items.iter().enumerate() { 174 | draw_item(font, font_color, i, pos + k, item2, dx as f32, y, total_width, textures, scale); 175 | } 176 | draw_rectangle_lines(dx, y0, total_width, *y - y0, 2.0, font_color); 177 | }, 178 | } 179 | } 180 | 181 | fn draw_slide(font: Font, font_color: Color, bar_color: Color, textures: &mut HashMap<(i32, usize), Texture2D>, bema: &Bema, i: i32, dx: f32, scale: f32, total_width: f32) { 182 | let title_size : u16 = scalef(80, scale); 183 | let index_size : u16 = scalef(20, scale); 184 | 185 | let k = if i >= (bema.slides.len() as i32) { 0 } else if i < 0 { bema.slides.len() as i32 - 1 } else { i }; 186 | let slide = bema.slides.get(k as usize).unwrap(); 187 | let mut y = index_size as f32; 188 | draw_rectangle(dx, 0.0, total_width * ((i as f32 + 1.0) / bema.slides.len() as f32), index_size as f32 / 10.0, bar_color); 189 | draw_text_ex(format!("{}/{}", i + 1, bema.slides.len()).as_str(), 20.0 + dx, y, TextParams { font_size: index_size, font, 190 | color: bar_color, 191 | ..Default::default() 192 | }); 193 | y += title_size as f32; 194 | 195 | draw_text_ex(&slide.title, get_justify_px(title_size, vec![&slide.title], total_width) + dx, y, TextParams { font_size: title_size, font, 196 | color: font_color, 197 | ..Default::default() 198 | }); 199 | y += 2.0 * title_size as f32; 200 | for (pos, item) in slide.items.iter().enumerate() { 201 | draw_item(font, font_color, i, pos, item, dx, &mut y, total_width, textures, scale); 202 | }; 203 | } 204 | 205 | 206 | fn draw_help(font: Font, font_color: Color, bar_color: Color, textures: &mut HashMap<(i32, usize), Texture2D>, decoration: bool, white_mode: bool, scale: f32) { 207 | draw_slide(font, font_color, bar_color, textures, &Bema { 208 | slides: vec![Slide { 209 | title: "bema help".to_string(), 210 | items: vec![ 211 | SlideItem::Text { text: "keys:".to_string() }, 212 | SlideItem::Text { text: "".to_string() }, 213 | SlideItem::Text { text: format!(indoc! {" 214 | next slide right, down, L, J, N 215 | previous slide left, up, H, K, P 216 | exit Q 217 | scale up M 218 | scale down R 219 | screenshot S 220 | [{}] decoration D 221 | [{}] white mode C 222 | [{}] help Escape" 223 | }, if decoration { "x" } else { " " }, if white_mode { "x" } else { " " }, "x") }, 224 | ], 225 | }] 226 | }, 0, 0.0, scale, screen_width()); 227 | } 228 | 229 | async fn main_gui_runner(bema: Bema) { 230 | let font = load_ttf_font_from_bytes(include_bytes!("3270 Narrow Nerd Font Complete.ttf")); 231 | let mut i : i32 = 0; 232 | let mut antibounce = SystemTime::now(); 233 | let mut transition = SystemTime::now(); 234 | let mut textures = HashMap::new(); 235 | 236 | let mut transition_direction = 0.0; 237 | let mut scale : f32 = 1.0; 238 | 239 | let mut help = false; 240 | let mut decoration = true; 241 | let mut white_mode = false; 242 | 243 | let render_target = render_target(screen_width() as u32, (screen_height() * 0.6) as u32); 244 | let material = 245 | load_material(CRT_VERTEX_SHADER, CRT_FRAGMENT_SHADER, Default::default()).unwrap(); 246 | 247 | let pipeline_params = PipelineParams { 248 | color_blend: Some(BlendState::new( 249 | Equation::Add, 250 | BlendFactor::Value(BlendValue::SourceAlpha), 251 | BlendFactor::OneMinusValue(BlendValue::SourceAlpha), 252 | )), 253 | ..Default::default() 254 | }; 255 | 256 | let reverse_material = load_material( 257 | CRT_VERTEX_SHADER, 258 | CRT_FRAGMENT_SHADER_REVERSE_BLACK, 259 | MaterialParams { 260 | pipeline_params, 261 | ..Default::default() 262 | }, 263 | ) 264 | .unwrap(); 265 | 266 | 267 | let mut font_color; 268 | let mut background_color; 269 | let mut bar_color; 270 | 271 | loop { 272 | if white_mode { 273 | font_color = BLACK; 274 | background_color = WHITE; 275 | bar_color = LIGHTGRAY; 276 | } 277 | else { 278 | font_color = WHITE; 279 | background_color = BLACK; 280 | bar_color = DARKGRAY; 281 | } 282 | if decoration { 283 | // draw to texture 284 | let camera = Camera2D { 285 | zoom: vec2(2.0 / screen_width(), 3.0 / screen_height()), 286 | target: vec2(screen_width() / 2.0, screen_height() / 3.0), 287 | render_target: Some(render_target), 288 | ..Default::default() 289 | }; 290 | set_camera(&camera); 291 | } 292 | clear_background(background_color); 293 | 294 | if help { 295 | draw_help(font, font_color, bar_color, &mut textures, decoration, white_mode, scale); 296 | } 297 | else { 298 | let dt = transition.elapsed().unwrap_or(Duration::from_millis(0)).as_millis(); 299 | let dt = if dt > get_transition_duration() || transition_direction == 0.0 { transition_direction = 0.0; get_transition_duration() } else { dt }; 300 | let dx = transition_direction * screen_width() * dt as f32 / get_transition_duration() as f32; 301 | if transition_direction != 0.0 { draw_slide(font, font_color, bar_color, &mut textures, &bema, i - 1 + transition_direction as i32, dx - screen_width(), scale, screen_width()); } 302 | 303 | draw_slide(font, font_color, bar_color, &mut textures, &bema, i + transition_direction as i32, dx, scale, screen_width()); 304 | if transition_direction != 0.0 { draw_slide(font, font_color, bar_color, &mut textures, &bema, i + 1 + transition_direction as i32, dx + screen_width(), scale, screen_width()); } 305 | } 306 | 307 | 308 | // draw to screen 309 | if decoration { 310 | set_default_camera(); 311 | 312 | clear_background(background_color); 313 | gl_use_material(material); 314 | draw_texture_ex( 315 | render_target.texture, 316 | 0.0, 317 | 0.0, 318 | WHITE, 319 | DrawTextureParams { 320 | dest_size: Some(vec2(screen_width(), screen_height() * 0.6)), 321 | ..Default::default() 322 | }, 323 | ); 324 | gl_use_material(reverse_material); 325 | draw_texture_ex( 326 | render_target.texture, 327 | 0.0, 328 | screen_height() * 0.6, 329 | WHITE, 330 | DrawTextureParams { 331 | dest_size: Some(vec2(screen_width(), screen_height() * 0.6)), 332 | ..Default::default() 333 | }, 334 | ); 335 | gl_use_default_material(); 336 | } 337 | main_capture_input(&bema, &mut i, &mut scale, &mut antibounce, &mut transition, &mut transition_direction, &mut help, &mut decoration, &mut white_mode); 338 | next_frame().await; 339 | } 340 | } 341 | 342 | impl Runner for GuiRunner { 343 | fn run(&self, bema: &Bema) -> Result<()> { 344 | 345 | macroquad::Window::new("Bema", main_gui_runner(bema.clone())); 346 | Ok(()) 347 | } 348 | } 349 | 350 | 351 | const CRT_FRAGMENT_SHADER: &'static str = r#"#version 100 352 | precision lowp float; 353 | varying vec4 color; 354 | varying vec2 uv; 355 | uniform sampler2D Texture; 356 | void main() { 357 | 358 | vec3 res = texture2D(Texture, uv).rgb * color.rgb; 359 | gl_FragColor = vec4(res, 1.0); 360 | } 361 | "#; 362 | 363 | 364 | const CRT_FRAGMENT_SHADER_REVERSE_BLACK: &'static str = r#"#version 100 365 | precision lowp float; 366 | varying vec4 color; 367 | varying vec2 uv; 368 | uniform sampler2D Texture; 369 | uniform vec4 _Time; 370 | void main() { 371 | 372 | vec2 uv2 = vec2(uv[0] + 0.003 * uv[1] * sin(mod(_Time.x, 100.0) + 100.0 * uv[1]), 1.0 - uv[1]); 373 | vec3 res = texture2D(Texture, uv2).rgb * color.rgb; 374 | gl_FragColor = vec4(res, 1.0 * pow(uv2[1], 4.0)); 375 | } 376 | "#; 377 | 378 | const CRT_VERTEX_SHADER: &'static str = "#version 100 379 | attribute vec3 position; 380 | attribute vec2 texcoord; 381 | attribute vec4 color0; 382 | varying lowp vec2 uv; 383 | varying lowp vec4 color; 384 | uniform mat4 Model; 385 | uniform mat4 Projection; 386 | void main() { 387 | gl_Position = Projection * Model * vec4(position, 1); 388 | color = color0 / 255.0; 389 | uv = texcoord; 390 | } 391 | "; 392 | -------------------------------------------------------------------------------- /src/hovercraft_runner.rs: -------------------------------------------------------------------------------- 1 | use crate::runner::Runner; 2 | use crate::bema::{Bema, SlideItem}; 3 | use std::fs::File; 4 | use crossterm::Result; 5 | use std::io::Write; 6 | 7 | pub struct HovercraftRunner { 8 | } 9 | 10 | impl HovercraftRunner { 11 | fn render_item(&self, item: &SlideItem, img_i: &mut usize) -> Result<()> { 12 | match item { 13 | SlideItem::Image { image, extension, width } => { 14 | let file_path = format!("bema_{}{}", img_i, extension); 15 | *img_i += 1; 16 | let mut buffer = File::create(&file_path)?; 17 | buffer.write_all(image)?; 18 | println!(".. image:: {}", &file_path); 19 | width.map( |w| println!(" :width: {} px", w)); 20 | }, 21 | SlideItem::Code { extension, source } => { 22 | println!(".. code:: {}", extension); 23 | println!(); 24 | let splits = source.split("\n").map( |x| x.to_string()).collect::>(); 25 | for split in splits { 26 | println!(" {}", split); 27 | } 28 | }, 29 | SlideItem::Text { text } => { 30 | println!("{}", text); 31 | }, 32 | SlideItem::Cols { items } => { 33 | for item2 in items { 34 | self.render_item(item2, img_i)?; 35 | } 36 | }, 37 | SlideItem::Rows { items } => { 38 | for item2 in items { 39 | self.render_item(item2, img_i)?; 40 | } 41 | }, 42 | SlideItem::Framed { items } => { 43 | for item2 in items { 44 | self.render_item(item2, img_i)?; 45 | } 46 | }, 47 | } 48 | Ok(()) 49 | } 50 | } 51 | 52 | impl Runner for HovercraftRunner { 53 | fn run(&self, bema: &Bema) -> Result<()> { 54 | for (i, slide) in bema.slides.iter().enumerate() { 55 | if i > 0 { println!("----"); } 56 | println!(""); 57 | println!("{}", slide.title); 58 | for _ in 0..slide.title.len() { 59 | print!("="); 60 | } 61 | println!(""); 62 | println!(""); 63 | let mut img_i = 0; 64 | for item in &slide.items { 65 | self.render_item(item, &mut img_i)?; 66 | }; 67 | println!(""); 68 | } 69 | Ok(()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod runner; 2 | use crate::runner::{Runner}; 3 | mod hovercraft_runner; 4 | use crate::hovercraft_runner::HovercraftRunner; 5 | mod terminal_runner; 6 | use crate::terminal_runner::TerminalRunner; 7 | mod gui_runner; 8 | use crate::gui_runner::GuiRunner; 9 | mod bema; 10 | use crate::bema::{Bema, SlideItem, Slide}; 11 | 12 | use std::env; 13 | 14 | use crossterm::Result; 15 | 16 | pub fn slides(f: fn(Bema) -> Bema) -> Bema { 17 | f(Bema { 18 | slides: vec![], 19 | }) 20 | } 21 | 22 | impl Slide { 23 | } 24 | 25 | impl Bema { 26 | pub fn slide(mut self, title: &str, f: fn(Slide) -> Slide) -> Bema { 27 | 28 | let s = Slide { 29 | title: String::from(title), 30 | items: vec![], 31 | }; 32 | self.slides.push(f(s)); 33 | self 34 | } 35 | 36 | pub fn run(&self) -> Result<()> { 37 | if env::args().len() == 2 { 38 | let args : Vec = env::args().collect(); 39 | match args[1].as_str() { 40 | "hovercraft" => HovercraftRunner { }.run(&self)?, 41 | "gui" => GuiRunner { }.run(&self)?, 42 | _ => {} 43 | } 44 | } else { 45 | TerminalRunner { }.run(&self)?; 46 | } 47 | Ok(()) 48 | } 49 | } 50 | 51 | pub struct SlideItems { 52 | items: Vec, 53 | } 54 | 55 | pub trait Helper { 56 | fn push(self, item: SlideItem) -> Self where Self: Sized; 57 | 58 | fn text(self, s: &str) -> Self where Self: Sized { 59 | self.push(SlideItem::Text { text: String::from(s) }) 60 | } 61 | 62 | fn t(self, s: &str) -> Self where Self: Sized { 63 | self.text(s) 64 | } 65 | 66 | fn code(self, extension: &str, source: &str) -> Self where Self: Sized { 67 | self.push(SlideItem::Code { extension: String::from(extension), source: String::from(source) }) 68 | } 69 | 70 | fn image(self, image: Vec, extension: &str, width: Option) -> Self where Self: Sized { 71 | self.push(SlideItem::Image { image, extension: String::from(extension), width }) 72 | } 73 | 74 | fn cols(self, f: fn(SlideItems) -> SlideItems) -> Self where Self: Sized { 75 | self.push(SlideItem::Cols { items: f(SlideItems { items: vec![]}).items }) 76 | } 77 | 78 | fn rows(self, f: fn(SlideItems) -> SlideItems) -> Self where Self: Sized { 79 | self.push(SlideItem::Rows { items: f(SlideItems { items: vec![]}).items }) 80 | } 81 | 82 | fn framed(self, f: fn(SlideItems) -> SlideItems) -> Self where Self: Sized { 83 | self.push(SlideItem::Framed { items: f(SlideItems { items: vec![]}).items }) 84 | } 85 | } 86 | 87 | impl Helper for SlideItems { 88 | fn push(mut self, item: SlideItem) -> Self where Self: Sized { 89 | self.items.push(item); 90 | self 91 | } 92 | } 93 | 94 | impl Helper for Slide { 95 | fn push(mut self, item: SlideItem) -> Slide { 96 | self.items.push(item); 97 | self 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/runner.rs: -------------------------------------------------------------------------------- 1 | use crate::bema::Bema; 2 | use image::io::Reader as ImageReader; 3 | 4 | use crossterm::Result; 5 | 6 | pub trait Runner { 7 | fn run(&self, bema: &Bema) -> Result<()>; 8 | } 9 | 10 | pub fn fit_image_bytes(bytes: &[u8], width: &Option, extension: &String) -> Vec { 11 | let mut img = ImageReader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::from_extension(extension.replace(".", "")).unwrap()).decode().unwrap(); 12 | img = width.map(|w| img.resize(w as u32, (w * 2) as u32, image::imageops::FilterType::Lanczos3)).unwrap_or(img); 13 | let mut bytes: Vec = Vec::new(); 14 | img.write_to(&mut bytes, image::ImageOutputFormat::Png).unwrap(); 15 | bytes 16 | } 17 | 18 | 19 | pub fn get_justify(size: usize, texts: Vec<&String>) -> Result { 20 | 21 | let mut whitespaces : usize = size as usize; 22 | 23 | for text in texts { 24 | let new_whitespaces = if text.len() < size as usize { 25 | let x = (size as usize - text.len()) / 2; 26 | x 27 | } else { 28 | 0 29 | }; 30 | if new_whitespaces == 0 { 31 | whitespaces = 0; 32 | break 33 | } else if new_whitespaces < whitespaces { 34 | whitespaces = new_whitespaces 35 | } 36 | } 37 | 38 | Ok(whitespaces) 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/terminal_runner.rs: -------------------------------------------------------------------------------- 1 | use crate::runner::{Runner, get_justify, fit_image_bytes}; 2 | 3 | use crate::bema::{Bema, SlideItem, Slide}; 4 | use tempfile::Builder; 5 | use std::io::{stdout, Write}; 6 | use std::process::Command; 7 | use std::env; 8 | use blockish::render_image; 9 | 10 | use syntect::easy::HighlightLines; 11 | use syntect::parsing::SyntaxSet; 12 | use syntect::highlighting::{ThemeSet, Style}; 13 | use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings}; 14 | 15 | use crossterm::{ 16 | execute, 17 | cursor::{MoveTo, MoveRight, Hide, Show}, 18 | event::{Event, KeyCode, KeyEvent}, 19 | style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor, Attribute, SetAttribute}, 20 | terminal::{EnterAlternateScreen, LeaveAlternateScreen, Clear, ClearType, enable_raw_mode, disable_raw_mode, self}, 21 | ExecutableCommand, Result, 22 | event, 23 | }; 24 | 25 | 26 | fn justify_center(size: usize, text: Vec<&String>) -> Result<()> { 27 | let whitespaces = get_justify(size, text)?; 28 | stdout() 29 | .execute(MoveRight(whitespaces as u16))?; 30 | Ok(()) 31 | } 32 | 33 | pub struct TerminalRunner { 34 | } 35 | 36 | 37 | fn display_image(image_path: &String) { 38 | match env::var("KITTY_WINDOW_ID") { 39 | Ok(_) => { 40 | let _res = Command::new("kitty") 41 | .arg("+kitten") 42 | .arg("icat") 43 | .arg(image_path) 44 | .output(); 45 | }, 46 | Err(_) => { 47 | render_image(image_path, 100 * 4); 48 | } 49 | } 50 | } 51 | 52 | 53 | impl TerminalRunner { 54 | fn clear_screen(&self) -> Result<()> { 55 | 56 | stdout() 57 | .execute(Clear(ClearType::All))?; 58 | 59 | Ok(()) 60 | } 61 | 62 | fn read_keycode(&self) -> Result { 63 | enable_raw_mode()?; 64 | loop { 65 | if let Event::Key(KeyEvent { 66 | code, 67 | .. 68 | }) = event::read()? 69 | { 70 | disable_raw_mode()?; 71 | return Ok(code); 72 | } 73 | } 74 | } 75 | 76 | fn render_item(&self, item: &SlideItem) -> Result<()> { 77 | match item { 78 | SlideItem::Image { image, extension, width } => { 79 | let bytes = fit_image_bytes(image, width, extension); 80 | let mut file = Builder::new() 81 | .prefix("image") 82 | .suffix(".png") 83 | .rand_bytes(5) 84 | .tempfile()?; 85 | file.write(&bytes[..])?; 86 | display_image(&file.path().to_str().unwrap().to_string()); 87 | }, 88 | SlideItem::Code { extension, source } => { 89 | let ps = SyntaxSet::load_defaults_newlines(); 90 | let ts = ThemeSet::load_defaults(); 91 | 92 | let syntax = ps.find_syntax_by_extension(extension).unwrap(); 93 | let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]); 94 | let splits = source.split("\n").map( |x| x.to_string()).collect::>(); 95 | let v2: Vec<&String> = splits.iter().map(|s| s).collect::>(); 96 | let whitespaces = get_justify(terminal::size()?.0 as usize, v2)? as usize; 97 | for line in LinesWithEndings::from(source) { 98 | let ranges: Vec<(Style, &str)> = h.highlight(line, &ps); 99 | let escaped = as_24_bit_terminal_escaped(&ranges[..], true); 100 | stdout() 101 | .execute(ResetColor)? 102 | .execute(MoveRight(whitespaces as u16))?; 103 | print!("{}", escaped); 104 | } 105 | 106 | stdout() 107 | .execute(ResetColor)?; 108 | }, 109 | SlideItem::Text { text } => { 110 | let splits = text.split("\n").map( |x| x.to_string()).collect::>(); 111 | let v2: Vec<&String> = splits.iter().map(|s| s).collect::>(); 112 | let whitespaces = get_justify(terminal::size()?.0 as usize, v2)?; 113 | for split in splits { 114 | stdout().execute(MoveRight(whitespaces as u16))?; 115 | println!("{}", split); 116 | } 117 | }, 118 | SlideItem::Cols { items } => { 119 | for item2 in items { 120 | self.render_item(item2)?; 121 | } 122 | }, 123 | SlideItem::Rows { items } => { 124 | for item2 in items { 125 | self.render_item(item2)?; 126 | } 127 | }, 128 | SlideItem::Framed { items } => { 129 | for item2 in items { 130 | self.render_item(item2)?; 131 | } 132 | }, 133 | } 134 | Ok(()) 135 | } 136 | 137 | fn render_slide(&self, slide: &Slide) -> Result<()> { 138 | 139 | justify_center(terminal::size()?.0 as usize, vec![&slide.title])?; 140 | 141 | stdout() 142 | .execute(SetAttribute(Attribute::Bold))? 143 | .execute(SetForegroundColor(Color::Blue))? 144 | .execute(SetBackgroundColor(Color::Black))? 145 | .execute(Print(slide.title.to_string()))? 146 | .execute(ResetColor)? 147 | .execute(Print("\n\n"))?; 148 | 149 | for item in &slide.items { 150 | self.render_item(item)?; 151 | } 152 | 153 | Ok(()) 154 | } 155 | 156 | } 157 | 158 | impl Runner for TerminalRunner { 159 | 160 | fn run(&self, bema: &Bema) -> Result<()> { 161 | execute!(stdout(), EnterAlternateScreen)?; 162 | self.clear_screen()?; 163 | 164 | execute!( 165 | stdout(), 166 | Hide 167 | )?; 168 | 169 | let mut i : i16 = -1; 170 | loop { 171 | if i >= 0 { 172 | let c = self.read_keycode()?; 173 | match c { 174 | KeyCode::Char('g') => i = 0, 175 | KeyCode::Char('G') => i = bema.slides.len() as i16 - 1, 176 | KeyCode::Char('n')|KeyCode::Char('j')|KeyCode::Char('l')|KeyCode::Right|KeyCode::Down => i+=1, 177 | KeyCode::Char('p')|KeyCode::Char('k')|KeyCode::Char('h')|KeyCode::Left|KeyCode::Up => i-=1, 178 | KeyCode::Char('q') => break, 179 | _ => {} 180 | } 181 | if i as usize >= bema.slides.len() { 182 | i = 0; 183 | } 184 | if i < 0 { 185 | i = bema.slides.len() as i16 - 1; 186 | } 187 | } else { 188 | i = 0; 189 | } 190 | execute!( 191 | stdout(), 192 | MoveTo(0, 0), 193 | )?; 194 | self.clear_screen()?; 195 | println!("{}/{}", i + 1, bema.slides.len()); 196 | self.render_slide(bema.slides.get(i as usize).unwrap())?; 197 | } 198 | 199 | execute!( 200 | stdout(), 201 | Show 202 | )?; 203 | 204 | execute!(stdout(), LeaveAlternateScreen)?; 205 | 206 | Ok(()) 207 | } 208 | } 209 | --------------------------------------------------------------------------------