use crate::audio;
use crate::config::Config;
use crate::error::{AppError, AppResult};
use crate::state::{AppState, SharedState};
use crate::ui;
use cpal::traits::StreamTrait;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io;
use std::time::Duration;
pub struct App {
config: Config,
terminal: Terminal<CrosstermBackend<std::io::Stdout>>,
}
#[derive(Debug, Clone, Copy)]
pub enum ExitCode {
Success = 0,
UserExit = 1, Error = 2, }
pub type AppRunResult = Result<(), AppError>;
pub struct RunResult {
pub result: AppRunResult,
pub exit_code: ExitCode,
}
impl App {
pub fn new() -> AppResult<Self> {
let config = Config::from_args().map_err(|e| AppError::Config(e.to_string()))?;
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(App { config, terminal })
}
pub async fn run(mut self) -> RunResult {
let (device, audio_config) =
match audio::setup_audio_device(self.config.device_name.clone()) {
Ok(result) => result,
Err(e) => {
return RunResult {
result: Err(e),
exit_code: ExitCode::Error,
};
}
};
let device_name = audio_config.device_name;
let shared_state = SharedState::new();
let (current_db, smoothed_db, display_db, threshold_reached) = shared_state.audio_refs();
let mut app_state = AppState::new(device_name, self.config.threshold_db);
let audio_callback = audio::create_audio_callback(
current_db,
smoothed_db,
display_db,
threshold_reached,
self.config.linear_threshold(),
);
let config = cpal::StreamConfig {
channels: audio_config.channels,
sample_rate: cpal::SampleRate(audio_config.sample_rate),
buffer_size: crate::constants::audio::BUFFER_SIZE,
};
let stream = match audio::build_audio_stream(&device, &config, audio_callback) {
Ok(stream) => stream,
Err(e) => {
return RunResult {
result: Err(e),
exit_code: ExitCode::Error,
};
}
};
if let Err(e) = stream.play() {
return RunResult {
result: Err(e.into()),
exit_code: ExitCode::Error,
};
}
let mut interval = tokio::time::interval(Duration::from_millis(
crate::constants::ui::UPDATE_INTERVAL_MS,
));
let mut exit_reason = ExitCode::Success;
loop {
app_state.update_from_audio(
&shared_state.current_db,
&shared_state.smoothed_db,
&shared_state.display_db,
&shared_state.threshold_reached,
);
if let Err(e) = self.terminal.draw(|f| {
let ui_state = ui::UiState {
device_name: app_state.device_name.clone(),
current_db: app_state.current_db,
display_db: app_state.display_db,
threshold_db: app_state.threshold_db,
status: app_state.status.clone(),
};
ui::render_ui(f, &ui_state);
}) {
return RunResult {
result: Err(e.into()),
exit_code: ExitCode::Error,
};
}
if app_state.threshold_reached {
exit_reason = ExitCode::Success;
break;
}
let mut should_exit = false;
tokio::select! {
_ = tokio::signal::ctrl_c() => {
should_exit = true;
exit_reason = ExitCode::UserExit;
}
_ = tokio::time::sleep(Duration::from_millis(1)) => {
}
}
if !should_exit
&& crossterm::event::poll(Duration::from_millis(0)).unwrap_or(false)
&& let Ok(Event::Key(key_event)) = crossterm::event::read()
{
match key_event.code {
KeyCode::Esc => {
should_exit = true;
exit_reason = ExitCode::UserExit;
}
KeyCode::Char('c')
if key_event
.modifiers
.contains(crossterm::event::KeyModifiers::CONTROL) =>
{
should_exit = true;
exit_reason = ExitCode::UserExit;
}
_ => {}
}
}
if should_exit {
break;
}
interval.tick().await;
}
drop(stream);
let _ = self.cleanup();
RunResult {
result: Ok(()),
exit_code: exit_reason,
}
}
fn cleanup(mut self) -> AppResult<()> {
disable_raw_mode()?;
execute!(
self.terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
self.terminal.show_cursor()?;
Ok(())
}
}