use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use clap::Parser;
use crossterm::{
event::{self, Event},
};
use signal_hook::{consts::signal::*, iterator::Signals};
use crate::{
audio::AudioOutput,
config::Config,
library::{default_library_path, discover_tracks},
player::Player,
term::{hide_to_shell_toggleable, init_terminal, TerminalCleanup},
ui::{draw_ui, handle_key, UiAction, UiState},
};
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
#[arg(value_name = "PATH")]
path: Option<std::path::PathBuf>,
#[arg(long, default_value_t = 0)]
index: usize,
}
pub(crate) fn run() -> Result<()> {
TerminalCleanup::install_panic_hook();
let args = Args::parse();
let config = Config::load();
let theme = config.theme;
let library_path = args.path.unwrap_or_else(default_library_path);
let audio = AudioOutput::new_low_latency().context("Failed to initialize audio output")?;
let audio_ctl = audio.control();
let shutdown = Arc::new(AtomicBool::new(false));
{
let shutdown = Arc::clone(&shutdown);
let audio_ctl = audio_ctl.clone();
let mut signals = Signals::new([SIGINT, SIGTERM, SIGHUP, SIGQUIT])
.context("Failed to create signal watcher")?;
std::thread::spawn(move || {
for sig in signals.forever() {
shutdown.store(true, Ordering::Relaxed);
audio_ctl.shutdown_now();
if sig == SIGHUP {
}
}
});
}
let tracks = discover_tracks(&library_path)?;
let mut player = Player::new(tracks, args.index, audio)?;
if player.has_tracks() {
player.start_track(Duration::ZERO)?;
}
let mut terminal = init_terminal()?;
let _cleanup = TerminalCleanup;
let mut ui = UiState::new();
let tick_rate = Duration::from_millis(50);
loop {
if shutdown.load(Ordering::Relaxed) {
audio_ctl.shutdown_now();
player.stop_playback();
break;
}
player.refresh_volume();
if terminal.draw(|f| draw_ui(f, &player, &ui, &theme)).is_err() {
audio_ctl.shutdown_now();
player.stop_playback();
break;
}
if !player.loop_current && player.is_track_finished() {
let _ = player.next_track();
}
let timeout = tick_rate
.checked_sub(ui.last_tick.elapsed())
.unwrap_or(Duration::ZERO);
let polled = match event::poll(timeout) {
Ok(v) => v,
Err(_) => {
audio_ctl.shutdown_now();
player.stop_playback();
break;
}
};
if polled {
if shutdown.load(Ordering::Relaxed) {
audio_ctl.shutdown_now();
player.stop_playback();
break;
}
let ev = match event::read() {
Ok(v) => v,
Err(_) => {
audio_ctl.shutdown_now();
player.stop_playback();
break;
}
};
if let Event::Key(key) = ev {
match handle_key(key, &mut player, &mut ui)? {
UiAction::None => {}
UiAction::Quit => break,
UiAction::HideToShell => {
ui.reset_transient();
if let Err(e) = hide_to_shell_toggleable(&mut terminal) {
audio_ctl.shutdown_now();
player.stop_playback();
eprintln!("trix: hide failed: {e:#}");
break;
}
}
}
}
}
if ui.last_tick.elapsed() >= tick_rate {
ui.last_tick = Instant::now();
}
}
drop(terminal);
Ok(())
}