zik 0.1.0

A TUI web radio player with audio spectrum visualizer
mod app;
mod audio;
mod config;
mod player;
mod ui;
mod visualizer;

use std::env;
use std::io::{self, BufWriter};
use std::time::{Duration, Instant};

use crossterm::cursor;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use crossterm::execute;
use crossterm::terminal::{
    EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;

use app::App;
use config::Config;
use visualizer::Visualizer;

fn process_events(app: &mut App) -> io::Result<bool> {
    let mut changed = false;
    while event::poll(Duration::ZERO)? {
        if let Event::Key(key) = event::read()?
            && matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) {
                match key.code {
                    KeyCode::Char('q') => app.quit(),
                    KeyCode::Char('s') => app.toggle_selector(),
                    KeyCode::Char('?') => app.show_help = !app.show_help,
                    KeyCode::Esc if app.show_selector => app.show_selector = false,
                    KeyCode::Char('j') | KeyCode::Down => app.next(),
                    KeyCode::Char('k') | KeyCode::Up => app.previous(),
                    KeyCode::Enter => app.toggle_play(),
                    _ => continue,
                }
                changed = true;
            }
    }
    Ok(changed)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = env::args().collect();
    let config = Config::load()?;

    let initial_radio = args.get(1).map(|s| s.as_str());
    if let Some(name) = initial_radio
        && !config.radios.contains_key(name) {
            let available: Vec<&String> = config.radios.keys().collect();
            eprintln!("Unknown radio '{name}'. Available: {available:?}");
            std::process::exit(1);
        }

    let bar_color = config
        .ui
        .as_ref()
        .and_then(|u| u.bar_color.as_deref())
        .unwrap_or("Magenta");
    let mut viz = Visualizer::new(bar_color);
    let mut app = App::new(config, initial_radio);

    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, cursor::Hide)?;
    let backend = CrosstermBackend::new(BufWriter::with_capacity(1 << 16, stdout));
    let mut terminal = Terminal::new(backend)?;

    let frame_duration = Duration::from_millis(33);
    let mut next_frame = Instant::now();
    let mut needs_draw = true;

    while app.running {
        // Drain all pending input events (non-blocking)
        if process_events(&mut app)? {
            needs_draw = true;
        }

        let now = Instant::now();
        let viz_due = now >= next_frame && app.player.is_playing();

        if viz_due {
            app.tick();
            needs_draw = true;
        }

        if needs_draw {
            terminal.draw(|frame| {
                ui::draw(frame, &app, &mut viz);
            })?;
            needs_draw = false;
            if viz_due {
                next_frame = Instant::now() + frame_duration;
            }
        }

        // Wait for next event or next frame — wakes instantly on keypress
        let wait = if app.player.is_playing() {
            next_frame.saturating_duration_since(Instant::now())
        } else {
            Duration::from_millis(100)
        };
        event::poll(wait)?;
    }

    app.player.stop();
    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen, cursor::Show)?;

    Ok(())
}