diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-08-25 12:33:03 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-08-25 12:33:03 +0200 |
| commit | 8ff1162897acc9a393f41edf9faf5593d6c66995 (patch) | |
| tree | b93e52d0b0c36a84b29fe0e5dde3d7401ec130eb /src | |
| parent | 3ec8e14833b55d6bff4cf5702f90e1bb9a1b1e40 (diff) | |
Replace JS with rust
Diffstat (limited to 'src')
| -rw-r--r-- | src/configuration.rs | 63 | ||||
| -rw-r--r-- | src/main.rs | 41 | ||||
| -rw-r--r-- | src/renderer.rs | 41 | ||||
| -rw-r--r-- | src/screen.rs | 242 | ||||
| -rw-r--r-- | src/telnet.rs | 72 |
5 files changed, 459 insertions, 0 deletions
diff --git a/src/configuration.rs b/src/configuration.rs new file mode 100644 index 0000000..5995ffc --- /dev/null +++ b/src/configuration.rs @@ -0,0 +1,63 @@ +use lexopt::{Parser, prelude::*}; + +const DEFAULT_ADDRESS: &str = "127.0.0.1:6666"; +const DEFAULT_FREQUENCY: u64 = 150; +const DEFAULT_MODULATION: u8 = 5; + +#[derive(Clone)] +pub struct Configuration { + pub address: String, + pub frequency: u64, + pub modulation: u8, +} + +impl Configuration { + pub fn new() -> Self { + let mut address = DEFAULT_ADDRESS.to_string(); + let mut frequency = DEFAULT_FREQUENCY; + let mut modulation = DEFAULT_MODULATION; + + let mut parser = Parser::from_env(); + + while let Ok(Some(argument)) = parser.next() { + match argument { + Short('l') | Long("listen-address") => { + if let Ok(value) = parser.value().and_then(|v| v.parse()) { + address = value; + } else { + eprintln!("Warning: Invalid listen address ignored."); + } + } + Short('f') | Long("frequency") => { + if let Ok(value) = parser.value().and_then(|v| v.parse()) { + frequency = value; + } else { + eprintln!("Warning: Invalid frequency ignored."); + } + } + Short('m') | Long("modulation") => { + if let Ok(value) = parser.value().and_then(|v| v.parse()) { + modulation = value; + } else { + eprintln!("Warning: Invalid modulation ignored."); + } + } + Long("help") => { + println!( + "Usage: tomato-sauce [-l|--listen-address=LISTEN_ADDRESS] [-f|--frequency=NUMBER] [-m|--modulation=NUMBER]" + ); + std::process::exit(0); + } + _ => { + eprintln!("Warning: Unknown argument ignored"); + } + } + } + + Configuration { + address, + frequency, + modulation, + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d6908cb --- /dev/null +++ b/src/main.rs @@ -0,0 +1,41 @@ +mod configuration; +mod renderer; +mod screen; +mod telnet; + +use configuration::Configuration; +use telnet::handle_client; + +use std::io::Result; +use std::net::TcpListener; +use std::thread; + +/// Spawns a server and hands over the connection to the telnet client handler. +fn main() -> Result<()> { + let configuration = Configuration::new(); + + let listener = TcpListener::bind(&configuration.address)?; + eprintln!( + "Server is now listening on address {}", + configuration.address + ); + + for stream in listener.incoming() { + match stream { + Ok(stream) => { + thread::spawn(move || { + if let Err(error) = + handle_client(stream, configuration.frequency, configuration.modulation) + { + eprintln!("Error handling client: {error}"); + } + }); + } + Err(error) => { + eprintln!("Connection failed: {error}"); + } + } + } + + Ok(()) +} diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..b0e702d --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,41 @@ +use rand::{Rng, rng}; + +#[derive(Clone)] +pub enum Renderer { + Ansi, + Color256, + TrueColor, + FakeColor, +} + +impl Renderer { + pub fn random() -> Self { + let mut rng = rng(); + let renderers = [Self::Ansi, Self::Color256, Self::TrueColor, Self::FakeColor]; + let index = rng.random_range(0..renderers.len()); + renderers.get(index).unwrap_or(&Self::Ansi).clone() + } + pub fn render(&self, red: u8, green: u8, blue: u8) -> String { + match self { + Renderer::Ansi => { + let color_offset = + ((u16::from(red) + u16::from(green) + u16::from(blue)) * 7) / (255 * 3); + let color_number = 40 + color_offset; + format!("\x1B[{color_number}m") + } + Renderer::Color256 => { + let red_value = (u16::from(red) * 5) / 255; + let green_value = (u16::from(green) * 5) / 255; + let blue_value = (u16::from(blue) * 5) / 255; + let color_number = 16 + 36 * red_value + 6 * green_value + blue_value; + format!("\x1B[48;5;{color_number}m") + } + Renderer::TrueColor => { + format!("\x1B[48;2;{red};{green};{blue}m") + } + Renderer::FakeColor => { + format!("\x1B[28;2;{red};{green};{blue}m") + } + } + } +} diff --git a/src/screen.rs b/src/screen.rs new file mode 100644 index 0000000..2c701ea --- /dev/null +++ b/src/screen.rs @@ -0,0 +1,242 @@ +use crate::renderer::Renderer; +use rand::{Rng, rng}; +use std::fmt::Write; + +#[derive(Clone)] +pub enum Screen { + Circle, + Checkerboard, + Gradients, + Mirrors, + Random, + Sprinkles, +} + +impl Screen { + pub fn random() -> Self { + let mut rng = rng(); + let screens = [ + Self::Circle, + Self::Checkerboard, + Self::Gradients, + Self::Mirrors, + Self::Random, + Self::Sprinkles, + ]; + let index = rng.random_range(0..screens.len()); + screens.get(index).unwrap_or(&Self::Random).clone() + } + + pub fn render(&self, modulation: u8, width: u16, height: u16, renderer: &Renderer) -> String { + match self { + Screen::Circle => render_circle(modulation, width, height, renderer), + Screen::Checkerboard => render_checkerboard(modulation, width, height, renderer), + Screen::Gradients => render_gradients(modulation, width, height, renderer), + Screen::Mirrors => render_mirrors(modulation, width, height, renderer), + Screen::Random => render_random(modulation, width, height, renderer), + Screen::Sprinkles => render_sprinkles(modulation, width, height, renderer), + } + } +} + +fn render_circle(_modulation: u8, width: u16, height: u16, renderer: &Renderer) -> String { + let mut response = String::new(); + let mut rng = rng(); + + let circles = if width > height { height } else { width }; + + for i in 0..circles { + let center_x = (width / 2) + 1; + let center_y = (height / 2) + 1; + + let red = rng.random_range(0..=255); + let blue = rng.random_range(0..=255); + let green = rng.random_range(0..=255); + + for j in 0..180 { + let angle = 2.0 * f64::from(j) * (std::f64::consts::PI / 180.0); + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let x = (f64::from(center_x) + angle.sin() * f64::from(i)).round() as u16; + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let y = (f64::from(center_y) + angle.cos() * f64::from(i)).round() as u16; + + if x <= width && x > 0 && y <= height && y > 0 { + let position = format!("\x1B[{y};{x}H"); // Move cursor to y,x (CSI: y;x H) + let _ = write!( + response, + "{}{} ", + position, + renderer.render(red, green, blue) + ); + } + } + } + + response +} + +fn render_checkerboard(modulation: u8, width: u16, height: u16, renderer: &Renderer) -> String { + let mut response = String::new(); + + // Calculate offset based on modulation + // Full cycle every 25 steps (5x5 grid) + let cycle_pos = modulation % 25; + let offset_x = u16::from(cycle_pos / 5); + let offset_y = u16::from(cycle_pos / 5); + + // Use modulation / 25 as seed to get consistent colors during each cycle + let color_seed = u64::from(modulation / 25); + + // Generate two colors based on the current cycle + let red1 = u8::try_from((color_seed * 17 + 42) % 256).unwrap_or(0); + let green1 = u8::try_from((color_seed * 31 + 73) % 256).unwrap_or(0); + let blue1 = u8::try_from((color_seed * 47 + 91) % 256).unwrap_or(0); + + let red2 = u8::try_from((color_seed * 23 + 137) % 256).unwrap_or(0); + let green2 = u8::try_from((color_seed * 41 + 163) % 256).unwrap_or(0); + let blue2 = u8::try_from((color_seed * 37 + 197) % 256).unwrap_or(0); + + for i in 0..height { + for j in 0..width { + // Calculate which checker square we're in, accounting for offset + let checker_x = (j + offset_x) / 5; + let checker_y = (i + offset_y) / 5; + + // Checkerboard pattern: alternate colors + let is_first_color = (checker_x + checker_y) % 2 == 0; + + if is_first_color { + response.push_str(&renderer.render(red1, green1, blue1)); + } else { + response.push_str(&renderer.render(red2, green2, blue2)); + } + response.push(' '); + } + + if i < height - 1 { + response.push('\n'); + } + } + + response +} + +fn render_gradients(modulation: u8, width: u16, height: u16, renderer: &Renderer) -> String { + let mut response = String::new(); + + for i in 0..height { + for j in 0..width { + let red = + ((u32::from(modulation) + u32::from(i)) * 255 / u32::from(height).max(1)) % 255; + let blue = + ((u32::from(modulation) + u32::from(j)) * 255 / u32::from(width).max(1)) % 255; + let green = ((u32::from(modulation) + u32::from(i) * u32::from(j)) * 255 + / (u32::from(width) * u32::from(height)).max(1)) + % 255; + + response.push_str(&renderer.render(red as u8, green as u8, blue as u8)); + response.push(' '); + } + + if i < height - 1 { + response.push('\n'); + } + } + + response +} + +fn render_mirrors(modulation: u8, width: u16, height: u16, renderer: &Renderer) -> String { + let mut rng = rng(); + let scale = 2 + rng.random_range(0..5); + let scaled_height = height.saturating_div(scale).max(1); + let scaled_width = width.saturating_div(scale).max(1); + + let mut response = vec![String::new(); height as usize]; + + for i in 0..scaled_height { + let mut row = Vec::new(); + for j in 0..scaled_width { + let red = + ((u32::from(modulation) + u32::from(i)) * 255 / u32::from(height).max(1)) % 255; + let blue = + ((u32::from(modulation) + u32::from(j)) * 255 / u32::from(width).max(1)) % 255; + let green = ((u32::from(modulation) + u32::from(i) * u32::from(j)) * 255 + / (u32::from(width) * u32::from(height)).max(1)) + % 255; + + let cell = format!("{} ", renderer.render(red as u8, green as u8, blue as u8)); + row.push(cell); + } + + let mut row_text = String::new(); + for _ in 0..scale { + row.reverse(); + row_text.push_str(&row.join("")); + } + + for j in 1..scale { + let top_idx = (j.saturating_mul(i)) as usize; + let bottom_idx = height.saturating_sub(1).saturating_sub(j.saturating_mul(i)) as usize; + if let Some(line) = response.get_mut(top_idx) { + line.clone_from(&row_text); + } + if let Some(line) = response.get_mut(bottom_idx) { + line.clone_from(&row_text); + } + } + } + + response.join("\n") +} + +fn render_random(_modulation: u8, width: u16, height: u16, renderer: &Renderer) -> String { + let mut response = String::new(); + let mut rng = rng(); + + for i in 0..height { + for _j in 0..width { + let red = rng.random_range(0..=255); + let blue = rng.random_range(0..=255); + let green = rng.random_range(0..=255); + + response.push_str(&renderer.render(red, green, blue)); + response.push(' '); + } + + if i < height - 1 { + response.push('\n'); + } + } + + response +} + +fn render_sprinkles(_modulation: u8, width: u16, height: u16, renderer: &Renderer) -> String { + let mut response = String::new(); + let mut rng = rng(); + + let max_sprinkle_count = (width * height) / 2; + let min_sprinkle_count = (width * height) / 8; + let sprinkle_count = rng.random_range(min_sprinkle_count..=max_sprinkle_count); + + let red = rng.random_range(0..=255); + let blue = rng.random_range(0..=255); + let green = rng.random_range(0..=255); + + for _ in 0..sprinkle_count { + let x = rng.random_range(1..=width); + let y = rng.random_range(1..=height); + + let position = format!("\x1B[{y};{x}H"); // Move cursor to y,x + let _ = write!( + response, + "{}{} ", + position, + renderer.render(red, green, blue) + ); + } + + response +} diff --git a/src/telnet.rs b/src/telnet.rs new file mode 100644 index 0000000..c18fe53 --- /dev/null +++ b/src/telnet.rs @@ -0,0 +1,72 @@ +use std::io::{ErrorKind, Read, Result, Write}; +use std::net::TcpStream; +use std::thread; +use std::time::Duration; + +use crate::renderer::Renderer; +use crate::screen::Screen; + +/// Parses a 16bit buffer by parsing both u8 +fn parse_16bit_buffer(buffer: &[u8]) -> u16 { + let high = buffer.first().unwrap_or(&0); + let low = buffer.get(1).unwrap_or(&0); + (u16::from(*high) << 8) | u16::from(*low) +} + +// Telnet protocol constants +const NAWS_REQUEST: &[u8] = &[0xFF, 0xFD, 0x1F]; // IAC DO NAWS +const NAWS_RESPONSE_PREFIX: &[u8] = &[0xFF, 0xFB, 0x1F, 0xFF, 0xFA, 0x1F]; // IAC WILL NAWS IAC SB NAWS +const ESCAPE_SEQUENCE: &[u8] = &[0xFF, 0xF4, 0xFF, 0xFD, 0x06]; // IAC IP IAC DO TIMING_MARK + +/// Selects a random screen and renderer, waits for the NAWS handshake, and +/// starts the animation. +pub fn handle_client(mut stream: TcpStream, frequency: u64, modulation: u8) -> Result<()> { + let mut buffer = [0; 1024]; + + let screen = Screen::random(); + let renderer = Renderer::random(); + + let mut width: Option<u16> = None; + let mut height: Option<u16> = None; + let mut current_modulation: u8 = 0; + + // Send NAWS request to get terminal size + stream.write_all(NAWS_REQUEST)?; + stream.flush()?; + + loop { + stream.set_read_timeout(Some(Duration::from_millis(10)))?; + match stream.read(&mut buffer) { + Ok(0) => break, // Disconnect + Ok(n) => { + if n >= 10 && buffer[0..6] == *NAWS_RESPONSE_PREFIX { + width = Some(parse_16bit_buffer(&buffer[6..8])); + height = Some(parse_16bit_buffer(&buffer[8..10])); + + // Clear screen once we have dimensions + stream.write_all(b"\x1B[2J")?; // Clear screen + stream.flush()?; + } + + // Check for telnet escape sequence (Ctrl+C) + if n >= 5 && buffer[0..5] == *ESCAPE_SEQUENCE { + break; + } + } + Err(ref e) if e.kind() == ErrorKind::WouldBlock => {} + Err(_) => break, + } + + if let (Some(w), Some(h)) = (width, height) { + let payload = screen.render(current_modulation, w, h, &renderer); + let message = format!("\x1B[1;1H{payload}"); // Reset cursor + payload + stream.write_all(message.as_bytes())?; + stream.flush()?; + + current_modulation = current_modulation.wrapping_add(modulation); + } + + thread::sleep(Duration::from_millis(frequency)); + } + Ok(()) +} |