russel 0.0.1

A simple static site builder I built for myself.
Documentation
//! Contains the dev command execution logic

use axum::Router;
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use std::net::SocketAddr;
use std::path::Path;
use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use tokio::sync::oneshot;
use tower_http::services::ServeDir;

use crate::error::Result;

/// Executes the 'dev' command which runs in development mode
pub async fn execute() -> Result<()> {
    println!("Running in development mode...");

    start_watch().await;

    Ok(())
}

pub async fn start_watch() {
    // Directory to watch for changes
    let watch_path = "./content";

    println!("Starting hot reload server, watching: {}", watch_path);
    println!("Press Ctrl+C to stop");

    // Create a channel for file change events
    let (file_tx, file_rx) = channel();

    // Create a watcher
    let mut watcher = RecommendedWatcher::new(file_tx, Config::default()).unwrap();

    // Start watching the directory
    watcher
        .watch(Path::new(watch_path), RecursiveMode::Recursive)
        .unwrap();

    // Channel for server shutdown
    let (shutdown_tx, _) = oneshot::channel::<()>();
    let shutdown_tx = Arc::new(Mutex::new(Some(shutdown_tx)));

    // Start the server initially
    let mut server_handle = start_server(Arc::clone(&shutdown_tx));
    let mut last_restart = Instant::now();

    // Listen for file changes
    loop {
        match file_rx.recv_timeout(Duration::from_millis(100)) {
            Ok(_) => {
                // Debounce to avoid multiple restarts for simultaneous file changes
                if last_restart.elapsed() > Duration::from_millis(500) {
                    println!("File change detected. Restarting server...");

                    // Signal the server to shut down
                    if let Some(tx) = shutdown_tx.lock().unwrap().take() {
                        let _ = tx.send(());
                    }

                    // Wait for the server thread to complete
                    if let Some(handle) = server_handle.take() {
                        let _ = handle.join();
                    }

                    // Create a new shutdown channel
                    let (new_tx, _) = oneshot::channel::<()>();
                    *shutdown_tx.lock().unwrap() = Some(new_tx);

                    // Start a new server
                    server_handle = start_server(Arc::clone(&shutdown_tx));
                    last_restart = Instant::now();
                }
            }
            Err(_) => {
                // No file changes, just continue
            }
        }
    }
}

fn start_server(
    shutdown_tx: Arc<Mutex<Option<oneshot::Sender<()>>>>,
) -> Option<thread::JoinHandle<()>> {
    println!("Starting server...");

    // Spawn a new thread for the server
    let handle = thread::spawn(move || {
        // Create a runtime for the Axum server
        let rt = tokio::runtime::Runtime::new().unwrap();

        rt.block_on(async {
            // Create the Axum server
            let service = ServeDir::new("content");
            let app = Router::new().fallback_service(service);

            // Get a new shutdown channel
            let (tx, rx) = oneshot::channel::<()>();
            *shutdown_tx.lock().unwrap() = Some(tx);

            // Bind to an address
            let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
            println!("Server started at http://{}", addr);

            // Start the server with graceful shutdown
            let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
            axum::serve(listener, app)
                .with_graceful_shutdown(async {
                    rx.await.ok();
                    println!("Server shutting down gracefully");
                })
                .await
                .unwrap();
        });
    });

    Some(handle)
}