diff --git a/src/bin/build.rs b/src/bin/build.rs new file mode 100644 index 00000000..72251f8c --- /dev/null +++ b/src/bin/build.rs @@ -0,0 +1,44 @@ +use clap::{ArgMatches, SubCommand, App}; +use mdbook::MDBook; +use mdbook::errors::Result; +use {get_book_dir, open}; + +// Create clap subcommand arguments +pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("build") + .about("Build the book from the markdown files") + .arg_from_usage("-o, --open 'Open the compiled book in a web browser'") + .arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'") + .arg_from_usage("--no-create 'Will not create non-existent files linked from SUMMARY.md'") + .arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'") + .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'") +} + +// Build command implementation +pub fn execute(args: &ArgMatches) -> Result<()> { + let book_dir = get_book_dir(args); + let book = MDBook::new(&book_dir).read_config()?; + + let mut book = match args.value_of("dest-dir") { + Some(dest_dir) => book.with_destination(dest_dir), + None => book, + }; + + if args.is_present("no-create") { + book.create_missing = false; + } + + if args.is_present("curly-quotes") { + book = book.with_curly_quotes(true); + } + + book.build()?; + + if let Some(d) = book.get_destination() { + if args.is_present("open") { + open(d.join("index.html")); + } + } + + Ok(()) +} diff --git a/src/bin/init.rs b/src/bin/init.rs new file mode 100644 index 00000000..91224d0e --- /dev/null +++ b/src/bin/init.rs @@ -0,0 +1,79 @@ +use std::io; +use std::io::Write; +use clap::{ArgMatches, SubCommand, App}; +use mdbook::MDBook; +use mdbook::errors::Result; +use get_book_dir; + +// Create clap subcommand arguments +pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("init") + .about("Create boilerplate structure and files in the directory") + // the {n} denotes a newline which will properly aligned in all help messages + .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'") + .arg_from_usage("--theme 'Copies the default theme into your source folder'") + .arg_from_usage("--force 'skip confirmation prompts'") +} + +// Init command implementation +pub fn execute(args: &ArgMatches) -> Result<()> { + + let book_dir = get_book_dir(args); + let mut book = MDBook::new(&book_dir); + + // Call the function that does the initialization + book.init()?; + + // If flag `--theme` is present, copy theme to src + if args.is_present("theme") { + + // Skip this if `--force` is present + if !args.is_present("force") { + // Print warning + print!("\nCopying the default theme to {:?}", book.get_source()); + println!("could potentially overwrite files already present in that directory."); + print!("\nAre you sure you want to continue? (y/n) "); + + // Read answer from user and exit if it's not 'yes' + if !confirm() { + println!("\nSkipping...\n"); + println!("All done, no errors..."); + ::std::process::exit(0); + } + } + + // Call the function that copies the theme + book.copy_theme()?; + println!("\nTheme copied."); + + } + + // Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root` + let is_dest_inside_root = book.get_destination() + .map(|p| p.starts_with(book.get_root())) + .unwrap_or(false); + + if !args.is_present("force") && is_dest_inside_root { + println!("\nDo you want a .gitignore to be created? (y/n)"); + + if confirm() { + book.create_gitignore(); + println!("\n.gitignore created."); + } + } + + println!("\nAll done, no errors..."); + + Ok(()) +} + +// Simple function that user comfirmation +fn confirm() -> bool { + io::stdout().flush().unwrap(); + let mut s = String::new(); + io::stdin().read_line(&mut s).ok(); + match &*s.trim() { + "Y" | "y" | "yes" | "Yes" => true, + _ => false, + } +} diff --git a/src/bin/mdbook.rs b/src/bin/mdbook.rs index 1327a1f6..43fa0212 100644 --- a/src/bin/mdbook.rs +++ b/src/bin/mdbook.rs @@ -5,32 +5,19 @@ extern crate log; extern crate env_logger; extern crate open; -// Dependencies for the Watch feature -#[cfg(feature = "watch")] -extern crate notify; -#[cfg(feature = "watch")] -extern crate time; -#[cfg(feature = "watch")] -extern crate crossbeam; - use std::env; use std::ffi::OsStr; use std::io::{self, Write}; use std::path::{Path, PathBuf}; -use mdbook::errors::*; +use clap::{App, ArgMatches, AppSettings}; -use clap::{App, ArgMatches, SubCommand, AppSettings}; - -// Uses for the Watch feature +pub mod build; +pub mod init; +pub mod test; +#[cfg(feature = "serve")] +pub mod serve; #[cfg(feature = "watch")] -use notify::Watcher; -#[cfg(feature = "watch")] -use std::time::Duration; -#[cfg(feature = "watch")] -use std::sync::mpsc::channel; - - -use mdbook::MDBook; +pub mod watch; const NAME: &'static str = "mdbook"; @@ -38,55 +25,31 @@ fn main() { env_logger::init().unwrap(); // Create a list of valid arguments and sub-commands - let matches = App::new(NAME) - .about("Create a book in form of a static website from markdown files") - .author("Mathieu David ") - // Get the version from our Cargo.toml using clap's crate_version!() macro - .version(&*format!("v{}", crate_version!())) - .setting(AppSettings::SubcommandRequired) - .after_help("For more information about a specific command, try `mdbook --help`\nSource code for mdbook available at: https://github.com/azerupi/mdBook") - .subcommand(SubCommand::with_name("init") - .about("Create boilerplate structure and files in the directory") - // the {n} denotes a newline which will properly aligned in all help messages - .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'") - .arg_from_usage("--theme 'Copies the default theme into your source folder'") - .arg_from_usage("--force 'skip confirmation prompts'")) - .subcommand(SubCommand::with_name("build") - .about("Build the book from the markdown files") - .arg_from_usage("-o, --open 'Open the compiled book in a web browser'") - .arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'") - .arg_from_usage("--no-create 'Will not create non-existent files linked from SUMMARY.md'") - .arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'") - .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")) - .subcommand(SubCommand::with_name("watch") - .about("Watch the files for changes") - .arg_from_usage("-o, --open 'Open the compiled book in a web browser'") - .arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'") - .arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'") - .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")) - .subcommand(SubCommand::with_name("serve") - .about("Serve the book at http://localhost:3000. Rebuild and reload on change.") - .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'") - .arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'") - .arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'") - .arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'") - .arg_from_usage("-w, --websocket-port=[ws-port] 'Use another port for the websocket connection (livereload){n}(Defaults to 3001)'") - .arg_from_usage("-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'") - .arg_from_usage("-a, --address=[address] 'Address that the browser can reach the websocket server from{n}(Defaults to the interface address)'") - .arg_from_usage("-o, --open 'Open the book server in a web browser'")) - .subcommand(SubCommand::with_name("test") - .about("Test that code samples compile")) - .get_matches(); + let app = App::new(NAME) + .about("Create a book in form of a static website from markdown files") + .author("Mathieu David ") + // Get the version from our Cargo.toml using clap's crate_version!() macro + .version(concat!("v",crate_version!())) + .setting(AppSettings::SubcommandRequired) + .after_help("For more information about a specific command, try `mdbook --help`\nSource code for mdbook available at: https://github.com/azerupi/mdBook") + .subcommand(init::make_subcommand()) + .subcommand(build::make_subcommand()) + .subcommand(test::make_subcommand()); + + #[cfg(feature = "watch")] + let app = app.subcommand(watch::make_subcommand()); + #[cfg(feature = "serve")] + let app = app.subcommand(serve::make_subcommand()); // Check which subcomamnd the user ran... - let res = match matches.subcommand() { - ("init", Some(sub_matches)) => init(sub_matches), - ("build", Some(sub_matches)) => build(sub_matches), + let res = match app.get_matches().subcommand() { + ("init", Some(sub_matches)) => init::execute(sub_matches), + ("build", Some(sub_matches)) => build::execute(sub_matches), #[cfg(feature = "watch")] - ("watch", Some(sub_matches)) => watch(sub_matches), + ("watch", Some(sub_matches)) => watch::execute(sub_matches), #[cfg(feature = "serve")] - ("serve", Some(sub_matches)) => serve::serve(sub_matches), - ("test", Some(sub_matches)) => test(sub_matches), + ("serve", Some(sub_matches)) => serve::execute(sub_matches), + ("test", Some(sub_matches)) => test::execute(sub_matches), (_, _) => unreachable!(), }; @@ -96,254 +59,6 @@ fn main() { } } - -// Simple function that user comfirmation -fn confirm() -> bool { - io::stdout().flush().unwrap(); - let mut s = String::new(); - io::stdin().read_line(&mut s).ok(); - match &*s.trim() { - "Y" | "y" | "yes" | "Yes" => true, - _ => false, - } -} - - -// Init command implementation -fn init(args: &ArgMatches) -> Result<()> { - - let book_dir = get_book_dir(args); - let mut book = MDBook::new(&book_dir); - - // Call the function that does the initialization - book.init()?; - - // If flag `--theme` is present, copy theme to src - if args.is_present("theme") { - - // Skip this if `--force` is present - if !args.is_present("force") { - // Print warning - print!("\nCopying the default theme to {:?}", book.get_source()); - println!("could potentially overwrite files already present in that directory."); - print!("\nAre you sure you want to continue? (y/n) "); - - // Read answer from user and exit if it's not 'yes' - if !confirm() { - println!("\nSkipping...\n"); - println!("All done, no errors..."); - ::std::process::exit(0); - } - } - - // Call the function that copies the theme - book.copy_theme()?; - println!("\nTheme copied."); - - } - - // Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root` - let is_dest_inside_root = book.get_destination() - .map(|p| p.starts_with(book.get_root())) - .unwrap_or(false); - - if !args.is_present("force") && is_dest_inside_root { - println!("\nDo you want a .gitignore to be created? (y/n)"); - - if confirm() { - book.create_gitignore(); - println!("\n.gitignore created."); - } - } - - println!("\nAll done, no errors..."); - - Ok(()) -} - - -// Build command implementation -fn build(args: &ArgMatches) -> Result<()> { - let book_dir = get_book_dir(args); - let book = MDBook::new(&book_dir).read_config()?; - - let mut book = match args.value_of("dest-dir") { - Some(dest_dir) => book.with_destination(dest_dir), - None => book, - }; - - if args.is_present("no-create") { - book.create_missing = false; - } - - if args.is_present("curly-quotes") { - book = book.with_curly_quotes(true); - } - - book.build()?; - - if let Some(d) = book.get_destination() { - if args.is_present("open") { - open(d.join("index.html")); - } - } - - Ok(()) -} - - -// Watch command implementation -#[cfg(feature = "watch")] -fn watch(args: &ArgMatches) -> Result<()> { - let book_dir = get_book_dir(args); - let book = MDBook::new(&book_dir).read_config()?; - - let mut book = match args.value_of("dest-dir") { - Some(dest_dir) => book.with_destination(dest_dir), - None => book, - }; - - if args.is_present("curly-quotes") { - book = book.with_curly_quotes(true); - } - - if args.is_present("open") { - book.build()?; - if let Some(d) = book.get_destination() { - open(d.join("index.html")); - } - } - - trigger_on_change(&mut book, |path, book| { - println!("File changed: {:?}\nBuilding book...\n", path); - if let Err(e) = book.build() { - println!("Error while building: {:?}", e); - } - println!(""); - }); - - Ok(()) -} - -#[cfg(feature = "serve")] -mod serve { - extern crate iron; - extern crate staticfile; - extern crate ws; - - use std; - use std::path::Path; - use mdbook::errors::*; - use self::iron::{Iron, AfterMiddleware, IronResult, IronError, Request, Response, status, Set, Chain}; - use clap::ArgMatches; - use mdbook::MDBook; - - use {get_book_dir, open, trigger_on_change}; - - struct ErrorRecover; - - impl AfterMiddleware for ErrorRecover { - fn catch(&self, _: &mut Request, err: IronError) -> IronResult { - match err.response.status { - // each error will result in 404 response - Some(_) => Ok(err.response.set(status::NotFound)), - _ => Err(err), - } - } - } - - // Watch command implementation - pub fn serve(args: &ArgMatches) -> Result<()> { - const RELOAD_COMMAND: &'static str = "reload"; - - let book_dir = get_book_dir(args); - let book = MDBook::new(&book_dir).read_config()?; - - let mut book = match args.value_of("dest-dir") { - Some(dest_dir) => book.with_destination(Path::new(dest_dir)), - None => book, - }; - - if book.get_destination().is_none() { - println!("The HTML renderer is not set up, impossible to serve the files."); - std::process::exit(2); - } - - if args.is_present("curly-quotes") { - book = book.with_curly_quotes(true); - } - - let port = args.value_of("port").unwrap_or("3000"); - let ws_port = args.value_of("websocket-port").unwrap_or("3001"); - let interface = args.value_of("interface").unwrap_or("localhost"); - let public_address = args.value_of("address").unwrap_or(interface); - let open_browser = args.is_present("open"); - - let address = format!("{}:{}", interface, port); - let ws_address = format!("{}:{}", interface, ws_port); - - book.set_livereload(format!(r#" - - "#, - public_address, - ws_port, - RELOAD_COMMAND)); - - book.build()?; - - let mut chain = Chain::new(staticfile::Static::new(book.get_destination() - .expect("destination is present, checked before"))); - chain.link_after(ErrorRecover); - let _iron = Iron::new(chain).http(&*address).unwrap(); - - let ws_server = ws::WebSocket::new(|_| |_| Ok(())).unwrap(); - - let broadcaster = ws_server.broadcaster(); - - std::thread::spawn(move || { ws_server.listen(&*ws_address).unwrap(); }); - - let serving_url = format!("http://{}", address); - println!("\nServing on: {}", serving_url); - - if open_browser { - open(serving_url); - } - - trigger_on_change(&mut book, move |path, book| { - println!("File changed: {:?}\nBuilding book...\n", path); - match book.build() { - Err(e) => println!("Error while building: {:?}", e), - _ => broadcaster.send(RELOAD_COMMAND).unwrap(), - } - println!(""); - }); - - Ok(()) - } -} - -fn test(args: &ArgMatches) -> Result<()> { - let book_dir = get_book_dir(args); - let mut book = MDBook::new(&book_dir).read_config()?; - - book.test()?; - - Ok(()) -} - - fn get_book_dir(args: &ArgMatches) -> PathBuf { if let Some(dir) = args.value_of("dir") { // Check if path is relative from current dir, or absolute... @@ -363,72 +78,3 @@ fn open>(path: P) { println!("Error opening web browser: {}", e); } } - - -// Calls the closure when a book source file is changed. This is blocking! -#[cfg(feature = "watch")] -fn trigger_on_change(book: &mut MDBook, closure: F) -> () - where F: Fn(&Path, &mut MDBook) -> () -{ - use notify::RecursiveMode::*; - use notify::DebouncedEvent::*; - - // Create a channel to receive the events. - let (tx, rx) = channel(); - - let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) { - Ok(w) => w, - Err(e) => { - println!("Error while trying to watch the files:\n\n\t{:?}", e); - ::std::process::exit(0); - }, - }; - - // Add the source directory to the watcher - if let Err(e) = watcher.watch(book.get_source(), Recursive) { - println!("Error while watching {:?}:\n {:?}", book.get_source(), e); - ::std::process::exit(0); - }; - - // Add the theme directory to the watcher - if let Some(t) = book.get_theme_path() { - watcher.watch(t, Recursive).unwrap_or_default(); - } - - - // Add the book.{json,toml} file to the watcher if it exists, because it's not - // located in the source directory - if watcher - .watch(book.get_root().join("book.json"), NonRecursive) - .is_err() { - // do nothing if book.json is not found - } - if watcher - .watch(book.get_root().join("book.toml"), NonRecursive) - .is_err() { - // do nothing if book.toml is not found - } - - println!("\nListening for changes...\n"); - - loop { - match rx.recv() { - Ok(event) => { - match event { - NoticeWrite(path) | - NoticeRemove(path) | - Create(path) | - Write(path) | - Remove(path) | - Rename(_, path) => { - closure(&path, book); - }, - _ => {}, - } - }, - Err(e) => { - println!("An error occured: {:?}", e); - }, - } - } -} diff --git a/src/bin/serve.rs b/src/bin/serve.rs new file mode 100644 index 00000000..63ffa860 --- /dev/null +++ b/src/bin/serve.rs @@ -0,0 +1,121 @@ +extern crate iron; +extern crate staticfile; +extern crate ws; + +use std; +use std::path::Path; +use self::iron::{Iron, AfterMiddleware, IronResult, IronError, Request, Response, status, Set, Chain}; +use clap::{ArgMatches, SubCommand, App}; +use mdbook::MDBook; +use mdbook::errors::Result; +use {get_book_dir, open}; +#[cfg(feature = "watch")] +use watch; + +struct ErrorRecover; + +// Create clap subcommand arguments +pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("serve") + .about("Serve the book at http://localhost:3000. Rebuild and reload on change.") + .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'") + .arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'") + .arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'") + .arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'") + .arg_from_usage("-w, --websocket-port=[ws-port] 'Use another port for the websocket connection (livereload){n}(Defaults to 3001)'") + .arg_from_usage("-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'") + .arg_from_usage("-a, --address=[address] 'Address that the browser can reach the websocket server from{n}(Defaults to the interface address)'") + .arg_from_usage("-o, --open 'Open the book server in a web browser'") +} + +// Watch command implementation +pub fn execute(args: &ArgMatches) -> Result<()> { + const RELOAD_COMMAND: &'static str = "reload"; + + let book_dir = get_book_dir(args); + let book = MDBook::new(&book_dir).read_config()?; + + let mut book = match args.value_of("dest-dir") { + Some(dest_dir) => book.with_destination(Path::new(dest_dir)), + None => book, + }; + + if let None = book.get_destination() { + println!("The HTML renderer is not set up, impossible to serve the files."); + std::process::exit(2); + } + + if args.is_present("curly-quotes") { + book = book.with_curly_quotes(true); + } + + let port = args.value_of("port").unwrap_or("3000"); + let ws_port = args.value_of("websocket-port").unwrap_or("3001"); + let interface = args.value_of("interface").unwrap_or("localhost"); + let public_address = args.value_of("address").unwrap_or(interface); + let open_browser = args.is_present("open"); + + let address = format!("{}:{}", interface, port); + let ws_address = format!("{}:{}", interface, ws_port); + + book.set_livereload(format!(r#" + +"#, + public_address, + ws_port, + RELOAD_COMMAND)); + + book.build()?; + + let mut chain = Chain::new(staticfile::Static::new(book.get_destination() + .expect("destination is present, checked before"))); + chain.link_after(ErrorRecover); + let _iron = Iron::new(chain).http(&*address).unwrap(); + + let ws_server = ws::WebSocket::new(|_| |_| Ok(())).unwrap(); + + let broadcaster = ws_server.broadcaster(); + + std::thread::spawn(move || { ws_server.listen(&*ws_address).unwrap(); }); + + let serving_url = format!("http://{}", address); + println!("\nServing on: {}", serving_url); + + if open_browser { + open(serving_url); + } + + #[cfg(feature = "watch")] + watch::trigger_on_change(&mut book, move |path, book| { + println!("File changed: {:?}\nBuilding book...\n", path); + match book.build() { + Err(e) => println!("Error while building: {:?}", e), + _ => broadcaster.send(RELOAD_COMMAND).unwrap(), + } + println!(""); + }); + + Ok(()) +} + +impl AfterMiddleware for ErrorRecover { + fn catch(&self, _: &mut Request, err: IronError) -> IronResult { + match err.response.status { + // each error will result in 404 response + Some(_) => Ok(err.response.set(status::NotFound)), + _ => Err(err), + } + } +} diff --git a/src/bin/test.rs b/src/bin/test.rs new file mode 100644 index 00000000..e2ce5471 --- /dev/null +++ b/src/bin/test.rs @@ -0,0 +1,19 @@ +use clap::{ArgMatches, SubCommand, App}; +use mdbook::MDBook; +use mdbook::errors::Result; +use get_book_dir; + +// Create clap subcommand arguments +pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("test").about("Test that code samples compile") +} + +// test command implementation +pub fn execute(args: &ArgMatches) -> Result<()> { + let book_dir = get_book_dir(args); + let mut book = MDBook::new(&book_dir).read_config()?; + + book.test()?; + + Ok(()) +} diff --git a/src/bin/watch.rs b/src/bin/watch.rs new file mode 100644 index 00000000..8b520194 --- /dev/null +++ b/src/bin/watch.rs @@ -0,0 +1,121 @@ +extern crate notify; +extern crate time; +extern crate crossbeam; + +use std::path::Path; +use self::notify::Watcher; +use std::time::Duration; +use std::sync::mpsc::channel; +use clap::{ArgMatches, SubCommand, App}; +use mdbook::MDBook; +use mdbook::errors::Result; +use {get_book_dir, open}; + +// Create clap subcommand arguments +pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { + SubCommand::with_name("watch") + .about("Watch the files for changes") + .arg_from_usage("-o, --open 'Open the compiled book in a web browser'") + .arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'") + .arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'") + .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'") +} + +// Watch command implementation +pub fn execute(args: &ArgMatches) -> Result<()> { + let book_dir = get_book_dir(args); + let book = MDBook::new(&book_dir).read_config()?; + + let mut book = match args.value_of("dest-dir") { + Some(dest_dir) => book.with_destination(dest_dir), + None => book, + }; + + if args.is_present("curly-quotes") { + book = book.with_curly_quotes(true); + } + + if args.is_present("open") { + book.build()?; + if let Some(d) = book.get_destination() { + open(d.join("index.html")); + } + } + + trigger_on_change(&mut book, |path, book| { + println!("File changed: {:?}\nBuilding book...\n", path); + if let Err(e) = book.build() { + println!("Error while building: {:?}", e); + } + println!(""); + }); + + Ok(()) +} + +// Calls the closure when a book source file is changed. This is blocking! +pub fn trigger_on_change(book: &mut MDBook, closure: F) -> () + where F: Fn(&Path, &mut MDBook) -> () +{ + use self::notify::RecursiveMode::*; + use self::notify::DebouncedEvent::*; + + // Create a channel to receive the events. + let (tx, rx) = channel(); + + let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) { + Ok(w) => w, + Err(e) => { + println!("Error while trying to watch the files:\n\n\t{:?}", e); + ::std::process::exit(0); + }, + }; + + // Add the source directory to the watcher + if let Err(e) = watcher.watch(book.get_source(), Recursive) { + println!("Error while watching {:?}:\n {:?}", book.get_source(), e); + ::std::process::exit(0); + }; + + // Add the theme directory to the watcher + if let Some(t) = book.get_theme_path() { + watcher.watch(t, Recursive).unwrap_or_default(); + } + + + // Add the book.{json,toml} file to the watcher if it exists, because it's not + // located in the source directory + if watcher + .watch(book.get_root().join("book.json"), NonRecursive) + .is_err() { + // do nothing if book.json is not found + } + if watcher + .watch(book.get_root().join("book.toml"), NonRecursive) + .is_err() { + // do nothing if book.toml is not found + } + + println!("\nListening for changes...\n"); + + loop { + match rx.recv() { + Ok(event) => { + match event { + NoticeWrite(path) | + NoticeRemove(path) | + Create(path) | + Write(path) | + Remove(path) | + Rename(_, path) => { + closure(&path, book); + }, + _ => {}, + } + }, + Err(e) => { + println!("An error occured: {:?}", e); + }, + } + } +}