tui_breath 0.2.0

Terminal breathing guide built with Rust + Ratatui. Smooth eased animations, 4-7-8 / box / diaphragmatic patterns, JSON session tracking.
use anyhow::Result;
use crossterm::{
    event,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
    ExecutableCommand,
};
use ratatui::prelude::*;
use std::io;
use std::time::Instant;
use tokio::time::Duration;

mod animator;
mod app;
mod audio;
mod engine;
mod storage;
mod ui;

use app::App;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
    setup_terminal()?;

    let result = run_app().await;

    restore_terminal()?;

    if let Err(e) = result {
        eprintln!("Application error: {}", e);
        std::process::exit(1);
    }

    Ok(())
}

fn setup_terminal() -> Result<()> {
    enable_raw_mode()?;
    io::stdout().execute(EnterAlternateScreen)?;
    Ok(())
}

fn restore_terminal() -> Result<()> {
    disable_raw_mode()?;
    io::stdout().execute(LeaveAlternateScreen)?;
    Ok(())
}

async fn run_app() -> Result<()> {
    let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
    terminal.clear()?;

    let mut app = App::new()?;
    let tick_rate = Duration::from_millis(33);
    let mut interval = tokio::time::interval(tick_rate);
    let mut last_tick = Instant::now();

    loop {
        terminal.draw(|f| ui::draw(f, &app))?;

        if event::poll(Duration::from_millis(0))? {
            if let event::Event::Key(key) = event::read()? {
                app.on_key(key);
            }
        }

        interval.tick().await;
        let delta = last_tick.elapsed().as_secs_f64();
        last_tick = Instant::now();

        if !app.session_is_paused() {
            if let Some(anim) = app.session_animator.as_mut() {
                anim.tick(delta);
            }
        }

        app.on_tick(delta);

        if app.is_quitting() {
            break;
        }
    }

    Ok(())
}