bunflared 0.1.2

Share localhost on a public link, with an absurdly fun terminal show and an agent mode
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};

use super::Theme;

pub const PINK: Color = Color::Rgb(255, 121, 198);
pub const CARROT: Color = Color::Rgb(255, 170, 80);
pub const GREEN: Color = Color::Rgb(80, 250, 123);
pub const CYAN: Color = Color::Rgb(139, 233, 253);
pub const YELLOW: Color = Color::Rgb(241, 250, 140);
pub const RED: Color = Color::Rgb(255, 85, 85);
pub const PURPLE: Color = Color::Rgb(189, 147, 249);
pub const DIM: Color = Color::Rgb(110, 118, 160);
pub const FG: Color = Color::Rgb(240, 240, 235);
pub const GOLD: Color = Color::Rgb(255, 215, 90);
pub const DIRT: Color = Color::Rgb(120, 78, 42);
pub const GRASS: Color = Color::Rgb(90, 200, 100);
pub const LEAF: Color = Color::Rgb(60, 190, 80);

/// xorshift: plenty for confetti, no dependency.
pub struct Rng(u64);

impl Rng {
    pub fn seeded() -> Self {
        let nanos = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map_or(1, |d| d.as_nanos() as u64);
        Self(nanos | 1)
    }

    pub fn next(&mut self) -> u64 {
        self.0 ^= self.0 << 13;
        self.0 ^= self.0 >> 7;
        self.0 ^= self.0 << 17;
        self.0
    }

    pub fn range(&mut self, low: f32, high: f32) -> f32 {
        low + (self.next() % 10_000) as f32 / 10_000.0 * (high - low)
    }

    pub fn below(&mut self, n: usize) -> usize {
        (self.next() % n.max(1) as u64) as usize
    }

    pub fn pick(&mut self, text: &str) -> char {
        let chars: Vec<char> = text.chars().collect();
        chars[self.below(chars.len())]
    }
}

pub fn hsv(hue: f32, saturation: f32, value: f32) -> Color {
    let h = hue.rem_euclid(360.0) / 60.0;
    let c = value * saturation;
    let x = c * (1.0 - (h % 2.0 - 1.0).abs());
    let (r, g, b) = match h as u32 {
        0 => (c, x, 0.0),
        1 => (x, c, 0.0),
        2 => (0.0, c, x),
        3 => (0.0, x, c),
        4 => (x, 0.0, c),
        _ => (c, 0.0, x),
    };
    let m = value - c;
    let byte = |v: f32| ((v + m) * 255.0) as u8;
    Color::Rgb(byte(r), byte(g), byte(b))
}

/// The palette is drawn for dark backgrounds; this deepens a color so it
/// reads on a light one. Near-white text turns near-black.
pub fn for_light(color: Color) -> Color {
    let Color::Rgb(r, g, b) = color else {
        return color;
    };
    let (r, g, b) = (r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0);
    let max = r.max(g).max(b);
    let delta = max - r.min(g).min(b);
    let saturation = if max == 0.0 { 0.0 } else { delta / max };
    if saturation < 0.15 {
        let gray = ((1.0 - max) * 255.0) as u8;
        return Color::Rgb(gray, gray, gray);
    }
    let hue = if max == r {
        60.0 * ((g - b) / delta).rem_euclid(6.0)
    } else if max == g {
        60.0 * ((b - r) / delta + 2.0)
    } else {
        60.0 * ((r - g) / delta + 4.0)
    };
    // Yellow stays pale at any brightness on white: pull it toward amber.
    let (hue, value) = if (35.0..80.0).contains(&hue) {
        (hue.min(48.0) - 6.0, 0.48)
    } else {
        (hue, max * 0.6)
    };
    hsv(hue, (saturation * 1.3 + 0.1).min(1.0), value)
}

pub fn rainbow(hue: f32) -> Color {
    hsv(hue, 0.65, 1.0)
}

/// Writes `text` at (x, y), clipped to the buffer; never panics off-screen.
pub fn put(buf: &mut Buffer, x: i32, y: i32, text: &str, style: Style) {
    let area = *buf.area();
    if y < area.top() as i32 || y >= area.bottom() as i32 {
        return;
    }
    for (i, ch) in text.chars().enumerate() {
        let cx = x + i as i32;
        if cx < area.left() as i32 || cx >= area.right() as i32 {
            continue;
        }
        if let Some(cell) = buf.cell_mut((cx as u16, y as u16)) {
            cell.set_char(ch).set_style(style);
        }
    }
}

/// Draws multi-line art, spaces transparent.
pub fn art(buf: &mut Buffer, x: i32, y: i32, lines: &[&str], style: Style) {
    for (row, line) in lines.iter().enumerate() {
        for (col, ch) in line.chars().enumerate() {
            if ch != ' ' {
                put(buf, x + col as i32, y + row as i32, &ch.to_string(), style);
            }
        }
    }
}

pub fn center(area: Rect, w: i32) -> i32 {
    area.x as i32 + (area.width as i32 - w) / 2
}

pub struct Particle {
    pub x: f32,
    pub y: f32,
    pub vx: f32,
    pub vy: f32,
    pub gravity: f32,
    pub life: f32,
    pub ch: char,
    pub color: Color,
    pub leaf: bool,
}

#[derive(Default)]
pub struct Particles(pub Vec<Particle>);

impl Particles {
    pub fn step(&mut self, dt: f32) {
        for p in &mut self.0 {
            p.vy += p.gravity * dt;
            p.x += p.vx * dt;
            p.y += p.vy * dt;
            p.life -= dt;
        }
        self.0.retain(|p| p.life > 0.0 && p.y < 500.0);
    }

    pub fn draw(&self, buf: &mut Buffer, theme: &Theme, avoid: Option<Rect>) {
        for p in &self.0 {
            let (x, y) = (p.x.round() as i32, p.y.round() as i32);
            let inside = |rect: Rect| {
                x >= rect.left() as i32
                    && x < rect.right() as i32
                    && y >= rect.top() as i32 - 1
                    && y < rect.bottom() as i32
            };
            if avoid.is_some_and(inside) {
                continue;
            }
            let mut style = theme.fg(p.color);
            if p.life < 0.3 {
                style = style.add_modifier(Modifier::DIM);
            }
            put(buf, x, y, &p.ch.to_string(), style);
            if p.leaf {
                put(buf, x, y - 1, "ψ", theme.fg(LEAF));
            }
        }
    }

    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    pub fn fireworks(&mut self, rng: &mut Rng, x: f32, y: f32) {
        let hue = rng.range(0.0, 360.0);
        for _ in 0..48 {
            let angle = rng.range(0.0, std::f32::consts::TAU);
            let speed = rng.range(3.0, 11.0);
            self.0.push(Particle {
                x,
                y,
                vx: angle.cos() * speed * 2.0,
                vy: angle.sin() * speed,
                gravity: 7.0,
                life: rng.range(0.7, 1.6),
                ch: rng.pick("*+·•o"),
                color: hsv(hue + rng.range(-35.0, 35.0), 0.7, 1.0),
                leaf: false,
            });
        }
    }

    pub fn confetti(&mut self, rng: &mut Rng, x: f32, y: f32, count: usize) {
        for _ in 0..count {
            self.0.push(Particle {
                x,
                y,
                vx: rng.range(-28.0, 28.0),
                vy: rng.range(-20.0, -5.0),
                gravity: 15.0,
                life: rng.range(1.4, 2.6),
                ch: rng.pick("▪*+~o•▫"),
                color: rainbow(rng.range(0.0, 360.0)),
                leaf: false,
            });
        }
    }

    pub fn dirt(&mut self, rng: &mut Rng, x: f32, y: f32) {
        for _ in 0..3 {
            self.0.push(Particle {
                x,
                y,
                vx: rng.range(-16.0, -4.0),
                vy: rng.range(-9.0, -2.0),
                gravity: 20.0,
                life: rng.range(0.4, 0.9),
                ch: rng.pick(".,'`:;"),
                color: hsv(28.0, 0.65, rng.range(0.45, 0.8)),
                leaf: false,
            });
        }
    }

    pub fn flare(&mut self, rng: &mut Rng, x: f32, y: f32) {
        self.0.push(Particle {
            x,
            y,
            vx: rng.range(-2.0, 2.0),
            vy: rng.range(-8.0, -3.0),
            gravity: -1.0,
            life: rng.range(0.3, 0.8),
            ch: rng.pick("'.`^*"),
            color: hsv(rng.range(5.0, 50.0), 0.85, 1.0),
            leaf: false,
        });
    }

    pub fn snore(&mut self, rng: &mut Rng, x: f32, y: f32) {
        self.0.push(Particle {
            x,
            y,
            vx: 1.6,
            vy: -1.1,
            gravity: 0.0,
            life: 2.2,
            ch: if rng.below(2) == 0 { 'z' } else { 'Z' },
            color: DIM,
            leaf: false,
        });
    }

    pub fn carrot(&mut self, rng: &mut Rng, area: Rect) {
        self.0.push(Particle {
            x: rng.range(area.left() as f32, area.right() as f32),
            y: area.top() as f32,
            vx: 0.0,
            vy: rng.range(7.0, 14.0),
            gravity: 0.0,
            life: 6.0,
            ch: '▼',
            color: CARROT,
            leaf: true,
        });
    }
}