zik 0.1.0

A TUI web radio player with audio spectrum visualizer
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};

use crate::audio::BinMapper;

const BAR_WIDTH: u16 = 2;
const BAR_GAP: u16 = 1;

const BAR_BLOCKS: [&str; 8] = [
    " ", "\u{2581}", "\u{2582}", "\u{2583}", "\u{2584}", "\u{2585}", "\u{2586}", "\u{2587}",
];
const FULL_BLOCK: &str = "\u{2588}";

pub fn parse_color(name: &str) -> Color {
    match name.to_lowercase().as_str() {
        "black" => Color::Black,
        "red" => Color::Red,
        "green" => Color::Green,
        "yellow" => Color::Yellow,
        "blue" => Color::Blue,
        "magenta" => Color::Magenta,
        "cyan" => Color::Cyan,
        "white" => Color::White,
        "darkgray" | "dark_gray" => Color::DarkGray,
        "lightred" | "light_red" => Color::LightRed,
        "lightgreen" | "light_green" => Color::LightGreen,
        "lightyellow" | "light_yellow" => Color::LightYellow,
        "lightblue" | "light_blue" => Color::LightBlue,
        "lightmagenta" | "light_magenta" => Color::LightMagenta,
        "lightcyan" | "light_cyan" => Color::LightCyan,
        _ => Color::Magenta,
    }
}

fn calc_num_bars(inner_w: u16) -> usize {
    let bar_step = BAR_WIDTH + BAR_GAP;
    if inner_w >= BAR_WIDTH {
        ((inner_w + BAR_GAP) / bar_step) as usize
    } else {
        1
    }
}

pub struct Visualizer {
    pub bin_mapper: BinMapper,
    pub bar_values: Vec<f32>,
    pub bar_color: Color,
}

impl Visualizer {
    pub fn new(bar_color_name: &str) -> Self {
        Self {
            bin_mapper: BinMapper::new(1),
            bar_values: vec![0.0f32; 1],
            bar_color: parse_color(bar_color_name),
        }
    }

    fn ensure_bars(&mut self, num_bars: usize) {
        if self.bin_mapper.num_bars() != num_bars {
            self.bin_mapper = BinMapper::new(num_bars);
            self.bar_values.resize(num_bars, 0.0);
        }
    }

    pub fn render(&mut self, buf: &mut Buffer, area: Rect, spectrum: &[f32]) {
        let num_bars = calc_num_bars(area.width);
        self.ensure_bars(num_bars);
        self.bin_mapper.bin_into(spectrum, &mut self.bar_values);

        let height = area.height;
        let bar_step = BAR_WIDTH + BAR_GAP;
        let style = Style::default().fg(self.bar_color);

        for (i, &val) in self.bar_values.iter().enumerate() {
            let x_start = area.x + (i as u16) * bar_step;
            if x_start + BAR_WIDTH > area.x + area.width {
                break;
            }

            let h = (val * height as f32 * 8.0) as u16;
            let full_rows = (h / 8).min(height);
            let frac = (h % 8) as usize;

            // Draw full block rows
            for row in 0..full_rows {
                let y = area.y + height - 1 - row;
                for dx in 0..BAR_WIDTH {
                    if let Some(cell) = buf.cell_mut((x_start + dx, y)) {
                        cell.set_symbol(FULL_BLOCK).set_style(style);
                    }
                }
            }

            // Draw fractional top row
            if frac > 0 && full_rows < height {
                let y = area.y + height - 1 - full_rows;
                for dx in 0..BAR_WIDTH {
                    if let Some(cell) = buf.cell_mut((x_start + dx, y)) {
                        cell.set_symbol(BAR_BLOCKS[frac]).set_style(style);
                    }
                }
            }
        }
    }
}