confetty-tui 0.1.2

Confetti and fireworks in your terminal
Documentation
use anyhow::Result;
use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
    backend::CrosstermBackend,
    layout::Rect,
    style::{Color, Style},
    text::{Line, Span},
    widgets::Paragraph,
    Frame, Terminal,
};
use std::{
    env,
    io,
    time::{Duration, Instant},
};
use tokio::time;

mod array;
mod confetti;
mod fireworks;
mod simulation;

use simulation::System;

enum Mode {
    Confetti,
    Fireworks,
}

struct App {
    system: System,
    mode: Mode,
    last_frame: Instant,
}

impl App {
    fn new(mode: Mode) -> Self {
        Self {
            system: System::new(),
            mode,
            last_frame: Instant::now(),
        }
    }

    fn update(&mut self, terminal_size: Rect) {
        // Update frame size if needed
        if self.system.frame.width != terminal_size.width as i32 
            || self.system.frame.height != terminal_size.height as i32 {
            self.system.frame.width = terminal_size.width as i32;
            self.system.frame.height = terminal_size.height as i32;
            
            // Spawn initial particles if frame was empty
            if self.system.particles.is_empty() {
                match self.mode {
                    Mode::Confetti => {
                        let new_particles = confetti::spawn(self.system.frame.width, self.system.frame.height);
                        self.system.particles.extend(new_particles);
                    }
                    Mode::Fireworks => {
                        let new_particle = fireworks::spawn_shoot(self.system.frame.width, self.system.frame.height);
                        self.system.particles.push(new_particle);
                    }
                }
            }
        }

        // Update physics at 30 FPS
        let now = Instant::now();
        if now.duration_since(self.last_frame) >= Duration::from_millis(33) { // ~30 FPS
            self.system.update();
            self.last_frame = now;
        }
    }

    fn spawn_new_particles(&mut self) {
        match self.mode {
            Mode::Confetti => {
                let new_particles = confetti::spawn(self.system.frame.width, self.system.frame.height);
                self.system.particles.extend(new_particles);
            }
            Mode::Fireworks => {
                let new_particle = fireworks::spawn_shoot(self.system.frame.width, self.system.frame.height);
                self.system.particles.push(new_particle);
            }
        }
    }

    fn render(&self, frame: &mut Frame) {
        let plane = self.system.render();
        let mut lines = Vec::new();
        
        for row in plane {
            let mut spans = Vec::new();
            for (x, cell) in row.iter().enumerate() {
                if cell.is_empty() {
                    spans.push(Span::raw(" "));
                } else {
                    // Find the particle at this position to get its color
                    let mut color = Color::White;
                    for particle in &self.system.particles {
                        if self.system.visible(particle) {
                            let pos = particle.physics.position();
                            if pos.x as usize == x && pos.y as usize == lines.len() {
                                color = particle.color;
                                break;
                            }
                        }
                    }
                    spans.push(Span::styled(cell.clone(), Style::default().fg(color)));
                }
            }
            lines.push(Line::from(spans));
        }
        
        let paragraph = Paragraph::new(lines);
        frame.render_widget(paragraph, frame.area());
    }
}

async fn run_app() -> Result<()> {
    let args: Vec<String> = env::args().collect();
    let mode = if args.len() > 1 && args[1] == "fireworks" {
        Mode::Fireworks
    } else {
        Mode::Confetti
    };

    // Setup terminal
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    // Create app
    let mut app = App::new(mode);
    let mut last_tick = Instant::now();

    loop {
        // Handle events
        if event::poll(Duration::from_millis(16))? {
            if let Event::Key(key) = event::read()? {
                match key.code {
                    KeyCode::Char('q') | KeyCode::Char('Q') => break,
                    KeyCode::Esc => break,
                    _ => app.spawn_new_particles(),
                }
            }
        }

        // Update and render
        let now = Instant::now();
        if now.duration_since(last_tick) >= Duration::from_millis(33) {
            terminal.draw(|f| {
                app.update(f.area());
                app.render(f);
            })?;
            last_tick = now;
        }

        // Small sleep to prevent excessive CPU usage
        time::sleep(Duration::from_millis(1)).await;
    }

    // Restore terminal
    disable_raw_mode()?;
    execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture
    )?;

    Ok(())
}

#[tokio::main]
async fn main() -> Result<()> {
    run_app().await
}