ddoc 0.18.0

doc site generator
Documentation
use {
    crate::*,
    crossbeam::channel,
    notify::{
        EventKind,
        RecommendedWatcher,
        RecursiveMode,
        Watcher,
        event::{
            AccessKind,
            AccessMode,
            DataChange,
            ModifyKind,
            RenameMode,
        },
    },
    std::{
        path::PathBuf,
        sync::{
            Arc,
            atomic::{
                AtomicBool,
                Ordering,
            },
        },
        thread,
    },
    termimad::crossterm::style::Stylize,
};

const DEBOUNCE_DELAY_MS: u64 = 40;

#[derive(Debug)]
pub enum FileChange {
    /// Creation, move inside, edition, etc.
    Write(PathBuf),
    /// Removal, move outside, etc. but also first part of a move inside
    Removal(PathBuf),
    /// Anything else looking relevant (e.g. multiple files written)
    /// A full rebuild is probably needed
    Other,
}

/// Something that should be watched for changes to trigger a rebuild of the project
pub struct WatchTarget {
    pub path: PathBuf,
    pub recursive: bool, // ie directory or not
}

impl WatchTarget {
    pub fn new_dir<P: Into<PathBuf>>(path: P) -> Self {
        Self {
            path: path.into(),
            recursive: true,
        }
    }
    pub fn new_file<P: Into<PathBuf>>(path: P) -> Self {
        Self {
            path: path.into(),
            recursive: false,
        }
    }
}

/// watch for file changes to keep a project up to date
///
/// Caller should keep the returned watcher alive (e.g., by storing it in a variable)
/// as watching stops when the watcher is dropped.
pub fn rebuild_on_change(
    mut project: Project,
    base_url: String, // to display the modified page URL
) -> Result<RecommendedWatcher, notify::Error> {
    let skip = Arc::new(AtomicBool::new(false));
    let snd_skip = skip.clone();
    //let (snd, rcv) = mpsc::sync_channel::<FileChange>(100);
    let (snd, rcv) = channel::unbounded::<FileChange>();
    let mut watcher =
        notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
            Ok(we) => {
                // Filter to get events which are relevant for a rebuild
                // (not a cleaning) of the project:
                // - file being modified
                // - file being created
                // - file being renamed
                // - file being removed (matters when the file is linked from the head)
                let mut is_removal = false;
                match we.kind {
                    EventKind::Modify(ModifyKind::Metadata(_)) => {
                        return; // useless event
                    }
                    EventKind::Modify(ModifyKind::Data(DataChange::Content)) => {
                        debug!("modify content event: {we:?}");
                    }
                    EventKind::Modify(ModifyKind::Data(DataChange::Any)) => {
                        return; // probably useless event with no real change
                    }
                    EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
                        debug!("rename from event: {we:?}");
                        is_removal = true;
                    }
                    EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
                        debug!("rename to event: {we:?}");
                    }
                    EventKind::Access(AccessKind::Close(AccessMode::Write)) => {
                        // file was created or modified
                        debug!("close write event: {we:?}");
                    }
                    EventKind::Access(_) => {
                        return; // probably useless event
                    }
                    _ => {
                        // probably useless, log just in case a user has missing rebuilds
                        debug!("skipped notify event: {we:?}");
                        return;
                    }
                }
                if snd_skip.load(Ordering::SeqCst) {
                    debug!("skipping event due to skip flag: {we:?}");
                    return;
                }
                snd_skip.store(true, Ordering::SeqCst);
                let path = if we.paths.len() == 1 {
                    Some(we.paths[0].clone())
                } else {
                    None // several paths changed
                };
                let change = match (path, is_removal) {
                    (Some(p), true) => FileChange::Removal(p),
                    (Some(p), false) => FileChange::Write(p),
                    (None, _) => FileChange::Other,
                };
                let _ = snd.send(change);
            }
            Err(e) => warn!("watch error: {e:?}"),
        })?;
    for target in project.watch_targets() {
        watcher.watch(
            &target.path,
            if target.recursive {
                RecursiveMode::Recursive
            } else {
                RecursiveMode::NonRecursive
            },
        )?;
    }
    // start the build thread
    thread::spawn(move || {
        let debounce_delay = std::time::Duration::from_millis(DEBOUNCE_DELAY_MS);
        loop {
            match rcv.recv() {
                Ok(change) => {
                    thread::sleep(debounce_delay);
                    info!("rebuilding site due to {change:?}");
                    let start = std::time::Instant::now();
                    match project.update(change, &base_url) {
                        Ok(true) => eprintln!("Site rebuilt in {}", duration_since(start)),
                        Ok(false) => debug!("No rebuild needed"),
                        Err(e) => eprintln!("{}{}", "Error rebuilding site: ".red().bold(), e),
                    }
                    skip.store(false, Ordering::SeqCst);
                }
                Err(e) => {
                    warn!("rebuild_on_change channel error: {e:?}");
                    break;
                }
            }
        }
    });
    Ok(watcher)
}

pub fn duration_since(start: std::time::Instant) -> String {
    let millis = start.elapsed().as_secs_f32() / 1000.0;
    format!("{:.3}ms", millis)
}