summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2025-08-25 12:36:37 +0200
committerRuben Beltran del Rio <git@r.bdr.sh>2025-08-25 12:36:37 +0200
commite5de70f2c1dcac767bd34a6b90ac1797a7acb433 (patch)
tree25a8c8f339fb5b496b460391ee06a9effeb64855 /src
parent6b909d95ec07848136a6f337db28318c1cd46c60 (diff)
parentd7bc21d19e168f3a99f54e5ba4866ed49f1187f1 (diff)
Merge branch 'rust'
Diffstat (limited to 'src')
-rw-r--r--src/configuration.rs63
-rw-r--r--src/main.rs41
-rw-r--r--src/renderer.rs41
-rw-r--r--src/screen.rs242
-rw-r--r--src/telnet.rs72
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(())
+}