tomatrix 0.1.1

Make any text document output a matrix screensaver
Documentation
use anyhow::Result;
use crossterm::style::Color::{self, DarkGreen, DarkGrey, Green, White};
use crossterm::{
    cursor::MoveTo,
    style::{Print, SetForegroundColor},
    ExecutableCommand,
};
use std::io::Read;

pub const MAX_SPEED: usize = 4;

lazy_static::lazy_static! {
    pub static ref COLORS: [Color; 4] = [DarkGreen, DarkGrey, Green, White];
}

#[derive(Debug, Clone)]
pub struct Data {
    height: usize,
    position: Position,
    symbol: char,
    speed: usize,
    color: Color,
    volatility: f32,
}

fn pick_speed() -> usize {
    rand::random::<usize>() % MAX_SPEED
}

fn pick_color() -> Color {
    COLORS[rand::random::<usize>() % COLORS.len()]
}

fn pick_volatility() -> f32 {
    rand::random::<f32>() % 1.0
}

fn pick_symbol(corpus: &Corpus) -> char {
    let c = corpus[rand::random::<usize>() % corpus.len()];
    if rand::random::<u8>() % 10 < 3 {
        return c;
    }

    ' '
}

fn get_corpus() -> Result<Corpus> {
    let mut v = Vec::new();
    std::io::stdin().read_to_end(&mut v)?;

    Ok(v.iter().map(|x| (*x).into()).collect::<Corpus>())
}

impl Data {
    pub fn new(window: &Window) -> Self {
        Self {
            height: window.height,
            position: Position(rand::random::<usize>() % window.width, 0),
            symbol: pick_symbol(&window.corpus),
            speed: pick_speed(),
            color: pick_color(),
            volatility: pick_volatility(),
        }
    }

    pub fn iterate(&mut self) -> bool {
        if (rand::random::<f32>() % 1.0) > self.volatility {
            if rand::random::<bool>() {
                self.position.1 += self.speed;
                if self.position.1 >= self.height - (rand::random::<usize>() % (self.height / 8)) {
                    return false;
                }
            }
            self.speed = pick_speed();
            self.color = pick_color();
            self.volatility = pick_volatility();
        }

        return true;
    }
}

pub type Corpus = Vec<char>;

#[derive(Debug, Clone)]
pub struct Position(usize, usize);

#[derive(Debug, Clone)]
pub struct Window {
    data: Vec<Data>,
    corpus: Corpus,
    height: usize,
    width: usize,
    last_data: std::time::Instant,
}

impl Default for Window {
    fn default() -> Self {
        Window::from_terminal().expect("Could not get terminal settings")
    }
}

impl Window {
    pub fn new(width: usize, height: usize) -> Result<Self> {
        Ok(Self {
            data: Vec::new(),
            corpus: get_corpus()?,
            height,
            width,
            last_data: std::time::Instant::now(),
        })
    }

    pub fn from_terminal() -> Result<Self> {
        let ws = crossterm::terminal::size()?;
        Self::new((ws.0 - 1).into(), (ws.1 - 1).into())
    }

    pub fn draw_next(&mut self) -> Result<()> {
        let mut newdata = Vec::new();

        for item in &mut self.data {
            std::io::stdout()
                .execute(MoveTo(
                    item.position.0.try_into()?,
                    item.position.1.try_into()?,
                ))?
                .execute(SetForegroundColor(item.color))?
                .execute(Print(&format!("{}", item.symbol)))?;

            if rand::random() {
                if item.iterate() {
                    newdata.push(item.clone())
                }
            } else {
                newdata.push(item.clone())
            }
        }

        self.data = newdata;

        if std::time::Instant::now() - self.last_data > std::time::Duration::new(0, 2500000) {
            self.next_data(rand::random::<usize>() % 5);
            self.last_data = std::time::Instant::now();
        }

        Ok(())
    }

    fn next_data(&mut self, count: usize) {
        for _ in 0..count {
            let data = Data::new(self);
            self.data.push(data.clone());
        }
    }
}