diff --git a/Cargo.lock b/Cargo.lock index c1ff712..16e614c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,6 +332,18 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + [[package]] name = "gimli" version = "0.31.1" @@ -489,7 +501,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys", ] @@ -549,6 +561,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.97" @@ -567,6 +588,41 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + [[package]] name = "ratatui" version = "0.29.0" @@ -594,6 +650,7 @@ version = "0.1.0" dependencies = [ "crossterm 0.29.0", "futures", + "rand", "ratatui", "terminput", "terminput-crossterm", @@ -866,6 +923,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "winapi" version = "0.3.9" @@ -960,3 +1026,32 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 7da3dce..80ee91d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] crossterm = { version = "0.29.0", features = ["event-stream"] } futures = "0.3.31" +rand = "0.9.2" ratatui = "0.29.0" terminput = "0.5.3" terminput-crossterm = "0.3.3" diff --git a/src/app_matrix.rs b/src/app_matrix.rs new file mode 100644 index 0000000..49e9648 --- /dev/null +++ b/src/app_matrix.rs @@ -0,0 +1,234 @@ +use ratatui::{layout::Rect, style::{Style, Stylize}, text::{Line, Span}, widgets::{Block, Paragraph}}; +use terminput::KeyCode; +use rand::prelude::*; +use rand::rng; + +use crate::Demo; + +struct MatrixColumn { + x: u16, + y: u16, + length: u16, + speed: u16, + chars: Vec, +} + +impl MatrixColumn { + fn new(x: u16, _frame_counter: usize) -> Self { + let mut rng = rng(); + let length = rng.random_range(5..15); + let speed = rng.random_range(1..4); + let chars = (0..length) + .map(|_| Self::random_matrix_char(&mut rng)) + .collect(); + + Self { + x, + y: 0, + length, + speed, + chars, + } + } + + fn random_matrix_char(rng: &mut ThreadRng) -> char { + let matrix_chars = [ + // Japanese katakana + 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', + 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', + 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', + 'マ', 'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'リ', + 'ル', 'レ', 'ロ', 'ワ', 'ヲ', 'ン', + // Numbers + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + // Some ASCII symbols + ':', '・', '"', '=', '*', '+', '-', '<', '>', '¦', '|', 'Z', + ]; + *matrix_chars.choose(rng).unwrap() + } + + fn update(&mut self, height: u16, frame_counter: usize, speed_multiplier: usize) { + // Calculate effective speed with multiplier (higher multiplier = faster) + let effective_speed = if speed_multiplier == 0 { + self.speed as usize * 4 // Much slower when multiplier is 0 + } else { + (self.speed as usize * 2).saturating_sub(speed_multiplier.saturating_sub(1)).max(1) + }; + + if frame_counter % effective_speed == 0 { + self.y = self.y.wrapping_add(1); + + // Reset column when it goes off screen + if self.y > height + self.length { + self.y = 0; + let mut rng = rng(); + self.length = rng.random_range(5..15); + self.speed = rng.random_range(1..4); + self.chars = (0..self.length) + .map(|_| Self::random_matrix_char(&mut rng)) + .collect(); + } + + // Occasionally change characters (also affected by speed multiplier) + let char_change_rate = (10 + (speed_multiplier * 5).min(40)) as f64 / 100.0; + if rng().random_bool(char_change_rate) { + let mut rng = rng(); + for ch in &mut self.chars { + if rng.random_bool(0.3) { + *ch = Self::random_matrix_char(&mut rng); + } + } + } + } + } +} + +pub(crate) struct MyApp { + counter: usize, + frame_counter: usize, + columns: Vec, + initialized: bool, + density: usize, +} + +impl Demo for MyApp { + fn new() -> Self { + Self { + counter: 1, + frame_counter: 0, + columns: Vec::new(), + initialized: false, + density: 3, // Default density (lower = more dense) + } + } + + async fn frame(&mut self, term: &mut crate::Term) -> Result<(), String> { + term.draw(|fr| { + self.frame_counter = self.frame_counter.wrapping_add(1); + + let style = Style::new().green().on_black(); + let area = fr.area(); + + let block = Block::bordered() + .style(style) + .title("Matrix Rain") + .title_bottom("By: Kurocon (Kevin Alberts), kurocon@snt.utwente.nl"); + + let barea = block.inner(area); + + fr.render_widget(block, area); + + // Initialize columns on first frame or when density changes + if !self.initialized { + self.columns.clear(); + + // Create multiple layers based on density + // Lower density value = more layers = more dense + let layers = match self.density { + 1 => 12, // Very dense - 12 layers + 2 => 8, // Dense - 8 layers + 3 => 6, // Normal - 6 layers + 4 => 4, // Sparse - 4 layers + 5 => 3, // More sparse - 3 layers + _ => 2, // Very sparse - 2 layers + }; + + let base_spacing = (self.density * 2).max(2) as u16; + + for layer in 0..layers { + let layer_offset = (layer as u16 * base_spacing / layers as u16).max(1); + let spacing = base_spacing; + + let layer_columns: Vec = (barea.x..(barea.x + barea.width)) + .step_by(spacing as usize) + .map(|x| { + let offset_x = (x + layer_offset) % (barea.x + barea.width); + let mut col = MatrixColumn::new(offset_x, self.frame_counter + layer * 100); + // Give each layer slightly different starting positions + col.y = (layer as u16 * barea.height / layers as u16) % (barea.height + col.length); + col + }) + .collect(); + + self.columns.extend(layer_columns); + } + + self.initialized = true; + } + + // Update and draw columns + let buf = fr.buffer_mut(); + for column in &mut self.columns { + column.update(barea.height, self.frame_counter, self.counter); + + for (i, &ch) in column.chars.iter().enumerate() { + let char_y = column.y.saturating_sub(i as u16); + + if char_y >= barea.y && char_y < barea.y + barea.height { + let style = if i == 0 { + // Head of the stream - bright white + Style::new().white().bold() + } else if i < 3 { + // Near head - bright green + Style::new().light_green().bold() + } else if i < column.length as usize / 2 { + // Middle - regular green + Style::new().green() + } else { + // Tail - dark green + Style::new().dark_gray() + }; + + buf.set_span(column.x, char_y, &Span::from(ch.to_string()).style(style), 1); + } + } + } + + // Center text with instructions and counter + let mut lines: Vec = Vec::new(); + lines.push("W/S: Speed | E/D: Density | Enter: Exit".into()); + lines.push(format!("Speed multiplier: {}", self.counter).into()); + lines.push(format!("Density: {} ({})", self.density, if self.density <= 2 { "Dense" } else if self.density <= 4 { "Normal" } else { "Sparse" }).into()); + lines.push("".into()); + lines.push("\"Wake up, Neo...\"".light_green().into()); + + let text = Paragraph::new(lines).centered().style(Style::new().on_black()); + + let text_width = 45; + let text_height = 7; + let text_x = (barea.width.saturating_sub(text_width)) / 2 + barea.x; + let text_y = (barea.height.saturating_sub(text_height)) / 2 + barea.y; + + fr.render_widget(text, Rect::new(text_x, text_y, text_width, text_height)); + }).map_err(|e| e.to_string())?; + + Ok(()) + } + + async fn input(&mut self, event: terminput::Event) -> Result<(), String> { + if let terminput::Event::Key(kv) = event { + match kv.code { + KeyCode::Char('w') => { + self.counter = self.counter.wrapping_add(1); + }, + KeyCode::Char('s') => { + self.counter = self.counter.saturating_sub(1); + }, + KeyCode::Char('e') => { + self.density = self.density.saturating_sub(1).max(1); + self.initialized = false; // Force reinitialization + }, + KeyCode::Char('d') => { + self.density = (self.density + 1).min(10); + self.initialized = false; // Force reinitialization + }, + _ => {} + } + } + Ok(()) + } + + async fn disconnected(&mut self) { + () + } +} diff --git a/src/main.rs b/src/main.rs index 6de4e01..a4f8c16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,8 @@ use crossterm::event::Event; use ratatui::{prelude::CrosstermBackend, Terminal}; use futures::{future::FutureExt, StreamExt}; -mod app; -use app::MyApp; +mod app_matrix; +use app_matrix::MyApp; use terminput::KeyCode; use terminput_crossterm::to_terminput; use termion::raw::{IntoRawMode, RawTerminal};