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 {
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;
}
}
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(())
}