bim 0.1.0

A content-first static site generator for blogs, digital gardens, and knowledge bases
Documentation
use crate::config::Config;
use crate::discovery::discover_site;
use crate::render::build_site;
use anyhow::Result;
use notify::RecursiveMode;
use notify_debouncer_mini::new_debouncer;
use std::path::Path;
use std::sync::mpsc::channel;
use std::time::Duration;

const IGNORED_PATTERNS: &[&str] = &[
    ".git",
    ".obsidian",
    "node_modules",
    "target",
    "dist",
    ".DS_Store",
];

pub fn watch_and_rebuild(config: &Config) -> Result<()> {
    // Initial build
    tracing::info!("Performing initial build...");
    let site = discover_site(config)?;
    build_site(&site, config)?;
    tracing::info!("Initial build complete. Watching for changes...");

    // Set up file watcher
    let (tx, rx) = channel();

    let mut debouncer = new_debouncer(Duration::from_millis(500), tx)?;

    debouncer
        .watcher()
        .watch(&config.source_dir, RecursiveMode::Recursive)?;

    // Watch for changes
    loop {
        match rx.recv() {
            Ok(Ok(events)) => {
                // Filter out ignored paths
                let relevant_events: Vec<_> =
                    events.iter().filter(|e| !should_ignore(&e.path)).collect();

                if relevant_events.is_empty() {
                    continue;
                }

                tracing::info!(
                    "Detected changes in {} file(s), rebuilding...",
                    relevant_events.len()
                );

                match discover_site(config) {
                    Ok(site) => match build_site(&site, config) {
                        Ok(()) => tracing::info!("Rebuild complete!"),
                        Err(e) => tracing::error!("Build error: {}", e),
                    },
                    Err(e) => tracing::error!("Discovery error: {}", e),
                }
            }
            Ok(Err(e)) => {
                tracing::warn!("Watch error: {:?}", e);
            }
            Err(e) => {
                tracing::error!("Channel error: {}", e);
                break;
            }
        }
    }

    Ok(())
}

fn should_ignore(path: &Path) -> bool {
    let path_str = path.to_string_lossy();

    for pattern in IGNORED_PATTERNS {
        if path_str.contains(pattern) {
            return true;
        }
    }

    // Ignore hidden files
    if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
        if name.starts_with('.') {
            return true;
        }
    }

    false
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_should_ignore() {
        assert!(should_ignore(Path::new("/project/.git/config")));
        assert!(should_ignore(Path::new("/project/node_modules/pkg")));
        assert!(should_ignore(Path::new("/project/.DS_Store")));
        assert!(!should_ignore(Path::new("/project/blog/post.md")));
    }
}