tui_breath 0.4.0

Terminal breathing guide built with Rust + Ratatui. 6 patterns, breath hold, workout mode, smooth animations, 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;
use tokio::sync::watch;

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

use app::App;

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

    let result = run_app().await;

    restore_terminal()?;

    match result {
        Ok(should_update) => {
            if should_update {
                std::process::Command::new("cargo")
                    .args(["install", "tui_breath", "--force"])
                    .status()?;
            }
            Ok(())
        }
        Err(e) => {
            eprintln!("Application error: {}", e);
            std::process::exit(1);
        }
    }
}

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<bool> {
    let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
    terminal.clear()?;

    let (update_tx, update_rx) = watch::channel::<Option<String>>(None);
    tokio::task::spawn(async move {
        if let Some(v) = crate::update::check_for_update().await {
            let _ = update_tx.send(Some(v));
        }
    });

    let mut app = App::new(update_rx)?;
    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(app.run_update)
}