cube-tui 0.1.9

Terminal UI timer and session manager for speedcubing, with optional web dashboard and BLE (GAN) timer support.
#[cfg(feature = "dashboard")]
use axum::extract::Path;
#[cfg(feature = "dashboard")]
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
#[cfg(feature = "dashboard")]
use axum::response::{IntoResponse, Response};
#[cfg(feature = "dashboard")]
use axum::routing::get;
#[cfg(feature = "dashboard")]
use axum::{Json, Router};
#[cfg(feature = "dashboard")]
use rust_embed::RustEmbed;

#[cfg(feature = "dashboard")]
#[derive(RustEmbed)]
#[folder = "dashboard/dist"]
struct DashboardAssets;

#[cfg(feature = "dashboard")]
fn serve_asset(path: &str) -> Response {
    match DashboardAssets::get(path) {
        Some(content) => {
            let mime = mime_guess::from_path(path)
                .first_or_octet_stream()
                .to_string();
            let mut headers = HeaderMap::new();
            headers.insert(
                header::CONTENT_TYPE,
                HeaderValue::from_str(&mime)
                    .unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")),
            );
            (StatusCode::OK, headers, content.data.to_vec()).into_response()
        }
        None => (StatusCode::NOT_FOUND, "Not found").into_response(),
    }
}

#[cfg(feature = "dashboard")]
fn api_sessions() -> Response {
    crate::persistence::load().map_or_else(
        || Json(Vec::<serde_json::Value>::new()).into_response(),
        |sessions| Json(sessions).into_response(),
    )
}

#[cfg(feature = "dashboard")]
pub fn run_dashboard(port: u16) -> ! {
    let rt = crate::utils::runtime::runtime();
    let result = rt.block_on(async move { run_dashboard_async(port).await });
    match result {
        Ok(()) => std::process::exit(0),
        Err(err) => {
            eprintln!("Dashboard error: {err}");
            std::process::exit(1);
        }
    }
}

#[cfg(feature = "dashboard")]
async fn run_dashboard_async(port: u16) -> anyhow::Result<()> {
    let app = Router::new()
        .route("/api/sessions", get(|| async { api_sessions() }))
        .route("/", get(|| async { serve_asset("index.html") }))
        .route(
            "/{*path}",
            get(|Path(path): Path<String>| async move { serve_asset(&path) }),
        );

    let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
    let url = format!("http://localhost:{port}");

    println!("Dashboard running at {url}");

    if let Err(e) = open::that(&url) {
        eprintln!("Could not open browser automatically: {e}");
    }

    let listener = tokio::net::TcpListener::bind(addr)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to bind to address: {e}"))?;

    axum::serve(listener, app)
        .await
        .map_err(|e| anyhow::anyhow!("Server error: {e}"))?;
    Ok(())
}