rizzoo 0.6.7

a cross platform color generation tool implementing Material 3 Expressive
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::channel;
use std::time::Duration;

use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};

use crate::color::types::{ColorScheme, Rgb};
use crate::config::{self, TemplateConfiguration};
use crate::error::LRatio;
use crate::export;
use crate::paths::Paths;
use crate::template::renderer::TemplateRenderer;

type LoadedState = (
    ColorScheme,
    Option<HashMap<String, Rgb>>,
    HashMap<String, TemplateConfiguration>,
);

pub fn start(paths: &Paths) -> Result<(), LRatio> {
    let paths = paths.clone();
    let (tx, rx) = channel();

    let mut watcher = RecommendedWatcher::new(tx, Config::default())
        .map_err(|e| LRatio::Custom(format!("failed to start watcher: {e}")))?;

    if paths.templates_dir.exists() {
        watcher
            .watch(&paths.templates_dir, RecursiveMode::Recursive)
            .map_err(|e| LRatio::Custom(format!("failed to watch templates dir: {e}")))?;
    }

    if paths.config_toml.exists() {
        watcher
            .watch(&paths.config_toml, RecursiveMode::NonRecursive)
            .map_err(|e| LRatio::Custom(format!("failed to watch config toml: {e}")))?;
    }

    log::info!(
        "Watching for changes in {} (Ctrl+C to quit)",
        paths.config_dir.display()
    );

    let running = Arc::new(AtomicBool::new(true));
    let r = running.clone();
    ctrlc::set_handler(move || {
        r.store(false, Ordering::SeqCst);
    })
    .map_err(|e| LRatio::Custom(format!("failed to set ctrl-c handler: {e}")))?;

    while running.load(Ordering::SeqCst) {
        match rx.recv_timeout(Duration::from_millis(500)) {
            Ok(Ok(event)) => {
                if event.kind.is_access() {
                    continue;
                }

                std::thread::sleep(Duration::from_millis(150));
                while rx.try_recv().is_ok() {}

                log::info!("Change detected! Reloading...");

                let Some((scheme, custom_colors, templates)) = load_state(&paths) else {
                    continue;
                };

                if let Err(e) =
                    TemplateRenderer::render_all(&paths, &scheme, custom_colors.as_ref())
                {
                    log::warn!("{e}");
                    continue;
                }

                if let Err(e) = export::generate::render_all(&paths, &templates) {
                    log::warn!("{e}");
                } else {
                    log::info!("Reload complete.");
                }
            }
            Ok(Err(e)) => log::warn!("watch error: {e}"),
            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
        }
    }

    log::info!("Watch mode terminated.");
    Ok(())
}

fn load_state(paths: &Paths) -> Option<LoadedState> {
    let scheme = export::json::read(paths).ok()?;

    let seed_color = scheme
        .seed_colors
        .as_ref()
        .and_then(|v| v.first())
        .copied()
        .unwrap_or(scheme.roles.primary);

    let config = config::Configuration::load(&paths.config_toml);
    let custom_map = config.harmonize_custom_colors(&seed_color);
    let custom_colors = if custom_map.is_empty() {
        None
    } else {
        Some(custom_map)
    };
    let templates = config.templates;

    Some((scheme, custom_colors, templates))
}