<commit_before>// SPDX-License-Identifier: MIT
//
// Copyright (C) 2020-2022 Jason Ish

#![allow(clippy::redundant_field_names)]

use clap::{Arg, Command};
use evebox::logger;
use evebox::prelude::*;
use evebox::version;

#[tokio::main]
async fn main() {
    if let Err(err) = _main().await {
        error!("{}", err);
        std::process::exit(1);
    }
}

async fn _main() -> Result<(), Box<dyn std::error::Error>> {
    let parser = clap::Command::new("EveBox")
        .version(std::env!("CARGO_PKG_VERSION"))
        .arg(
            Arg::new("verbose")
                .long("verbose")
                .short('v')
                .multiple_occurrences(true)
                .global(true)
                .help("Increase verbosity"),
        )
        .arg(
            Arg::new("data-directory")
                .long("data-directory")
                .short('D')
                .takes_value(true)
                .value_name("DIR")
                .help("Data directory")
                .global(true),
        )
        .subcommand(clap::Command::new("version").about("Display version"));

    let sqlite_import = Command::new("sqlite-import")
        .about("Import to SQLite")
        .arg(
            Arg::new("oneshot")
                .long("oneshot")
                .help("One shot mode (exit on EOF"),
        )
        .arg(Arg::new("end").long("end").help("Start at end of file"))
        .arg(Arg::new("INPUT").required(true).index(1));

    let server = clap::Command::new("server")
        .about("EveBox Server")
        .arg(
            clap::Arg::new("config")
                .long("config")
                .short('c')
                .takes_value(true)
                .value_name("FILE")
                .help("Configuration filename"),
        )
        .arg(
            clap::Arg::new("http.host")
                .long("host")
                .value_name("HOSTNAME")
                .takes_value(true)
                .default_value("127.0.0.1")
                .help("Hostname/IP address to bind to"),
        )
        .arg(
            clap::Arg::new("http.port")
                .short('p')
                .long("port")
                .value_name("PORT")
                .takes_value(true)
                .default_value("5636")
                .help("Port to bind to"),
        )
        .arg(
            clap::Arg::new("database.elasticsearch.url")
                .short('e')
                .long("elasticsearch")
                .takes_value(true)
                .value_name("URL")
                .default_value("http://localhost:9200")
                .help("Elastic Search URL"),
        )
        .arg(
            clap::Arg::new("database.elasticsearch.index")
                .short('i')
                .long("index")
                .takes_value(true)
                .default_value("logstash")
                .value_name("INDEX")
                .help("Elastic Search index prefix"),
        )
        .arg(
            Arg::new("database.elasticsearch.no-index-suffix")
                .long("no-index-suffix")
                .takes_value(false)
                .help("Do not add a suffix to the index name"),
        )
        .arg(
            Arg::new("database.elasticsearch.ecs")
                .long("ecs")
                .help("Enable Elastic ECS support"),
        )
        .arg(
            Arg::new("database.type")
                .long("database")
                .aliases(&["datastore"])
                .takes_value(true)
                .value_name("DATABASE")
                .default_value("elasticsearch")
                .help("Database type"),
        )
        .arg(
            Arg::new("sqlite-filename")
                .long("sqlite-filename")
                .takes_value(true)
                .value_name("FILENAME")
                .default_value("events.sqlite")
                .help("SQLite events filename"),
        )
        .arg(
            clap::Arg::new("no-check-certificate")
                .short('k')
                .long("no-check-certificate")
                .help("Disable TLS certificate validation"),
        )
        .arg(
            Arg::new("http.request-logging")
                .long("http-request-logging")
                .env("EVEBOX_HTTP_REQUEST_LOGGING")
                .help("Enable HTTP access logging"),
        )
        .arg(Arg::new("http.tls.enabled").long("tls").help("Enable TLS"))
        .arg(
            Arg::new("http.tls.certificate")
                .long("tls-cert")
                .takes_value(true)
                .value_name("FILENAME")
                .help("TLS certificate filename"),
        )
        .arg(
            Arg::new("http.tls.key")
                .long("tls-key")
                .takes_value(true)
                .value_name("FILENAME")
                .help("TLS key filename"),
        )
        .arg(
            Arg::new("input.filename")
                .long("input")
                .takes_value(true)
                .value_name("FILENAME")
                .help("Input Eve file to read"),
        )
        .arg(
            Arg::new("end")
                .long("end")
                .help("Read from end (tail) of input file"),
        )
        .arg(Arg::new("input-start").long("input-start").hide(true));

    let oneshot = Command::new("oneshot")
        .about("Import a single eve.json and review in EveBox")
        .arg(
            Arg::new("limit")
                .long("limit")
                .takes_value(true)
                .help("Limit the number of events read"),
        )
        .arg(
            Arg::new("no-open")
                .long("no-open")
                .help("Don't open browser"),
        )
        .arg(
            Arg::new("no-wait")
                .long("no-wait")
                .help("Don't wait for events to load"),
        )
        .arg(
            Arg::new("database-filename")
                .long("database-filename")
                .takes_value(true)
                .default_value("./oneshot.sqlite")
                .help("Database filename"),
        )
        // --host, but keep th name as http.host to be campatible with the
        // EVEBOX_HTTP_HOST environment variable.
        .arg(
            clap::Arg::new("http.host")
                .long("host")
                .value_name("HOSTNAME")
                .takes_value(true)
                .default_value("127.0.0.1")
                .help("Hostname/IP address to bind to"),
        )
        .arg(Arg::new("INPUT").required(true).index(1));

    let elastic_import = Command::new("elastic-import")
        .alias("esimport")
        .about("Import to Elastic Search")
        .arg(
            Arg::new("config")
                .short('c')
                .long("config")
                .takes_value(true)
                .help("Configuration file"),
        )
        .arg(
            Arg::new("oneshot")
                .long("oneshot")
                .help("One shot mode (exit on EOF)"),
        )
        .arg(Arg::new("end").long("end").help("Start at end of file"))
        .arg(
            clap::Arg::new("elasticsearch")
                .short('e')
                .long("elasticsearch")
                .takes_value(true)
                .default_value("http://localhost:9200")
                .help("Elastic Search URL"),
        )
        .arg(
            Arg::new("index")
                .long("index")
                .takes_value(true)
                .default_value("logstash")
                .help("Elastic Search index prefix"),
        )
        .arg(
            Arg::new("no-index-suffix")
                .long("no-index-suffix")
                .takes_value(false)
                .help("Do not add a suffix to the index name"),
        )
        .arg(
            Arg::new("bookmark")
                .long("bookmark")
                .help("Enable bookmarking"),
        )
        .arg(
            Arg::new("bookmark-filename")
                .long("bookmark-filename")
                .takes_value(true)
                .default_value("")
                .help("Bookmark filename"),
        )
        .arg(
            Arg::new("bookmark-dir")
                .long("bookmark-dir")
                .takes_value(true)
                .default_value(".")
                .help("Bookmark directory"),
        )
        .arg(
            Arg::new("stdout")
                .long("stdout")
                .help("Print events to stdout"),
        )
        .arg(
            Arg::new("username")
                .long("username")
                .short('u')
                .takes_value(true)
                .help("Elasticsearch username"),
        )
        .arg(
            Arg::new("password")
                .long("password")
                .short('p')
                .takes_value(true)
                .help("Elasticsearch password"),
        )
        .arg(
            clap::Arg::new(evebox::commands::elastic_import::NO_CHECK_CERTIFICATE)
                .short('k')
                .long("no-check-certificate")
                .help("Disable TLS certificate validation"),
        )
        .arg(
            Arg::new("geoip.disabled")
                .long("no-geoip")
                .help("Disable GeoIP"),
        )
        .arg(
            Arg::new("geoip.database-filename")
                .long("geoip-database")
                .takes_value(true)
                .value_name("filename")
                .help("GeoIP database filename"),
        )
        .arg(
            Arg::new("input")
                .multiple_values(true)
                .value_name("INPUT")
                .index(1),
        );

    let mut parser = parser
        .subcommand(server)
        .subcommand(elastic_import)
        .subcommand(oneshot)
        .subcommand(evebox::commands::agent::command())
        .subcommand(sqlite_import)
        .subcommand(evebox::commands::config::config_subcommand())
        .subcommand(evebox::commands::print::command())
        .subcommand(elastic_debug());

    let matches = parser.clone().get_matches();

    // Initialize logging.
    let log_level = {
        let verbosity = matches.occurrences_of("verbose");
        if verbosity > 1 {
            tracing::Level::TRACE
        } else if verbosity > 0 {
            tracing::Level::DEBUG
        } else {
            tracing::Level::INFO
        }
    };
    logger::init_logger(log_level);
    logger::init_stdlog();

    let rc: anyhow::Result<()> = match matches.subcommand() {
        Some(("server", args)) => {
            evebox::server::main(args).await?;
            Ok(())
        }
        Some(("version", _)) => {
            version::print_version();
            Ok(())
        }
        Some(("sqlite-import", args)) => evebox::commands::sqlite_import::main(args).await,
        Some(("elastic-import", args)) => {
            if let Err(err) = evebox::commands::elastic_import::main(args).await {
                error!("{}", err);
                std::process::exit(1);
            }
            Ok(())
        }
        Some(("elastic-debug", args)) => evebox::commands::elastic_debug::main(args).await,
        Some(("oneshot", args)) => evebox::commands::oneshot::main(args).await,
        Some(("agent", args)) => evebox::commands::agent::main(args).await,
        Some(("config", args)) => evebox::commands::config::main(args),
        Some(("print", args)) => evebox::commands::print::main(args),
        _ => {
            parser.print_help().ok();
            println!();
            std::process::exit(1);
        }
    };
    if let Err(err) = rc {
        error!("error: {}", err);
        std::process::exit(1);
    } else {
        Ok(())
    }
}

fn elastic_debug() -> clap::Command<'static> {
    Command::new("elastic-debug").arg(
        Arg::new("elasticsearch")
            .long("elasticsearch")
            .short('e')
            .takes_value(true)
            .default_value("127.0.0.1"),
    )
}
<commit_msg>elastic-import: hide defaults in help usage<commit_after>// SPDX-License-Identifier: MIT
//
// Copyright (C) 2020-2022 Jason Ish

#![allow(clippy::redundant_field_names)]

use clap::{Arg, Command};
use evebox::logger;
use evebox::prelude::*;
use evebox::version;

#[tokio::main]
async fn main() {
    if let Err(err) = _main().await {
        error!("{}", err);
        std::process::exit(1);
    }
}

async fn _main() -> Result<(), Box<dyn std::error::Error>> {
    let parser = clap::Command::new("EveBox")
        .version(std::env!("CARGO_PKG_VERSION"))
        .arg(
            Arg::new("verbose")
                .long("verbose")
                .short('v')
                .multiple_occurrences(true)
                .global(true)
                .help("Increase verbosity"),
        )
        .arg(
            Arg::new("data-directory")
                .long("data-directory")
                .short('D')
                .takes_value(true)
                .value_name("DIR")
                .help("Data directory")
                .global(true),
        )
        .subcommand(clap::Command::new("version").about("Display version"));

    let sqlite_import = Command::new("sqlite-import")
        .about("Import to SQLite")
        .arg(
            Arg::new("oneshot")
                .long("oneshot")
                .help("One shot mode (exit on EOF"),
        )
        .arg(Arg::new("end").long("end").help("Start at end of file"))
        .arg(Arg::new("INPUT").required(true).index(1));

    let server = clap::Command::new("server")
        .about("EveBox Server")
        .arg(
            clap::Arg::new("config")
                .long("config")
                .short('c')
                .takes_value(true)
                .value_name("FILE")
                .help("Configuration filename"),
        )
        .arg(
            clap::Arg::new("http.host")
                .long("host")
                .value_name("HOSTNAME")
                .takes_value(true)
                .default_value("127.0.0.1")
                .help("Hostname/IP address to bind to"),
        )
        .arg(
            clap::Arg::new("http.port")
                .short('p')
                .long("port")
                .value_name("PORT")
                .takes_value(true)
                .default_value("5636")
                .help("Port to bind to"),
        )
        .arg(
            clap::Arg::new("database.elasticsearch.url")
                .short('e')
                .long("elasticsearch")
                .takes_value(true)
                .value_name("URL")
                .default_value("http://localhost:9200")
                .help("Elastic Search URL"),
        )
        .arg(
            clap::Arg::new("database.elasticsearch.index")
                .short('i')
                .long("index")
                .takes_value(true)
                .default_value("logstash")
                .value_name("INDEX")
                .help("Elastic Search index prefix"),
        )
        .arg(
            Arg::new("database.elasticsearch.no-index-suffix")
                .long("no-index-suffix")
                .takes_value(false)
                .help("Do not add a suffix to the index name"),
        )
        .arg(
            Arg::new("database.elasticsearch.ecs")
                .long("ecs")
                .help("Enable Elastic ECS support"),
        )
        .arg(
            Arg::new("database.type")
                .long("database")
                .aliases(&["datastore"])
                .takes_value(true)
                .value_name("DATABASE")
                .default_value("elasticsearch")
                .help("Database type"),
        )
        .arg(
            Arg::new("sqlite-filename")
                .long("sqlite-filename")
                .takes_value(true)
                .value_name("FILENAME")
                .default_value("events.sqlite")
                .help("SQLite events filename"),
        )
        .arg(
            clap::Arg::new("no-check-certificate")
                .short('k')
                .long("no-check-certificate")
                .help("Disable TLS certificate validation"),
        )
        .arg(
            Arg::new("http.request-logging")
                .long("http-request-logging")
                .env("EVEBOX_HTTP_REQUEST_LOGGING")
                .help("Enable HTTP access logging"),
        )
        .arg(Arg::new("http.tls.enabled").long("tls").help("Enable TLS"))
        .arg(
            Arg::new("http.tls.certificate")
                .long("tls-cert")
                .takes_value(true)
                .value_name("FILENAME")
                .help("TLS certificate filename"),
        )
        .arg(
            Arg::new("http.tls.key")
                .long("tls-key")
                .takes_value(true)
                .value_name("FILENAME")
                .help("TLS key filename"),
        )
        .arg(
            Arg::new("input.filename")
                .long("input")
                .takes_value(true)
                .value_name("FILENAME")
                .help("Input Eve file to read"),
        )
        .arg(
            Arg::new("end")
                .long("end")
                .help("Read from end (tail) of input file"),
        )
        .arg(Arg::new("input-start").long("input-start").hide(true));

    let oneshot = Command::new("oneshot")
        .about("Import a single eve.json and review in EveBox")
        .arg(
            Arg::new("limit")
                .long("limit")
                .takes_value(true)
                .help("Limit the number of events read"),
        )
        .arg(
            Arg::new("no-open")
                .long("no-open")
                .help("Don't open browser"),
        )
        .arg(
            Arg::new("no-wait")
                .long("no-wait")
                .help("Don't wait for events to load"),
        )
        .arg(
            Arg::new("database-filename")
                .long("database-filename")
                .takes_value(true)
                .default_value("./oneshot.sqlite")
                .help("Database filename"),
        )
        // --host, but keep th name as http.host to be campatible with the
        // EVEBOX_HTTP_HOST environment variable.
        .arg(
            clap::Arg::new("http.host")
                .long("host")
                .value_name("HOSTNAME")
                .takes_value(true)
                .default_value("127.0.0.1")
                .help("Hostname/IP address to bind to"),
        )
        .arg(Arg::new("INPUT").required(true).index(1));

    let elastic_import = Command::new("elastic-import")
        .about("Import to Elastic Search")
        .arg(
            Arg::new("config")
                .short('c')
                .long("config")
                .takes_value(true)
                .help("Configuration file"),
        )
        .arg(
            Arg::new("oneshot")
                .long("oneshot")
                .help("One shot mode (exit on EOF)"),
        )
        .arg(Arg::new("end").long("end").help("Start at end of file"))
        .arg(
            clap::Arg::new("elasticsearch")
                .short('e')
                .long("elasticsearch")
                .takes_value(true)
                .default_value("http://localhost:9200")
                .hide_default_value(true)
                .help("Elastic Search URL"),
        )
        .arg(
            Arg::new("index")
                .long("index")
                .takes_value(true)
                .default_value("logstash")
                .hide_default_value(true)
                .help("Elastic Search index prefix"),
        )
        .arg(
            Arg::new("no-index-suffix")
                .long("no-index-suffix")
                .takes_value(false)
                .help("Do not add a suffix to the index name"),
        )
        .arg(
            Arg::new("bookmark")
                .long("bookmark")
                .help("Enable bookmarking"),
        )
        .arg(
            Arg::new("bookmark-filename")
                .long("bookmark-filename")
                .takes_value(true)
                .default_value("")
                .hide_default_value(true)
                .help("Bookmark filename"),
        )
        .arg(
            Arg::new("bookmark-dir")
                .long("bookmark-dir")
                .takes_value(true)
                .default_value(".")
                .hide_default_value(true)
                .help("Bookmark directory"),
        )
        .arg(
            Arg::new("stdout")
                .long("stdout")
                .help("Print events to stdout"),
        )
        .arg(
            Arg::new("username")
                .long("username")
                .short('u')
                .takes_value(true)
                .help("Elasticsearch username"),
        )
        .arg(
            Arg::new("password")
                .long("password")
                .short('p')
                .takes_value(true)
                .help("Elasticsearch password"),
        )
        .arg(
            clap::Arg::new(evebox::commands::elastic_import::NO_CHECK_CERTIFICATE)
                .short('k')
                .long("no-check-certificate")
                .help("Disable TLS certificate validation"),
        )
        .arg(
            Arg::new("geoip.disabled")
                .long("no-geoip")
                .help("Disable GeoIP"),
        )
        .arg(
            Arg::new("geoip.database-filename")
                .long("geoip-database")
                .takes_value(true)
                .value_name("filename")
                .help("GeoIP database filename"),
        )
        .arg(
            Arg::new("input")
                .multiple_values(true)
                .value_name("INPUT")
                .index(1),
        );

    let mut parser = parser
        .subcommand(server)
        .subcommand(elastic_import)
        .subcommand(oneshot)
        .subcommand(evebox::commands::agent::command())
        .subcommand(sqlite_import)
        .subcommand(evebox::commands::config::config_subcommand())
        .subcommand(evebox::commands::print::command())
        .subcommand(elastic_debug());

    let matches = parser.clone().get_matches();

    // Initialize logging.
    let log_level = {
        let verbosity = matches.occurrences_of("verbose");
        if verbosity > 1 {
            tracing::Level::TRACE
        } else if verbosity > 0 {
            tracing::Level::DEBUG
        } else {
            tracing::Level::INFO
        }
    };
    logger::init_logger(log_level);
    logger::init_stdlog();

    let rc: anyhow::Result<()> = match matches.subcommand() {
        Some(("server", args)) => {
            evebox::server::main(args).await?;
            Ok(())
        }
        Some(("version", _)) => {
            version::print_version();
            Ok(())
        }
        Some(("sqlite-import", args)) => evebox::commands::sqlite_import::main(args).await,
        Some(("elastic-import", args)) => {
            if let Err(err) = evebox::commands::elastic_import::main(args).await {
                error!("{}", err);
                std::process::exit(1);
            }
            Ok(())
        }
        Some(("elastic-debug", args)) => evebox::commands::elastic_debug::main(args).await,
        Some(("oneshot", args)) => evebox::commands::oneshot::main(args).await,
        Some(("agent", args)) => evebox::commands::agent::main(args).await,
        Some(("config", args)) => evebox::commands::config::main(args),
        Some(("print", args)) => evebox::commands::print::main(args),
        _ => {
            parser.print_help().ok();
            println!();
            std::process::exit(1);
        }
    };
    if let Err(err) = rc {
        error!("error: {}", err);
        std::process::exit(1);
    } else {
        Ok(())
    }
}

fn elastic_debug() -> clap::Command<'static> {
    Command::new("elastic-debug").arg(
        Arg::new("elasticsearch")
            .long("elasticsearch")
            .short('e')
            .takes_value(true)
            .default_value("127.0.0.1"),
    )
}
<|endoftext|>


<commit_before>#[macro_use]
extern crate log;
extern crate hubris;
extern crate env_logger;
extern crate rustc_serialize;
extern crate docopt;

use docopt::Docopt;
use std::path::PathBuf;

const USAGE: &'static str = r#"
Hubris, version 0.0.1.

  ___ ___      ___.         .__
 /   |   \ __ _\_ |_________|__| ______
/    ~    \  |  \ __ \_  __ \  |/  ___/
\    Y    /  |  / \_\ \  | \/  |\___ \
 \___|_  /|____/|___  /__|  |__/____  >
       \/           \/              \/

Usage:
    hubris repl [<file>]
    hubris server
    hubris <file> [--output=<exe>]
    hubris (-h | --help)
    hubris --version

Options:
    -h --help    Show this screen.
    --version    Show version.
"#;

#[derive(Debug, RustcDecodable)]
struct Args {
    arg_file: Option<String>,
    flag_output: Option<String>,
    cmd_server: bool,
    cmd_repl: bool,
}

fn main() {
    env_logger::init().unwrap();

    let args: Args = Docopt::new(USAGE)
                         .and_then(|d| d.decode())
                         .unwrap_or_else(|e| e.exit());

    if args.cmd_server {
        println!("Starting Server...");
        hubris::server::server_main();
    } else if args.cmd_repl {
        // verify file exists
        let pb = args.arg_file.map(|f| {
            let pb = PathBuf::from(f);
            if !pb.is_file() {
                panic!("Hubris: file {} does not exist", pb.to_str().unwrap());
            }
            pb
        });
        let repl = hubris::repl::Repl::from_path(&pb).expect("Launching repl failed");
        repl.start().expect("Starting repl failed");
    } else {
        let input = args.arg_file.expect("No input files");
        debug!("main: compiling {} output to {:?}",
               &input[..],
               args.flag_output);

        let result = hubris::compile_file(&input[..],
                                          args.flag_output.map(|p| PathBuf::from(p)));

        match result {
            Err(e) => panic!("failed with {:?}", e),
            Ok(_) => {}
        }
    }
}
<commit_msg>make version flag work<commit_after>#[macro_use]
extern crate log;
extern crate hubris;
extern crate env_logger;
extern crate rustc_serialize;
extern crate docopt;

use docopt::Docopt;
use std::path::PathBuf;

const USAGE: &'static str = r#"
Hubris, version 0.0.1.

  ___ ___      ___.         .__
 /   |   \ __ _\_ |_________|__| ______
/    ~    \  |  \ __ \_  __ \  |/  ___/
\    Y    /  |  / \_\ \  | \/  |\___ \
 \___|_  /|____/|___  /__|  |__/____  >
       \/           \/              \/

Usage:
    hubris repl [<file>]
    hubris server
    hubris <file> [--output=<exe>]
    hubris (-h | --help)
    hubris --version

Options:
    -h --help    Show this screen.
    --version    Show version.
"#;

#[derive(Debug, RustcDecodable)]
struct Args {
    arg_file: Option<String>,
    flag_output: Option<String>,
    flag_version: bool,
    cmd_server: bool,
    cmd_repl: bool,
}

fn main() {
    env_logger::init().unwrap();

    let args: Args = Docopt::new(USAGE)
                         .and_then(|d| d.decode())
                         .unwrap_or_else(|e| e.exit());

    if args.flag_version {
        println!("hubris 0.0.1.0 (we are trying)");
    } else if args.cmd_server {
        println!("Starting Server...");
        hubris::server::server_main();
    } else if args.cmd_repl {
        // verify file exists
        let pb = args.arg_file.map(|f| {
            let pb = PathBuf::from(f);
            if !pb.is_file() {
                panic!("Hubris: file {} does not exist", pb.to_str().unwrap());
            }
            pb
        });
        let repl = hubris::repl::Repl::from_path(&pb).expect("Launching repl failed");
        repl.start().expect("Starting repl failed");
    } else {
        let input = args.arg_file.expect("No input files");
        debug!("main: compiling {} output to {:?}",
               &input[..],
               args.flag_output);

        let result = hubris::compile_file(&input[..],
                                          args.flag_output.map(|p| PathBuf::from(p)));

        match result {
            Err(e) => panic!("failed with {:?}", e),
            Ok(_) => {}
        }
    }
}
<|endoftext|>


<commit_before>#[macro_use]
extern crate mdbook;
#[macro_use]
extern crate clap;
extern crate crossbeam;

// Dependencies for the Watch feature
#[cfg(feature = "watch")]
extern crate notify;
#[cfg(feature = "watch")]
extern crate time;

use std::env;
use std::error::Error;
use std::io::{self, Write};
use std::path::{Path, PathBuf};

use clap::{App, ArgMatches, SubCommand};

// Uses for the Watch feature
#[cfg(feature = "watch")]
use notify::Watcher;
#[cfg(feature = "watch")]
use std::sync::mpsc::channel;

use mdbook::MDBook;

const NAME: &'static str = "mdbook";

fn main() {
    // 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 <mathieudavid@mathieudavid.org>")
                    // Get the version from our Cargo.toml using clap's crate_version!() macro
                    .version(&*format!("v{}", crate_version!()))
                    .subcommand_required(true)
                    .after_help("For more information about a specific command, try `mdbook <command> --help`")
                    .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 ommitted)'")
                        .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("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
                    .subcommand(SubCommand::with_name("watch")
                        .about("Watch the files for changes")
                        .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
                    .subcommand(SubCommand::with_name("test")
                        .about("Test that code samples compile"))
                    .get_matches();

    // 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),
        #[cfg(feature = "watch")]
        ("watch", Some(sub_matches)) => watch(sub_matches),
        ("test", Some(sub_matches)) => test(sub_matches),
        (_, _)                       => unreachable!()
    };

    if let Err(e) = res {
        writeln!(&mut io::stderr(), "An error occured:\n{}", e).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
    }
}

// Init command implementation
fn init(args: &ArgMatches) -> Result<(), Box<Error>> {

    let book_dir = get_book_dir(args);
    let mut book = MDBook::new(&book_dir);

    // Call the function that does the initialization
    try!(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_src());
            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
        try!(book.copy_theme());
        println!("\nTheme copied.");

    }

    // Because of `src/book/mdbook.rs#L37-L39`, the following will always evaluate to `true`
    let is_dest_inside_root = book.get_dest().starts_with(book.get_root());

    if !args.is_present("force") && is_dest_inside_root {
        let gitignore = book.get_dest().join(".gitignore");
        println!("\nCreating default .gitignore at {:?}", gitignore);
        print!("\nAre you sure you want to continue? (y/n) ");

        if confirm() {
            book.create_gitignore();
            println!("\n.gitignore created.");
        } else {
            println!("\nSkipping...\n");
        }
    }

    println!("\nAll done, no errors...");

    Ok(())
}

// Build command implementation
fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
    let book_dir = get_book_dir(args);
    let mut book = MDBook::new(&book_dir).read_config();

    try!(book.build());

    Ok(())
}

// Watch command implementation
#[cfg(feature = "watch")]
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
    let book_dir = get_book_dir(args);
    let book = MDBook::new(&book_dir).read_config();

    // Create a channel to receive the events.
     let (tx, rx) = channel();

     let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);

     match w {
         Ok(mut watcher) => {

             // Add the source directory to the watcher
             if let Err(e) = watcher.watch(book.get_src()) {
                 println!("Error while watching {:?}:\n    {:?}", book.get_src(), e);
                 ::std::process::exit(0);
             };

             // Add the book.json file to the watcher if it exists, because it's not
             // located in the source directory
             if let Err(_) = watcher.watch(book_dir.join("book.json")) {
                 // do nothing if book.json is not found
             }

             let previous_time = time::get_time().sec;

             crossbeam::scope(|scope| {
                 loop {
                     match rx.recv() {
                         Ok(event) => {

                             // Skip the event if an event has already been issued in the last second
                             if time::get_time().sec - previous_time < 1 { continue }

                             if let Some(path) = event.path {
                                 // Trigger the build process in a new thread (to keep receiving events)
                                 scope.spawn(move || {
                                     println!("File changed: {:?}\nBuilding book...\n", path);
                                     match build(args) {
                                         Err(e) => println!("Error while building: {:?}", e),
                                         _ => {}
                                     }
                                     println!("");
                                 });

                             } else {
                                 continue;
                             }
                         },
                         Err(e) => {
                             println!("An error occured: {:?}", e);
                         }
                     }
                 }
             });

         },
         Err(e) => {
             println!("Error while trying to watch the files:\n\n\t{:?}", e);
             ::std::process::exit(0);
         }
     }

     Ok(())
 }

fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
    let book_dir = get_book_dir(args);
    let mut book = MDBook::new(&book_dir).read_config();

    try!(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...
        let p = Path::new(dir);
        if p.is_relative() {
           env::current_dir().unwrap().join(dir)
        } else {
           p.to_path_buf()
        }
    } else {
        env::current_dir().unwrap()
    }
}
<commit_msg>Modify wording of confirmation request before creation of .gitignore<commit_after>#[macro_use]
extern crate mdbook;
#[macro_use]
extern crate clap;
extern crate crossbeam;

// Dependencies for the Watch feature
#[cfg(feature = "watch")]
extern crate notify;
#[cfg(feature = "watch")]
extern crate time;

use std::env;
use std::error::Error;
use std::io::{self, Write};
use std::path::{Path, PathBuf};

use clap::{App, ArgMatches, SubCommand};

// Uses for the Watch feature
#[cfg(feature = "watch")]
use notify::Watcher;
#[cfg(feature = "watch")]
use std::sync::mpsc::channel;

use mdbook::MDBook;

const NAME: &'static str = "mdbook";

fn main() {
    // 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 <mathieudavid@mathieudavid.org>")
                    // Get the version from our Cargo.toml using clap's crate_version!() macro
                    .version(&*format!("v{}", crate_version!()))
                    .subcommand_required(true)
                    .after_help("For more information about a specific command, try `mdbook <command> --help`")
                    .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 ommitted)'")
                        .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("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
                    .subcommand(SubCommand::with_name("watch")
                        .about("Watch the files for changes")
                        .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
                    .subcommand(SubCommand::with_name("test")
                        .about("Test that code samples compile"))
                    .get_matches();

    // 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),
        #[cfg(feature = "watch")]
        ("watch", Some(sub_matches)) => watch(sub_matches),
        ("test", Some(sub_matches)) => test(sub_matches),
        (_, _)                       => unreachable!()
    };

    if let Err(e) = res {
        writeln!(&mut io::stderr(), "An error occured:\n{}", e).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
    }
}

// Init command implementation
fn init(args: &ArgMatches) -> Result<(), Box<Error>> {

    let book_dir = get_book_dir(args);
    let mut book = MDBook::new(&book_dir);

    // Call the function that does the initialization
    try!(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_src());
            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
        try!(book.copy_theme());
        println!("\nTheme copied.");

    }

    // Because of `src/book/mdbook.rs#L37-L39`, the following will always evaluate to `true`
    let is_dest_inside_root = book.get_dest().starts_with(book.get_root());

    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<(), Box<Error>> {
    let book_dir = get_book_dir(args);
    let mut book = MDBook::new(&book_dir).read_config();

    try!(book.build());

    Ok(())
}

// Watch command implementation
#[cfg(feature = "watch")]
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
    let book_dir = get_book_dir(args);
    let book = MDBook::new(&book_dir).read_config();

    // Create a channel to receive the events.
     let (tx, rx) = channel();

     let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);

     match w {
         Ok(mut watcher) => {

             // Add the source directory to the watcher
             if let Err(e) = watcher.watch(book.get_src()) {
                 println!("Error while watching {:?}:\n    {:?}", book.get_src(), e);
                 ::std::process::exit(0);
             };

             // Add the book.json file to the watcher if it exists, because it's not
             // located in the source directory
             if let Err(_) = watcher.watch(book_dir.join("book.json")) {
                 // do nothing if book.json is not found
             }

             let previous_time = time::get_time().sec;

             crossbeam::scope(|scope| {
                 loop {
                     match rx.recv() {
                         Ok(event) => {

                             // Skip the event if an event has already been issued in the last second
                             if time::get_time().sec - previous_time < 1 { continue }

                             if let Some(path) = event.path {
                                 // Trigger the build process in a new thread (to keep receiving events)
                                 scope.spawn(move || {
                                     println!("File changed: {:?}\nBuilding book...\n", path);
                                     match build(args) {
                                         Err(e) => println!("Error while building: {:?}", e),
                                         _ => {}
                                     }
                                     println!("");
                                 });

                             } else {
                                 continue;
                             }
                         },
                         Err(e) => {
                             println!("An error occured: {:?}", e);
                         }
                     }
                 }
             });

         },
         Err(e) => {
             println!("Error while trying to watch the files:\n\n\t{:?}", e);
             ::std::process::exit(0);
         }
     }

     Ok(())
 }

fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
    let book_dir = get_book_dir(args);
    let mut book = MDBook::new(&book_dir).read_config();

    try!(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...
        let p = Path::new(dir);
        if p.is_relative() {
           env::current_dir().unwrap().join(dir)
        } else {
           p.to_path_buf()
        }
    } else {
        env::current_dir().unwrap()
    }
}
<|endoftext|>


<commit_before>use piece::Piece;
use square::Square;
use std::fmt;
use std::cmp::Ordering;

/// Represent a ChessMove in memory
#[derive(Clone, Copy, Eq, PartialOrd, PartialEq)]
pub struct ChessMove {
    source: Square,
    dest: Square,
    promotion: Option<Piece>
}

impl ChessMove {
    /// Create a new chess move, given a source `Square`, a destination `Square`, and an optional
    /// promotion `Piece`
    pub fn new(source: Square, dest: Square, promotion: Option<Piece>) -> ChessMove {
        ChessMove { source: source, dest: dest, promotion: promotion }
    }

    /// Get the source square (square the piece is currently on).
    pub fn get_source(&self) -> Square {
        self.source
    }

    /// Get the destination square (square the piece is going to).
    pub fn get_dest(&self) -> Square {
        self.dest
    }

    /// Get the promotion piece (maybe).
    pub fn get_promotion(&self) -> Option<Piece> {
        self.promotion
    }
}

impl fmt::Display for ChessMove {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.promotion {
            None => write!(f, "{}-{}", self.source, self.dest),
            Some(x) => write!(f, "{}-{}=?", self.source, self.dest)
        }
    }
}

impl Ord for ChessMove {
    fn cmp(&self, other: &ChessMove) -> Ordering {
        if self.source != other.source {
            self.source.cmp(&other.source)
        } else if self.dest != other.dest {
            self.dest.cmp(&other.dest)
        } else if self.promotion != other.promotion {
            match self.promotion {
                None => Ordering::Less,
                Some(x) => {
                    match other.promotion {
                        None => Ordering::Greater,
                        Some(y) => x.cmp(&y)
                    }
                }
            }
        } else {
            Ordering::Equal
        }
    }
}

<commit_msg>Finish fmt::Display for chess_move<commit_after>use piece::Piece;
use square::Square;
use std::fmt;
use std::cmp::Ordering;

/// Represent a ChessMove in memory
#[derive(Clone, Copy, Eq, PartialOrd, PartialEq)]
pub struct ChessMove {
    source: Square,
    dest: Square,
    promotion: Option<Piece>
}

impl ChessMove {
    /// Create a new chess move, given a source `Square`, a destination `Square`, and an optional
    /// promotion `Piece`
    pub fn new(source: Square, dest: Square, promotion: Option<Piece>) -> ChessMove {
        ChessMove { source: source, dest: dest, promotion: promotion }
    }

    /// Get the source square (square the piece is currently on).
    pub fn get_source(&self) -> Square {
        self.source
    }

    /// Get the destination square (square the piece is going to).
    pub fn get_dest(&self) -> Square {
        self.dest
    }

    /// Get the promotion piece (maybe).
    pub fn get_promotion(&self) -> Option<Piece> {
        self.promotion
    }
}

impl fmt::Display for ChessMove {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.promotion {
            None => write!(f, "{}-{}", self.source, self.dest),
            Some(x) => write!(f, "{}-{}={}", self.source, self.dest, x)
        }
    }
}

impl Ord for ChessMove {
    fn cmp(&self, other: &ChessMove) -> Ordering {
        if self.source != other.source {
            self.source.cmp(&other.source)
        } else if self.dest != other.dest {
            self.dest.cmp(&other.dest)
        } else if self.promotion != other.promotion {
            match self.promotion {
                None => Ordering::Less,
                Some(x) => {
                    match other.promotion {
                        None => Ordering::Greater,
                        Some(y) => x.cmp(&y)
                    }
                }
            }
        } else {
            Ordering::Equal
        }
    }
}

<|endoftext|>


<commit_before>#[macro_use] extern crate error_chain;
#[macro_use] extern crate prettytable;
#[macro_use] extern crate stderr;
extern crate chan;
extern crate clap;
extern crate filetime;
extern crate globset;
extern crate pretty_bytes;
extern crate separator;

#[macro_use] mod errors;
mod compression;
mod lists;
mod structs;

use clap::{App, Arg};
use errors::*;
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
use lists::*;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::mpsc;
use structs::*;

const DEBUG_FILTERS: bool = cfg!(debug_assertions);
fn debug(message: &str) {
    if DEBUG_FILTERS {
        errstln!("{}", message);
    }
}

quick_main!(run);

fn run() -> Result<()> {
    let matches = App::new("static-compress")
        .version("0.3.2")
        .about("Create statically-compresed copies of matching files")
        .author("NeoSmart Technologies")
        .arg(Arg::with_name("compressor")
            .short("c")
            .long("compressor")
            .value_name("[brotli|gzip|zopfli|webp]")
            .help("The compressor to use (default: gzip)")
            .takes_value(true))
        .arg(Arg::with_name("threads")
            .short("j")
            .long("threads")
            .value_name("COUNT")
            .help("The number of simultaneous compressions (default: 1)")
            .takes_value(true))
        .arg(Arg::with_name("filters")
            .value_name("FILTER")
            .multiple(true)
            .required(true))
        .arg(Arg::with_name("ext")
            .short("e")
            .value_name("EXT")
            .long("extension")
            .help("The extension to use for compressed files (default: gz, br, or webp)"))
        .arg(Arg::with_name("quality")
             .short("q")
             .long("quality")
             .takes_value(true)
             .help("A quality parameter to be passed to the encoder. Algorithm-specific."))
        /*.arg(Arg::with_name("excludes")
            .short("x")
            .value_name("FILTER")
            .long("exclude")
            .multiple(true)
            .help("Exclude files matching this glob expression"))*/
        .get_matches();

    fn get_parameter<'a, T>(matches: &clap::ArgMatches, name: &'static str, default_value: T) -> Result<T>
        where T: std::str::FromStr
    {
        match matches.value_of(name) {
            Some(v) => {
                Ok(v.parse().map_err(|_| ErrorKind::InvalidParameterValue(name))?)
            }
            None => Ok(default_value),
        }
    }

    let case_sensitive = !matches.is_present("nocase");
    let compressor = get_parameter(&matches, "compressor", CompressionAlgorithm::GZip)?;
    let temp = Parameters {
        extension: matches.value_of("ext")
            .unwrap_or(compressor.extension())
            .trim_matches(|c: char| c.is_whitespace() || c.is_control() || c == '.')
            .to_owned(),
        compressor: compressor,
        quality: match matches.value_of("quality") {
            Some(q) => Some(q.parse::<u8>().map_err(|_| ErrorKind::InvalidParameterValue("quality"))?),
            None => None
        },
        threads: get_parameter(&matches, "threads", 1)?,
    };

    /*let exclude_filters = match matches.values_of("exclude") {
        Some(values)=> values.map(|s| s.to_owned()).collect(),
        None => Vec::<String>::new(),
    };*/

    let parameters = Arc::<Parameters>::new(temp);
    let (send_queue, stats_rx, wait_group) = start_workers(&parameters);

    let mut include_filters: Vec<String> = match matches.values_of("filters") {
        Some(values) => Ok(values.map(|s| s.to_owned()).collect()),
        None => Err(ErrorKind::InvalidUsage),
    }?;

    let mut builder = GlobSetBuilder::new();
    fix_filters(&mut include_filters);
    for filter in include_filters.iter() {
        let glob = GlobBuilder::new(filter)
            .case_insensitive(!case_sensitive)
            .literal_separator(true)
            .build().map_err(|_| ErrorKind::InvalidIncludeFilter)?;
        builder.add(glob);
    }
    let globset = builder.build().map_err(|_| ErrorKind::InvalidIncludeFilter)?;

    //convert filters to paths and deal out conversion jobs

    dispatch_jobs(send_queue, include_filters, globset/*, exclude_filters*/)?;

    //wait for all jobs to finish
    wait_group.wait();

    //merge statistics from all threads
    let mut stats = Statistics::new();
    while let Ok(thread_stats) = stats_rx.recv() {
        stats.merge(&thread_stats);
    }

    println!("{}", stats);

    Ok(())
}

type ThreadParam = std::path::PathBuf;

fn start_workers<'a>(params: &Arc<Parameters>) -> (chan::Sender<ThreadParam>, mpsc::Receiver<Statistics>, chan::WaitGroup) {
    let (tx, rx) = chan::sync::<ThreadParam>(params.threads);
    let (stats_tx, stats_rx) = std::sync::mpsc::channel::<Statistics>();
    let wg = chan::WaitGroup::new();

    for _ in 0..params.threads {
        let local_params = params.clone();
        let local_rx = rx.clone();
        let local_stats_tx = stats_tx.clone();
        let local_wg = wg.clone();
        wg.add(1);
        std::thread::spawn(move || {
            worker_thread(local_params, local_stats_tx, local_rx);
            local_wg.done();
        });
    }

    (tx, stats_rx, wg)
}

fn yield_file<F>(path: PathBuf, globset: &GlobSet, callback: &F) -> Result<()>
    where F: Fn(PathBuf) -> Result<()>
{
    if is_hidden(&path)? {
        //we are ignoring .files and .directories
        //we may add a command-line switch to control this behavior in the future
        return Ok(());
    }

    if path.is_dir() {
        for child in path.read_dir()? {
            let child_path = child?.path();
            yield_file(child_path, globset, callback)?;
        }
    }
    else {
        //I'm presuming the binary search in is_blacklisted is faster
        //than globset.is_match, but we should benchmark it at some point
        if !is_blacklisted(&path)? && globset.is_match(&path) {
            callback(path)?;
        }
    }

    Ok(())
}

fn dispatch_jobs(send_queue: chan::Sender<ThreadParam>, filters: Vec<String>, globset: GlobSet/*, exclude_filters: Vec<String>*/) -> Result<()> {
    let paths = extract_paths(&filters)?;
    for path in paths {
        yield_file(path, &globset, &|path: PathBuf| {
            send_queue.send(path);
            Ok(())
        })?
    }

    Ok(())
}

fn worker_thread(params: Arc<Parameters>, stats_tx: mpsc::Sender<Statistics>, rx: chan::Receiver<ThreadParam>) {
    let mut local_stats = Statistics::new();

    loop {
        let src = match rx.recv() {
            Some(task) => task,
            None => break, //no more tasks
        };

        //in a nested function so we can handle errors centrally
        fn compress_single(src: &ThreadParam, params: &Parameters, mut local_stats: &mut Statistics) -> Result<()> {
            let dst_path = format!("{}.{}",
                                   src.to_str().ok_or(ErrorKind::InvalidCharactersInPath)?,
                                   params.extension);
            let dst = Path::new(&dst_path);

            //again, in a scope for error handling
            |local_stats: &mut Statistics| -> Result<()> {
                    println!("{}", src.to_string_lossy());
                    let src_metadata = std::fs::metadata(src)?;

                    //don't compress files that are already compressed that haven't changed
                    if let Ok(dst_metadata) = std::fs::metadata(dst) {
                        //the destination already exists
                        let src_seconds = src_metadata.modified()?.duration_since(std::time::UNIX_EPOCH)?.as_secs();
                        let dst_seconds = dst_metadata.modified()?.duration_since(std::time::UNIX_EPOCH)?.as_secs();
                        match src_seconds == dst_seconds {
                            true => {
                                local_stats.update(src_metadata.len(), dst_metadata.len(), false);
                                return Ok(());//no need to recompress
                            },
                            false => {
                                std::fs::remove_file(dst)?; //throw if we can't
                            }
                        };
                    }
                    params.compressor.compress(src.as_path(), dst, params.quality)?;
                    let dst_metadata = std::fs::metadata(dst)?;
                    local_stats.update(src_metadata.len(), dst_metadata.len(), true);
                    let src_modified = filetime::FileTime::from_last_modification_time(&src_metadata);
                    filetime::set_file_times(dst, filetime::FileTime::zero(), src_modified).unwrap_or_default();

                    Ok(())
                }(&mut local_stats)
                .map_err(|e| {
                    //try deleting the invalid destination file, but don't care if we can't
                    std::fs::remove_file(dst).unwrap_or_default();
                    e //return the same error
                })
        }

        if let Err(e) = compress_single(&src, &params, &mut local_stats) {
            errstln!("Error compressing {}: {}", src.to_string_lossy(), e);
        }
    }

    if !stats_tx.send(local_stats).is_ok() {
        errstln!("Error compiling statistics!");
    }
}

fn str_search(sorted: &[&str], search_term: &str, case_sensitive: bool) -> std::result::Result<usize, usize> {
    let term = match case_sensitive {
        true => search_term.to_owned(),
        false => search_term.to_lowercase(),
    };

    sorted.binary_search_by(|probe| probe.cmp(&&*term))
}

fn is_hidden(path: &Path) -> Result<bool> {
    let hidden = match path.file_name() {
        Some(x) => x.to_str().ok_or(ErrorKind::InvalidCharactersInPath)?
            .starts_with("."),
        None => false
    };
    Ok(hidden)
}

fn is_blacklisted(path: &Path) -> Result<bool> {
    let r = match path.extension() {
        Some(x) => {
            let ext = x.to_str().ok_or(ErrorKind::InvalidCharactersInPath)?;
            str_search(COMP_EXTS, &ext, false).is_ok()
        },
        None => false,
    };

    return Ok(r);
}

//pre-pends ./ to relative paths
fn fix_filters(mut filters: &mut Vec<String>) {
    for i in 0..filters.len() {
        let new_path;
        {
            let ref path = filters[i];
            match path.chars().next().expect("Received blank filter!") {
                '.' | '/' => continue,
                _ => new_path = format!("./{}", path) //un-prefixed path
            }
        }
        filters[i] = new_path;
    }
}

//Given a list of filters, extracts the directories that should be searched
//To-Do: Also provide info about to what depth they should be recursed
use std::collections::HashSet;
fn extract_paths(filters: &Vec<String>) -> Result<HashSet<PathBuf>> {
    use std::iter::FromIterator;

    let mut dirs = std::collections::HashSet::<PathBuf>::new();

    {
        let insert_path = &mut |filter: &String, dir: PathBuf| {
            debug(&format!("filter {} mapped to search {}", filter, dir.display()));
            dirs.insert(dir);
        };

        for filter in filters {
            //take everything until the first expression
            let mut last_char = None::<char>;
            let dir;
            {
                let partial = filter.chars().take_while(|c| match c {
                    &'?' | &'*' | &'{' | &'[' => false,
                    c => { last_char = Some(c.clone()); true }
                });
                dir = String::from_iter(partial);
            }

            let dir = match dir.chars().next() {
                Some(c) => match c {
                    '.' | '/' => PathBuf::from(dir),
                    _ => {
                        let mut pb = PathBuf::from("./");
                        pb.push(dir);
                        pb
                    }
                },
                None => {
                    insert_path(filter, PathBuf::from("./"));
                    continue;
                }
            };

            if dir.to_str().ok_or(ErrorKind::InvalidCharactersInPath)?.ends_with(filter) {
                //the "dir" is actually a full path to a single file
                //return it as-is
                insert_path(filter, dir);
                continue;
            }

            if last_char == Some('/') {
                //dir is a already a directory, return it as-is
                insert_path(filter, dir);
                continue;
            }

            //we need to extract the directory from the path we have
            let dir = match PathBuf::from(dir).parent() {
                Some(parent) => parent.to_path_buf(),
                None => PathBuf::from("./"),
            };

            insert_path(filter, dir);
        }
    }

    debug(&format!("final search paths: {:?}", dirs));

    Ok(dirs)
}
<commit_msg>Only print names of files that are being processed<commit_after>#[macro_use] extern crate error_chain;
#[macro_use] extern crate prettytable;
#[macro_use] extern crate stderr;
extern crate chan;
extern crate clap;
extern crate filetime;
extern crate globset;
extern crate pretty_bytes;
extern crate separator;

#[macro_use] mod errors;
mod compression;
mod lists;
mod structs;

use clap::{App, Arg};
use errors::*;
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
use lists::*;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::mpsc;
use structs::*;

const DEBUG_FILTERS: bool = cfg!(debug_assertions);
fn debug(message: &str) {
    if DEBUG_FILTERS {
        errstln!("{}", message);
    }
}

quick_main!(run);

fn run() -> Result<()> {
    let matches = App::new("static-compress")
        .version("0.3.2")
        .about("Create statically-compresed copies of matching files")
        .author("NeoSmart Technologies")
        .arg(Arg::with_name("compressor")
            .short("c")
            .long("compressor")
            .value_name("[brotli|gzip|zopfli|webp]")
            .help("The compressor to use (default: gzip)")
            .takes_value(true))
        .arg(Arg::with_name("threads")
            .short("j")
            .long("threads")
            .value_name("COUNT")
            .help("The number of simultaneous compressions (default: 1)")
            .takes_value(true))
        .arg(Arg::with_name("filters")
            .value_name("FILTER")
            .multiple(true)
            .required(true))
        .arg(Arg::with_name("ext")
            .short("e")
            .value_name("EXT")
            .long("extension")
            .help("The extension to use for compressed files (default: gz, br, or webp)"))
        .arg(Arg::with_name("quality")
             .short("q")
             .long("quality")
             .takes_value(true)
             .help("A quality parameter to be passed to the encoder. Algorithm-specific."))
        /*.arg(Arg::with_name("excludes")
            .short("x")
            .value_name("FILTER")
            .long("exclude")
            .multiple(true)
            .help("Exclude files matching this glob expression"))*/
        .get_matches();

    fn get_parameter<'a, T>(matches: &clap::ArgMatches, name: &'static str, default_value: T) -> Result<T>
        where T: std::str::FromStr
    {
        match matches.value_of(name) {
            Some(v) => {
                Ok(v.parse().map_err(|_| ErrorKind::InvalidParameterValue(name))?)
            }
            None => Ok(default_value),
        }
    }

    let case_sensitive = !matches.is_present("nocase");
    let compressor = get_parameter(&matches, "compressor", CompressionAlgorithm::GZip)?;
    let temp = Parameters {
        extension: matches.value_of("ext")
            .unwrap_or(compressor.extension())
            .trim_matches(|c: char| c.is_whitespace() || c.is_control() || c == '.')
            .to_owned(),
        compressor: compressor,
        quality: match matches.value_of("quality") {
            Some(q) => Some(q.parse::<u8>().map_err(|_| ErrorKind::InvalidParameterValue("quality"))?),
            None => None
        },
        threads: get_parameter(&matches, "threads", 1)?,
    };

    /*let exclude_filters = match matches.values_of("exclude") {
        Some(values)=> values.map(|s| s.to_owned()).collect(),
        None => Vec::<String>::new(),
    };*/

    let parameters = Arc::<Parameters>::new(temp);
    let (send_queue, stats_rx, wait_group) = start_workers(&parameters);

    let mut include_filters: Vec<String> = match matches.values_of("filters") {
        Some(values) => Ok(values.map(|s| s.to_owned()).collect()),
        None => Err(ErrorKind::InvalidUsage),
    }?;

    let mut builder = GlobSetBuilder::new();
    fix_filters(&mut include_filters);
    for filter in include_filters.iter() {
        let glob = GlobBuilder::new(filter)
            .case_insensitive(!case_sensitive)
            .literal_separator(true)
            .build().map_err(|_| ErrorKind::InvalidIncludeFilter)?;
        builder.add(glob);
    }
    let globset = builder.build().map_err(|_| ErrorKind::InvalidIncludeFilter)?;

    //convert filters to paths and deal out conversion jobs

    dispatch_jobs(send_queue, include_filters, globset/*, exclude_filters*/)?;

    //wait for all jobs to finish
    wait_group.wait();

    //merge statistics from all threads
    let mut stats = Statistics::new();
    while let Ok(thread_stats) = stats_rx.recv() {
        stats.merge(&thread_stats);
    }

    println!("{}", stats);

    Ok(())
}

type ThreadParam = std::path::PathBuf;

fn start_workers<'a>(params: &Arc<Parameters>) -> (chan::Sender<ThreadParam>, mpsc::Receiver<Statistics>, chan::WaitGroup) {
    let (tx, rx) = chan::sync::<ThreadParam>(params.threads);
    let (stats_tx, stats_rx) = std::sync::mpsc::channel::<Statistics>();
    let wg = chan::WaitGroup::new();

    for _ in 0..params.threads {
        let local_params = params.clone();
        let local_rx = rx.clone();
        let local_stats_tx = stats_tx.clone();
        let local_wg = wg.clone();
        wg.add(1);
        std::thread::spawn(move || {
            worker_thread(local_params, local_stats_tx, local_rx);
            local_wg.done();
        });
    }

    (tx, stats_rx, wg)
}

fn yield_file<F>(path: PathBuf, globset: &GlobSet, callback: &F) -> Result<()>
    where F: Fn(PathBuf) -> Result<()>
{
    if is_hidden(&path)? {
        //we are ignoring .files and .directories
        //we may add a command-line switch to control this behavior in the future
        return Ok(());
    }

    if path.is_dir() {
        for child in path.read_dir()? {
            let child_path = child?.path();
            yield_file(child_path, globset, callback)?;
        }
    }
    else {
        //I'm presuming the binary search in is_blacklisted is faster
        //than globset.is_match, but we should benchmark it at some point
        if !is_blacklisted(&path)? && globset.is_match(&path) {
            callback(path)?;
        }
    }

    Ok(())
}

fn dispatch_jobs(send_queue: chan::Sender<ThreadParam>, filters: Vec<String>, globset: GlobSet/*, exclude_filters: Vec<String>*/) -> Result<()> {
    let paths = extract_paths(&filters)?;
    for path in paths {
        yield_file(path, &globset, &|path: PathBuf| {
            send_queue.send(path);
            Ok(())
        })?
    }

    Ok(())
}

fn worker_thread(params: Arc<Parameters>, stats_tx: mpsc::Sender<Statistics>, rx: chan::Receiver<ThreadParam>) {
    let mut local_stats = Statistics::new();

    loop {
        let src = match rx.recv() {
            Some(task) => task,
            None => break, //no more tasks
        };

        //in a nested function so we can handle errors centrally
        fn compress_single(src: &ThreadParam, params: &Parameters, mut local_stats: &mut Statistics) -> Result<()> {
            let dst_path = format!("{}.{}",
                                   src.to_str().ok_or(ErrorKind::InvalidCharactersInPath)?,
                                   params.extension);
            let dst = Path::new(&dst_path);

            //again, in a scope for error handling
            |local_stats: &mut Statistics| -> Result<()> {
                    let src_metadata = std::fs::metadata(src)?;

                    //don't compress files that are already compressed that haven't changed
                    if let Ok(dst_metadata) = std::fs::metadata(dst) {
                        //the destination already exists
                        let src_seconds = src_metadata.modified()?.duration_since(std::time::UNIX_EPOCH)?.as_secs();
                        let dst_seconds = dst_metadata.modified()?.duration_since(std::time::UNIX_EPOCH)?.as_secs();
                        match src_seconds == dst_seconds {
                            true => {
                                local_stats.update(src_metadata.len(), dst_metadata.len(), false);
                                return Ok(());//no need to recompress
                            },
                            false => {
                                std::fs::remove_file(dst)?; //throw if we can't
                            }
                        };
                    }

                    println!("{}", src.to_string_lossy());
                    params.compressor.compress(src.as_path(), dst, params.quality)?;
                    let dst_metadata = std::fs::metadata(dst)?;
                    local_stats.update(src_metadata.len(), dst_metadata.len(), true);
                    let src_modified = filetime::FileTime::from_last_modification_time(&src_metadata);
                    filetime::set_file_times(dst, filetime::FileTime::zero(), src_modified).unwrap_or_default();

                    Ok(())
                }(&mut local_stats)
                .map_err(|e| {
                    //try deleting the invalid destination file, but don't care if we can't
                    std::fs::remove_file(dst).unwrap_or_default();
                    e //return the same error
                })
        }

        if let Err(e) = compress_single(&src, &params, &mut local_stats) {
            errstln!("Error compressing {}: {}", src.to_string_lossy(), e);
        }
    }

    if !stats_tx.send(local_stats).is_ok() {
        errstln!("Error compiling statistics!");
    }
}

fn str_search(sorted: &[&str], search_term: &str, case_sensitive: bool) -> std::result::Result<usize, usize> {
    let term = match case_sensitive {
        true => search_term.to_owned(),
        false => search_term.to_lowercase(),
    };

    sorted.binary_search_by(|probe| probe.cmp(&&*term))
}

fn is_hidden(path: &Path) -> Result<bool> {
    let hidden = match path.file_name() {
        Some(x) => x.to_str().ok_or(ErrorKind::InvalidCharactersInPath)?
            .starts_with("."),
        None => false
    };
    Ok(hidden)
}

fn is_blacklisted(path: &Path) -> Result<bool> {
    let r = match path.extension() {
        Some(x) => {
            let ext = x.to_str().ok_or(ErrorKind::InvalidCharactersInPath)?;
            str_search(COMP_EXTS, &ext, false).is_ok()
        },
        None => false,
    };

    return Ok(r);
}

//pre-pends ./ to relative paths
fn fix_filters(mut filters: &mut Vec<String>) {
    for i in 0..filters.len() {
        let new_path;
        {
            let ref path = filters[i];
            match path.chars().next().expect("Received blank filter!") {
                '.' | '/' => continue,
                _ => new_path = format!("./{}", path) //un-prefixed path
            }
        }
        filters[i] = new_path;
    }
}

//Given a list of filters, extracts the directories that should be searched
//To-Do: Also provide info about to what depth they should be recursed
use std::collections::HashSet;
fn extract_paths(filters: &Vec<String>) -> Result<HashSet<PathBuf>> {
    use std::iter::FromIterator;

    let mut dirs = std::collections::HashSet::<PathBuf>::new();

    {
        let insert_path = &mut |filter: &String, dir: PathBuf| {
            debug(&format!("filter {} mapped to search {}", filter, dir.display()));
            dirs.insert(dir);
        };

        for filter in filters {
            //take everything until the first expression
            let mut last_char = None::<char>;
            let dir;
            {
                let partial = filter.chars().take_while(|c| match c {
                    &'?' | &'*' | &'{' | &'[' => false,
                    c => { last_char = Some(c.clone()); true }
                });
                dir = String::from_iter(partial);
            }

            let dir = match dir.chars().next() {
                Some(c) => match c {
                    '.' | '/' => PathBuf::from(dir),
                    _ => {
                        let mut pb = PathBuf::from("./");
                        pb.push(dir);
                        pb
                    }
                },
                None => {
                    insert_path(filter, PathBuf::from("./"));
                    continue;
                }
            };

            if dir.to_str().ok_or(ErrorKind::InvalidCharactersInPath)?.ends_with(filter) {
                //the "dir" is actually a full path to a single file
                //return it as-is
                insert_path(filter, dir);
                continue;
            }

            if last_char == Some('/') {
                //dir is a already a directory, return it as-is
                insert_path(filter, dir);
                continue;
            }

            //we need to extract the directory from the path we have
            let dir = match PathBuf::from(dir).parent() {
                Some(parent) => parent.to_path_buf(),
                None => PathBuf::from("./"),
            };

            insert_path(filter, dir);
        }
    }

    debug(&format!("final search paths: {:?}", dirs));

    Ok(dirs)
}
<|endoftext|>


<commit_before>use crossbeam::bounded;
use crossterm::{AlternateScreen, InputEvent, KeyEvent, TerminalInput};
use rand::distributions::Uniform;
use rand::prelude::Distribution;
use rusty_audio::Audio;
use rusty_sword::coord::{key_to_direction, Coord};
use rusty_sword::monster::Monster;
use rusty_sword::render::render_loop;
use rusty_sword::timer::Timer;
use rusty_sword::world::World;
use std::thread;
use std::time::{Duration, Instant};

fn main() {
    let mut audio = Audio::new();
    audio.add("monster_dies", "clips/monster_dies.wav");
    audio.add("monster_spawns", "clips/monster_spawns.wav");
    audio.add("player_dies", "clips/player_dies.wav");

    let mut world = World::new(30, 60);

    // Make a channel to send the world to the render thread, and a channel to receive it back
    // This is a silly design, we can demonstrate using channels for thread communication. :-P
    let (render_tx, render_rx) = bounded::<World>(0);
    let (main_tx, main_rx) = bounded::<World>(0);

    // Render Thread
    let render_thread = { thread::spawn(move || render_loop(render_rx, main_tx)) };

    // Game Loop
    let alternate_screen = AlternateScreen::to_alternate(true).unwrap();
    let input = TerminalInput::new();
    let mut reader = input.read_async();
    let mut rng = rand::thread_rng();
    let mut spawn_timer = Timer::from_millis(1000);
    let mut last_instant = Instant::now();
    'gameloop: loop {
        let delta = last_instant.elapsed();
        last_instant = Instant::now();
        let mut player = &mut world.player;

        // Player moves?
        let mut player_moved = false;
        while let Some(event) = reader.next() {
            match event {
                InputEvent::Keyboard(KeyEvent::Char('q')) | InputEvent::Keyboard(KeyEvent::Esc) => {
                    break 'gameloop
                }
                InputEvent::Keyboard(k) => {
                    if let Some(direction) = key_to_direction(k) {
                        player_moved =
                            player.travel(direction, &world.floor, &mut world.dirty_coords);
                    }
                }
                _ => {}
            }
        }

        // Update monster timers
        for monster in world.monsters.iter_mut() {
            monster.move_timer.update(delta);
        }

        // Monsters move?
        if !player_moved {
            for monster in world.monsters.iter_mut() {
                monster.try_travel(player.coord, &mut world.dirty_coords);
            }
        }

        // Did a monster die?
        let num_monsters = world.monsters.len();
        world
            .monsters
            .retain(|monster| monster.coord != player.sword_coord);
        let num_killed = num_monsters - world.monsters.len();
        if num_killed > 0 {
            player.score += num_killed as u64;
            audio.play("monster_dies");
        }

        // Spawn a new monster!
        spawn_timer.update(delta);
        if spawn_timer.ready {
            spawn_timer = Timer::from_millis(Uniform::new(1000, 5000).sample(&mut rng));
            let to_coord = Coord::new(
                Uniform::new(1, world.floor.rows).sample(&mut rng),
                Uniform::new(1, world.floor.cols).sample(&mut rng),
            );
            if to_coord != player.coord {
                world.monsters.push(Monster::new(to_coord, &mut rng));
                audio.play("monster_spawns");
            }
        }

        // Did the player die?
        if world
            .monsters
            .iter()
            .any(|monster| monster.coord == player.coord)
        {
            audio.play("player_dies");
            audio.wait(); // Wait until the sound finishes, so we can hear it before quitting.
            break 'gameloop;
        }

        // Give the whole world to the renderer
        render_tx.send(world).unwrap();
        // Get the whole world back
        world = main_rx.recv().unwrap();
        // Don't exceed ~60/fps
        if let Some(t) = Duration::from_secs_f64(1. / 60.).checked_sub(last_instant.elapsed()) {
            thread::sleep(t);
        }
    }

    // Close the render_tx channel, which will trigger the render thread to exit
    drop(render_tx);
    // Wait for the render thread to actually exit
    render_thread.join().unwrap();

    drop(alternate_screen);
    println!("Thanks for playing!");
}
<commit_msg>Simplify rand import<commit_after>use crossbeam::bounded;
use crossterm::{AlternateScreen, InputEvent, KeyEvent, TerminalInput};
use rand::distributions::{Distribution, Uniform};
use rusty_audio::Audio;
use rusty_sword::coord::{key_to_direction, Coord};
use rusty_sword::monster::Monster;
use rusty_sword::render::render_loop;
use rusty_sword::timer::Timer;
use rusty_sword::world::World;
use std::thread;
use std::time::{Duration, Instant};

fn main() {
    let mut audio = Audio::new();
    audio.add("monster_dies", "clips/monster_dies.wav");
    audio.add("monster_spawns", "clips/monster_spawns.wav");
    audio.add("player_dies", "clips/player_dies.wav");

    let mut world = World::new(30, 60);

    // Make a channel to send the world to the render thread, and a channel to receive it back
    // This is a silly design, we can demonstrate using channels for thread communication. :-P
    let (render_tx, render_rx) = bounded::<World>(0);
    let (main_tx, main_rx) = bounded::<World>(0);

    // Render Thread
    let render_thread = { thread::spawn(move || render_loop(render_rx, main_tx)) };

    // Game Loop
    let alternate_screen = AlternateScreen::to_alternate(true).unwrap();
    let input = TerminalInput::new();
    let mut reader = input.read_async();
    let mut rng = rand::thread_rng();
    let mut spawn_timer = Timer::from_millis(1000);
    let mut last_instant = Instant::now();
    'gameloop: loop {
        let delta = last_instant.elapsed();
        last_instant = Instant::now();
        let mut player = &mut world.player;

        // Player moves?
        let mut player_moved = false;
        while let Some(event) = reader.next() {
            match event {
                InputEvent::Keyboard(KeyEvent::Char('q')) | InputEvent::Keyboard(KeyEvent::Esc) => {
                    break 'gameloop
                }
                InputEvent::Keyboard(k) => {
                    if let Some(direction) = key_to_direction(k) {
                        player_moved =
                            player.travel(direction, &world.floor, &mut world.dirty_coords);
                    }
                }
                _ => {}
            }
        }

        // Update monster timers
        for monster in world.monsters.iter_mut() {
            monster.move_timer.update(delta);
        }

        // Monsters move?
        if !player_moved {
            for monster in world.monsters.iter_mut() {
                monster.try_travel(player.coord, &mut world.dirty_coords);
            }
        }

        // Did a monster die?
        let num_monsters = world.monsters.len();
        world
            .monsters
            .retain(|monster| monster.coord != player.sword_coord);
        let num_killed = num_monsters - world.monsters.len();
        if num_killed > 0 {
            player.score += num_killed as u64;
            audio.play("monster_dies");
        }

        // Spawn a new monster!
        spawn_timer.update(delta);
        if spawn_timer.ready {
            spawn_timer = Timer::from_millis(Uniform::new(1000, 5000).sample(&mut rng));
            let to_coord = Coord::new(
                Uniform::new(1, world.floor.rows).sample(&mut rng),
                Uniform::new(1, world.floor.cols).sample(&mut rng),
            );
            if to_coord != player.coord {
                world.monsters.push(Monster::new(to_coord, &mut rng));
                audio.play("monster_spawns");
            }
        }

        // Did the player die?
        if world
            .monsters
            .iter()
            .any(|monster| monster.coord == player.coord)
        {
            audio.play("player_dies");
            audio.wait(); // Wait until the sound finishes, so we can hear it before quitting.
            break 'gameloop;
        }

        // Give the whole world to the renderer
        render_tx.send(world).unwrap();
        // Get the whole world back
        world = main_rx.recv().unwrap();
        // Don't exceed ~60/fps
        if let Some(t) = Duration::from_secs_f64(1. / 60.).checked_sub(last_instant.elapsed()) {
            thread::sleep(t);
        }
    }

    // Close the render_tx channel, which will trigger the render thread to exit
    drop(render_tx);
    // Wait for the render thread to actually exit
    render_thread.join().unwrap();

    drop(alternate_screen);
    println!("Thanks for playing!");
}
<|endoftext|>


<commit_before>#[macro_use]
extern crate log;
extern crate env_logger;
extern crate irc;
extern crate github;

use irc::client::prelude::*;

fn main() {
    env_logger::init().unwrap();

    // This could be in a JSON config, but then we need to figure out how
    // to find that JSON config
    let irc_config: Config = Config {
        owners: Some(vec![format!("dbaron")]),
        nickname: Some(format!("wgmeeting-github-bot")),
        alt_nicks: Some(vec![format!("wgmeeting-github-bot-"),
                             format!("wgmeeting-github-bot--")]),
        username: Some(format!("dbaron-gh-bot")),
        realname: Some(format!("Bot to add meeting minutes to github issues.")),
        server: Some(format!("irc.w3.org")),
        port: Some(6667),
        use_ssl: Some(false),
        encoding: Some(format!("UTF-8")),
        channels: Some(vec![format!("#cssbottest")]),
        user_info: Some(format!("Bot to add meeting minutes to github issues.")),
        // FIXME: why doesn't this work as documented?
        //source: Some(format!("https://github.com/dbaron/wgmeeting-github-ircbot")),
        ..Default::default()
    };

    // FIXME: Eventually this should support multiple channels, plus
    // options to ask the bot which channels it's in, and which channels
    // it currently has buffers in.  (Then we can do things like ask the
    // bot to reboot itself, but it will only do so if it's not busy.)
    let mut channel_data = ChannelData::new();

    let server = IrcServer::from_config(irc_config).unwrap();
    server.identify().unwrap();
    for message in server.iter() {
        let message = message.unwrap(); // panic if there's an error

        match message.command {
            Command::PRIVMSG(ref target, ref msg) => {
                match message.source_nickname() {
                    None => {
                        // FIXME: trailing \n
                        warn!("PRIVMSG without a source! {}", message);
                    }
                    Some(ref source) => {
                        let mynick = server.current_nickname();
                        if target == mynick {
                            handle_bot_command(&server, msg, source, None)
                        } else if target.starts_with('#') {
                            match check_command_in_channel(mynick, msg) {
                                Some(ref command) => {
                                    handle_bot_command(&server, command, target, Some(source))
                                }
                                None => {
                                    channel_data.add_line(msg);
                                }
                            }
                        } else {
                            // FIXME: trailing \n
                            warn!("UNEXPECTED TARGET {} in message {}", target, message);
                        }
                    }
                }
            }
            _ => (),
        }
    }
}

// Take a message in the channel, and see if it was a message sent to
// this bot.
fn check_command_in_channel(mynick: &str, msg: &String) -> Option<String> {
    if !msg.starts_with(mynick) {
        return None;
    }
    let after_nick = &msg[mynick.len()..];
    if !after_nick.starts_with(":") && !after_nick.starts_with(",") {
        return None;
    }
    let after_punct = &after_nick[1..];
    Some(String::from(after_punct.trim_left()))
}

fn handle_bot_command(server: &IrcServer,
                      command: &str,
                      response_target: &str,
                      response_username: Option<&str>) {

    let send_line = |response_username: Option<&str>, line: &str| {
        let adjusted_line = match response_username {
            None => String::from(line),
            Some(username) => String::from(username) + ", " + line,
        };
        server
            .send_privmsg(response_target, &adjusted_line)
            .unwrap();
    };

    if command == "help" {
        send_line(response_username, "The commands I understand are:");
        send_line(None, "  help     Send this message.");
        return;
    }

    send_line(response_username,
              "Sorry, I don't understand that command.  Try 'help'.");
}

struct TopicData {
    lines: Vec<String>,
}

struct ChannelData {
    current_topic: TopicData,
}

impl TopicData {
    fn new() -> TopicData {
        TopicData { lines: vec![] }
    }
}

impl ChannelData {
    fn new() -> ChannelData {
        ChannelData { current_topic: TopicData::new() }
    }

    fn add_line(&mut self, line: &String) {
        self.current_topic.lines.push(line.clone())
    }
}
<commit_msg>Basic detection of topic boundaries (untested), including detection of /me commands in IRC (tested).<commit_after>#[macro_use]
extern crate log;
extern crate env_logger;
extern crate irc;
extern crate github;

use irc::client::prelude::*;

fn main() {
    env_logger::init().unwrap();

    // This could be in a JSON config, but then we need to figure out how
    // to find that JSON config
    let irc_config: Config = Config {
        owners: Some(vec![format!("dbaron")]),
        nickname: Some(format!("wgmeeting-github-bot")),
        alt_nicks: Some(vec![format!("wgmeeting-github-bot-"),
                             format!("wgmeeting-github-bot--")]),
        username: Some(format!("dbaron-gh-bot")),
        realname: Some(format!("Bot to add meeting minutes to github issues.")),
        server: Some(format!("irc.w3.org")),
        port: Some(6667),
        use_ssl: Some(false),
        encoding: Some(format!("UTF-8")),
        channels: Some(vec![format!("#cssbottest")]),
        user_info: Some(format!("Bot to add meeting minutes to github issues.")),
        // FIXME: why doesn't this work as documented?
        //source: Some(format!("https://github.com/dbaron/wgmeeting-github-ircbot")),
        ..Default::default()
    };

    // FIXME: Eventually this should support multiple channels, plus
    // options to ask the bot which channels it's in, and which channels
    // it currently has buffers in.  (Then we can do things like ask the
    // bot to reboot itself, but it will only do so if it's not busy.)
    let mut channel_data = ChannelData::new();

    let server = IrcServer::from_config(irc_config).unwrap();
    server.identify().unwrap();
    for message in server.iter() {
        let message = message.unwrap(); // panic if there's an error

        match message.command {
            Command::PRIVMSG(ref target, ref msg) => {
                match message.source_nickname() {
                    None => {
                        // FIXME: trailing \n
                        warn!("PRIVMSG without a source! {}", message);
                    }
                    Some(ref source) => {
                        let mynick = server.current_nickname();
                        if target == mynick {
                            handle_bot_command(&server, msg, source, None)
                        } else if target.starts_with('#') {
                            let source_ = String::from(*source);
                            let line = if msg.starts_with("\x01ACTION ") && msg.ends_with("\x01") {
                                ChannelLine {
                                    source: source_,
                                    is_action: true,
                                    message: String::from(&msg[8..msg.len() - 1]),
                                }
                            } else {
                                ChannelLine {
                                    source: source_,
                                    is_action: false,
                                    message: msg.clone(),
                                }
                            };

                            // FIXME: This needs to handle requests in /me
                            match check_command_in_channel(mynick, msg) {
                                Some(ref command) => {
                                    handle_bot_command(&server, command, target, Some(source))
                                }
                                None => {
                                    channel_data.add_line(line);
                                }
                            }
                        } else {
                            // FIXME: trailing \n
                            warn!("UNEXPECTED TARGET {} in message {}", target, message);
                        }
                    }
                }
            }
            _ => (),
        }
    }
}

// Take a message in the channel, and see if it was a message sent to
// this bot.
fn check_command_in_channel(mynick: &str, msg: &String) -> Option<String> {
    if !msg.starts_with(mynick) {
        return None;
    }
    let after_nick = &msg[mynick.len()..];
    if !after_nick.starts_with(":") && !after_nick.starts_with(",") {
        return None;
    }
    let after_punct = &after_nick[1..];
    Some(String::from(after_punct.trim_left()))
}

fn handle_bot_command(server: &IrcServer,
                      command: &str,
                      response_target: &str,
                      response_username: Option<&str>) {

    let send_line = |response_username: Option<&str>, line: &str| {
        let adjusted_line = match response_username {
            None => String::from(line),
            Some(username) => String::from(username) + ", " + line,
        };
        server
            .send_privmsg(response_target, &adjusted_line)
            .unwrap();
    };

    if command == "help" {
        send_line(response_username, "The commands I understand are:");
        send_line(None, "  help     Send this message.");
        return;
    }

    send_line(response_username,
              "Sorry, I don't understand that command.  Try 'help'.");
}

struct ChannelLine {
    source: String,
    is_action: bool,
    message: String,
}

struct TopicData {
    lines: Vec<ChannelLine>,
}

struct ChannelData {
    current_topic: Option<TopicData>,
}

impl TopicData {
    fn new() -> TopicData {
        TopicData { lines: vec![] }
    }
}

impl ChannelData {
    fn new() -> ChannelData {
        ChannelData { current_topic: None }
    }

    fn add_line(&mut self, line: ChannelLine) {
        if line.message.starts_with("Topic:") {
            self.start_topic();
        }
        if line.source == "trackbot" && line.is_action == true &&
           line.message == "is ending a teleconference." {
            self.end_topic();
        }
        print!("{} {} {}\n", line.source, line.is_action, line.message);
        match self.current_topic {
            None => (),
            Some(ref mut data) => {
                data.lines.push(line);
            }
        }
    }

    fn start_topic(&mut self) {
        if self.current_topic.is_some() {
            self.end_topic();
        }

        self.current_topic = Some(TopicData::new());
    }

    fn end_topic(&mut self) {
        // TODO: Test the topic boundary code.
        // FIXME: Do something with the data rather than throwing it away!
        self.current_topic = None;
    }
}
<|endoftext|>