<commit_before>#[macro_use] extern crate macro_attr;
#[macro_use] extern crate enum_derive;

use std::io::Write;
use std::env;

mod keybinds;
use keybinds::*;

mod file;
use file::*;

mod terminal;
use terminal::*;

fn render_file(mut out: &mut Terminal, file: &File) {
    let (screen_width, screen_height) = out.get_size();

    let mut x = 1;
    let mut y = 1;

    for (line_number_maybe, line) in file.wrapped_lines((screen_width - file.lineno_chars() - 1, screen_height - 3)) {
        if let Some(line_number) = line_number_maybe {
            out.goto((x, y));
            out.set_color_fg(Color::Grey);
            write!(out, "{:1$}",
                   line_number + 1, file.lineno_chars() as usize).unwrap();
            out.set_color_fg(Color::Reset);
        }
        x += file.lineno_chars() + 1;
        out.goto((x, y));
        write!(out, "{}", line).unwrap();
        x = 1;
        y += 1;
    }
}

fn render_footer(mut out: &mut Terminal, keys: &KeybindTable) {
    let (screen_width, screen_height) = out.get_size();

    let mut x = 1;
    let mut y = screen_height - 1;
    for (key, action) in keys.entries() {
        out.goto((x, y));
        out.set_color_fg(Color::Black);
        out.set_color_bg(Color::White);
        write!(out, "{}", key).unwrap();
        out.set_color_fg(Color::Reset);
        out.set_color_bg(Color::Reset);
        write!(out, " {}", action).unwrap();
        x += 16;
        if x > screen_width {
            x = 1;
            y -= 1;
        }
    }
}

fn render_status(mut out: &mut Terminal, file: &File) {
    let (width, height) = out.get_size();
    let file_size = (width - file.lineno_chars() - 1, height - 3);
    let x = 1;
    let y = height;
    out.goto((x, y));
    write!(out, "{}", file.debug(file_size)).unwrap();
}

fn main() {
    let keys = KeybindTable::default();
    let mut term = Terminal::default();
    term.clear();
    term.flush().unwrap();
    render_footer(&mut term, &keys);
    let args: Vec<String> = env::args().collect();
    let filename = args.get(1).cloned().unwrap_or(String::from("README.md"));
    let mut file = File::open(&filename);
    render_file(&mut term, &file);
    render_status(&mut term, &file);
    let (screen_w, screen_h) = term.get_size();
    let file_size = (screen_w - file.lineno_chars() - 1, screen_h - 3);
    term.goto((file.cursor(file_size).x + file.lineno_chars() + 1, file.cursor(file_size).y));
    term.flush().unwrap();
    let mut file_lines = file.lines.len();
    let mut file_wrapped_lines = file.wrapped_lines(file_size).len();
    let mut file_dirty = false;
    let mut screen_dirty = false;
    for evt in term.keys() {
        let (screen_w, screen_h) = term.get_size();
        let file_size = (screen_w - file.lineno_chars() - 1, screen_h - 3);
        match evt {
            Event::Mouse(_) => (),
            Event::Unsupported(_) => (),
            Event::Key(Key::Null) => (),
            Event::Key(Key::Insert) => (),
            Event::Key(Key::F(_)) => (),
            Event::Key(Key::Esc) => (),
            Event::Key(k @ Key::Ctrl(_)) | Event::Key(k @ Key::Alt(_)) => {
                match keys.lookup(k) {
                    Some(Command::Quit) => break,
                    Some(Command::Refresh) => {
                        file.refresh(file_size);
                        screen_dirty = true;
                    },
                    None => (),
                    _ => ()
                }
            },
            Event::Key(Key::Left) => {
                screen_dirty = file.move_cursor_left(file_size);
            },
            Event::Key(Key::Right) => {
                screen_dirty = file.move_cursor_right(file_size);
            },
            Event::Key(Key::Up) => {
                screen_dirty = file.move_cursor_up(file_size);
            },
            Event::Key(Key::Down) => {
                screen_dirty = file.move_cursor_down(file_size);
            },
            Event::Key(Key::Home) => file.move_cursor_home(file_size),
            Event::Key(Key::End) => file.move_cursor_end(file_size),
            Event::Key(Key::PageUp) => {
                file.page_up(file_size);
                screen_dirty = true;
            },
            Event::Key(Key::PageDown) => {
                file.page_down(file_size);
                screen_dirty = true;
            },
            Event::Key(Key::Char('\t')) => {
                for _ in 0..4 {
                    file.insert(file_size, ' ');
                }
                file_dirty = true;
            },
            Event::Key(Key::Char('\n')) => {
                file.insert_newline(file_size);
                screen_dirty = true;
            },
            Event::Key(Key::Delete) => {
                file.delete(file_size);
                screen_dirty = true;
            },
            Event::Key(Key::Backspace) => {
                file.backspace(file_size);
                screen_dirty = true;
            },
            Event::Key(Key::Char(c)) => {
                file.insert(file_size, c);
                file_dirty = true;
            },
        }
        if screen_dirty {
            file_dirty = true;
        }
        if file_dirty {
            let new_file_lines = file.lines.len();
            let new_file_wrapped_lines = file.wrapped_lines(file_size).len();
            if new_file_lines != file_lines {
                screen_dirty = true;
                file_lines = new_file_lines;
            }
            if new_file_wrapped_lines != file_wrapped_lines {
                screen_dirty = true;
                file_wrapped_lines = new_file_wrapped_lines;
            }
        }
        if screen_dirty {
            term.clear();
            render_footer(&mut term, &keys);
            screen_dirty = false;
        }
        if file_dirty {
            render_file(&mut term, &file);
            file_dirty = false;
        }
        render_status(&mut term, &file);
        term.goto((file.cursor(file_size).x + file.lineno_chars() + 1, file.cursor(file_size).y));
        term.flush().unwrap();
    }
}
<commit_msg>Centralize file bounds computation<commit_after>#[macro_use] extern crate macro_attr;
#[macro_use] extern crate enum_derive;

use std::io::Write;
use std::env;

mod keybinds;
use keybinds::*;

mod file;
use file::*;

mod terminal;
use terminal::*;

fn render_file(out: &mut Terminal, file_size: (i32, i32), file: &File) {
    let mut x = 1;
    let mut y = 1;

    for (line_number_maybe, line) in file.wrapped_lines(file_size) {
        if let Some(line_number) = line_number_maybe {
            out.goto((x, y));
            out.set_color_fg(Color::Grey);
            write!(out, "{:1$}",
                   line_number + 1, file.lineno_chars() as usize).unwrap();
            out.set_color_fg(Color::Reset);
        }
        x += file.lineno_chars() + 1;
        out.goto((x, y));
        write!(out, "{}", line).unwrap();
        x = 1;
        y += 1;
    }
}

fn render_footer(out: &mut Terminal, keys: &KeybindTable) {
    let (screen_width, screen_height) = out.get_size();

    let mut x = 1;
    let mut y = screen_height - 1;
    for (key, action) in keys.entries() {
        out.goto((x, y));
        out.set_color_fg(Color::Black);
        out.set_color_bg(Color::White);
        write!(out, "{}", key).unwrap();
        out.set_color_fg(Color::Reset);
        out.set_color_bg(Color::Reset);
        write!(out, " {}", action).unwrap();
        x += 16;
        if x > screen_width {
            x = 1;
            y -= 1;
        }
    }
}

fn render_status(out: &mut Terminal, file_size: (i32, i32), file: &File) {
    let (_, height) = out.get_size();
    let x = 1;
    let y = height;
    out.goto((x, y));
    write!(out, "{}", file.debug(file_size)).unwrap();
}

fn get_file_size(term: &Terminal, file: &File) -> (i32, i32) {
    let (screen_w, screen_h) = term.get_size();
    (screen_w - file.lineno_chars() - 1, screen_h - 3)
}

fn main() {
    let keys = KeybindTable::default();
    let mut term = Terminal::default();
    term.clear();
    term.flush().unwrap();
    render_footer(&mut term, &keys);
    let args: Vec<String> = env::args().collect();
    let filename = args.get(1).cloned().unwrap_or(String::from("README.md"));
    let mut file = File::open(&filename);
    let file_size = get_file_size(&term, &file);
    render_file(&mut term, file_size, &file);
    render_status(&mut term, file_size, &file);
    term.goto((file.cursor(file_size).x + file.lineno_chars() + 1, file.cursor(file_size).y));
    term.flush().unwrap();
    let mut file_lines = file.lines.len();
    let mut file_wrapped_lines = file.wrapped_lines(file_size).len();
    let mut file_dirty = false;
    let mut screen_dirty = false;
    for evt in term.keys() {
        let file_size = get_file_size(&term, &file);
        match evt {
            Event::Mouse(_) => (),
            Event::Unsupported(_) => (),
            Event::Key(Key::Null) => (),
            Event::Key(Key::Insert) => (),
            Event::Key(Key::F(_)) => (),
            Event::Key(Key::Esc) => (),
            Event::Key(k @ Key::Ctrl(_)) | Event::Key(k @ Key::Alt(_)) => {
                match keys.lookup(k) {
                    Some(Command::Quit) => break,
                    Some(Command::Refresh) => {
                        file.refresh(file_size);
                        screen_dirty = true;
                    },
                    None => (),
                    _ => ()
                }
            },
            Event::Key(Key::Left) => {
                screen_dirty = file.move_cursor_left(file_size);
            },
            Event::Key(Key::Right) => {
                screen_dirty = file.move_cursor_right(file_size);
            },
            Event::Key(Key::Up) => {
                screen_dirty = file.move_cursor_up(file_size);
            },
            Event::Key(Key::Down) => {
                screen_dirty = file.move_cursor_down(file_size);
            },
            Event::Key(Key::Home) => file.move_cursor_home(file_size),
            Event::Key(Key::End) => file.move_cursor_end(file_size),
            Event::Key(Key::PageUp) => {
                file.page_up(file_size);
                screen_dirty = true;
            },
            Event::Key(Key::PageDown) => {
                file.page_down(file_size);
                screen_dirty = true;
            },
            Event::Key(Key::Char('\t')) => {
                for _ in 0..4 {
                    file.insert(file_size, ' ');
                }
                file_dirty = true;
            },
            Event::Key(Key::Char('\n')) => {
                file.insert_newline(file_size);
                screen_dirty = true;
            },
            Event::Key(Key::Delete) => {
                file.delete(file_size);
                screen_dirty = true;
            },
            Event::Key(Key::Backspace) => {
                file.backspace(file_size);
                screen_dirty = true;
            },
            Event::Key(Key::Char(c)) => {
                file.insert(file_size, c);
                file_dirty = true;
            },
        }
        if screen_dirty {
            file_dirty = true;
        }
        if file_dirty {
            let new_file_lines = file.lines.len();
            let new_file_wrapped_lines = file.wrapped_lines(file_size).len();
            if new_file_lines != file_lines {
                screen_dirty = true;
                file_lines = new_file_lines;
            }
            if new_file_wrapped_lines != file_wrapped_lines {
                screen_dirty = true;
                file_wrapped_lines = new_file_wrapped_lines;
            }
        }
        if screen_dirty {
            term.clear();
            render_footer(&mut term, &keys);
            screen_dirty = false;
        }
        if file_dirty {
            render_file(&mut term, file_size, &file);
            file_dirty = false;
        }
        render_status(&mut term, file_size, &file);
        term.goto((file.cursor(file_size).x + file.lineno_chars() + 1, file.cursor(file_size).y));
        term.flush().unwrap();
    }
}
<|endoftext|>


<commit_before>extern crate clap;
extern crate inc_stats;

use std::f64;
use std::fs::File;
use std::io::{self, BufRead, Write};
use std::iter;

use clap::{Arg, ArgGroup, App};

use inc_stats::{SummStats, Percentiles, Mode};

fn main() {
    let matches = App::new("stats")
        .version("0.0")
        .author("Erik Brinkman <erik.brinkman@gmail.com>")
        .about("Compute summary statistics of streams of numbers")
        // Output
        .arg(Arg::with_name("count")
             .short("c")
             .long("count")
             .help("Print count"))
        .arg(Arg::with_name("min")
             .long("min")
             .help("Print min"))
        .arg(Arg::with_name("max")
             .long("max")
             .help("Print max"))
        .arg(Arg::with_name("mean")
             .short("m")
             .long("mean")
             .help("Print mean"))
        .arg(Arg::with_name("sum")
             .long("sum")
             .help("Print sum"))
        .arg(Arg::with_name("stddev")
             .short("s")
             .long("stddev")
             .help("Print sample standard deviation"))
        .arg(Arg::with_name("var")
             .short("v")
             .long("var")
             .help("Print sample variance"))
        .arg(Arg::with_name("stderr")
             .long("stderr")
             .help("Print standard error"))
        .arg(Arg::with_name("median")
             .long("median")
             .help("Print median (Note: computing median takes O(n) space"))
        .arg(Arg::with_name("percentiles")
             .short("p")
             .long("percentiles")
             .takes_value(true)
             .use_delimiter(true)
             .help("Print arbitrary percentiles. The argument should be a comma delimited list of floats in [0, 100] (Note: computing any percentile takes O(n) space)"))
        .arg(Arg::with_name("mode")
             .long("mode")
             .help("Print mode (Note: computing mode takes O(n) space"))
        .arg(Arg::with_name("mode-count")
             .long("mode-count")
             .help("Print the number of times the mode occured (Note: computing mode takes O(n) space"))
        // Formatting
        .group(ArgGroup::with_name("format").args(&["tsv", "json"]))
        .arg(Arg::with_name("tsv")
             .short("t")
             .long("tsv")
             .help("Output as tsv. This will force tsv output even if only one statistics is requested."))
        .arg(Arg::with_name("json")
             .short("j")
             .long("json")
             .help("Output as compressed json"))
        // IO
        .arg(Arg::with_name("input")
             .short("i")
             .long("input")
             .value_name("file")
             .default_value("-")
             .help("Take input from file"))
        .arg(Arg::with_name("output")
             .short("o")
             .long("output")
             .value_name("file")
             .default_value("-")
             .help("Write output to file"))
        .get_matches();

    // Setup input
    let stdin = io::stdin();
    let input: Box<BufRead> = match matches.value_of("input") {
        Some("-") => Box::new(stdin.lock()),
        Some(file_name) => Box::new(io::BufReader::new(
                File::open(file_name).expect(&format!("File \"{}\" does not exist", file_name)))),
        None => unreachable!(),
    };

    // Setup output
    let stdout = io::stdout();
    let mut output: Box<Write> = match matches.value_of("output") {
        Some("-") => Box::new(stdout.lock()),
        Some(file_name) => Box::new(io::BufWriter::new(
                File::open(file_name).expect(&format!("Couldn't open file \"{}\" for writing", file_name)))),
        None => unreachable!(),
    };

    // Process input
    let mut stats = SummStats::new();
    let mut percs = Percentiles::new();
    let mut mode = Mode::new();

    let add_mode = ["mode", "mode-count"].iter().any(|s| matches.is_present(s));
    let add_percs = ["percentiles", "median"].iter().any(|s| matches.is_present(s));
    let add_stats = ["count", "min", "max", "mean", "sum", "stddev", "var", "stderr"].iter().any(|s| matches.is_present(s));

    // XXX Possible to do with Iterators and mapping? Issues with lifetimes
    for line in input.lines() {
        for token in line.expect("Couldn't read from file").split(char::is_whitespace).filter(|s| !s.is_empty()) {
            let num = token.parse().expect(&format!("Could not parse \"{}\" as float", token));
            if add_mode {
                mode.add(num);
            }
            if add_percs {
                percs.add(num);
            }
            if add_stats {
                stats.add(num);
            }
        }
    }

    // Generate output
    let mut results = Vec::new();
    if matches.is_present("count") {
        results.push((String::from("count"), stats.count() as f64));
    }
    if matches.is_present("min") {
        results.push((String::from("min"), stats.min().unwrap_or(f64::NAN)));
    }
    if matches.is_present("max") {
        results.push((String::from("max"), stats.max().unwrap_or(f64::NAN)));
    }
    if matches.is_present("mean") {
        results.push((String::from("mean"), stats.mean().unwrap_or(f64::NAN)));
    }
    if matches.is_present("sum") {
        results.push((String::from("sum"), stats.sum()));
    }
    if matches.is_present("stddev") {
        results.push((String::from("stddev"), stats.standard_deviation().unwrap_or(f64::NAN)));
    }
    if matches.is_present("var") {
        results.push((String::from("var"), stats.variance().unwrap_or(f64::NAN)));
    }
    if matches.is_present("stderr") {
        results.push((String::from("stderr"), stats.standard_error().unwrap_or(f64::NAN)));
    }
    if matches.is_present("percentiles") {
        let percentiles: Vec<f64> = matches.values_of("percentiles").unwrap().map(|p| p.parse::<f64>().expect(&format!("Could not parse \"{}\" as float", p))).collect();
        let vals: Box<Iterator<Item=f64>> = match percs.percentiles(percentiles.iter().map(|p| p / 100.0)) {
            None => Box::new(iter::repeat(f64::NAN).take(percentiles.len())),
            Some(pvals) => Box::new(pvals.into_iter()),
        };
        for (perc, val) in percentiles.iter().zip(vals) {
            results.push((format!("{}%", perc), val));
        }
    }
    if matches.is_present("median") {
        results.push((String::from("median"), percs.median().unwrap_or(f64::NAN)));
    }
    if matches.is_present("mode") {
        results.push((String::from("mode"), mode.mode().unwrap_or(f64::NAN)));
    }
    if matches.is_present("mode-count") {
        results.push((String::from("mode #"), mode.mode_count() as f64));
    }

    // Defaults
    if results.is_empty() {
        results.push((String::from("count"), stats.count() as f64));
        results.push((String::from("min"), stats.min().unwrap_or(f64::NAN)));
        results.push((String::from("max"), stats.max().unwrap_or(f64::NAN)));
        results.push((String::from("mean"), stats.mean().unwrap_or(f64::NAN)));
        results.push((String::from("stddev"), stats.standard_deviation().unwrap_or(f64::NAN)));
    }

    // Write output
    if matches.is_present("tsv") {
        write_tsv(&results, &mut *output);
    } else if matches.is_present("json") {
        write_json(&results, &mut *output);
    } else if results.len() == 1 {
        let (_, val) = results[0];
        writeln!(output, "{}", val).expect("couldn't write to output");
    } else {
        write_tsv(&results, &mut *output);
    }
}

fn write_tsv(results: &Vec<(String, f64)>, output: &mut Write) {
    for &(ref name, ref val) in results {
        writeln!(output, "{}\t{}", name, val).expect("couldn't write to output");
    }
}

fn write_json(results: &Vec<(String, f64)>, output: &mut Write) {
    write!(output, "{{").expect("couldn't write to output");
    let mut iter = results.iter();
    let &(ref name, ref val) = iter.next().unwrap();
    write!(output, "\"{}\":{}", name, val).expect("couldn't write to output");
    for &(ref name, ref val) in iter {
        write!(output, ",\"{}\":{}", name, val).expect("couldn't write to output");
    }
    write!(output, "}}\n").expect("couldn't write to output");
}
<commit_msg>Fix a bug when no stats were specified<commit_after>extern crate clap;
extern crate inc_stats;

use std::f64;
use std::fs::File;
use std::io::{self, BufRead, Write};
use std::iter;

use clap::{Arg, ArgGroup, App};

use inc_stats::{SummStats, Percentiles, Mode};

fn main() {
    let matches = App::new("stats")
        .version("0.0")
        .author("Erik Brinkman <erik.brinkman@gmail.com>")
        .about("Compute summary statistics of streams of numbers")
        // Output
        .arg(Arg::with_name("count")
             .short("c")
             .long("count")
             .help("Print count"))
        .arg(Arg::with_name("min")
             .long("min")
             .help("Print min"))
        .arg(Arg::with_name("max")
             .long("max")
             .help("Print max"))
        .arg(Arg::with_name("mean")
             .short("m")
             .long("mean")
             .help("Print mean"))
        .arg(Arg::with_name("sum")
             .long("sum")
             .help("Print sum"))
        .arg(Arg::with_name("stddev")
             .short("s")
             .long("stddev")
             .help("Print sample standard deviation"))
        .arg(Arg::with_name("var")
             .short("v")
             .long("var")
             .help("Print sample variance"))
        .arg(Arg::with_name("stderr")
             .long("stderr")
             .help("Print standard error"))
        .arg(Arg::with_name("median")
             .long("median")
             .help("Print median (Note: computing median takes O(n) space"))
        .arg(Arg::with_name("percentiles")
             .short("p")
             .long("percentiles")
             .takes_value(true)
             .use_delimiter(true)
             .help("Print arbitrary percentiles. The argument should be a comma delimited list of floats in [0, 100] (Note: computing any percentile takes O(n) space)"))
        .arg(Arg::with_name("mode")
             .long("mode")
             .help("Print mode (Note: computing mode takes O(n) space"))
        .arg(Arg::with_name("mode-count")
             .long("mode-count")
             .help("Print the number of times the mode occured (Note: computing mode takes O(n) space"))
        // Formatting
        .group(ArgGroup::with_name("format").args(&["tsv", "json"]))
        .arg(Arg::with_name("tsv")
             .short("t")
             .long("tsv")
             .help("Output as tsv. This will force tsv output even if only one statistics is requested."))
        .arg(Arg::with_name("json")
             .short("j")
             .long("json")
             .help("Output as compressed json"))
        // IO
        .arg(Arg::with_name("input")
             .short("i")
             .long("input")
             .value_name("file")
             .default_value("-")
             .help("Take input from file"))
        .arg(Arg::with_name("output")
             .short("o")
             .long("output")
             .value_name("file")
             .default_value("-")
             .help("Write output to file"))
        .get_matches();

    // Setup input
    let stdin = io::stdin();
    let input: Box<BufRead> = match matches.value_of("input") {
        Some("-") => Box::new(stdin.lock()),
        Some(file_name) => Box::new(io::BufReader::new(
                File::open(file_name).expect(&format!("File \"{}\" does not exist", file_name)))),
        None => unreachable!(),
    };

    // Setup output
    let stdout = io::stdout();
    let mut output: Box<Write> = match matches.value_of("output") {
        Some("-") => Box::new(stdout.lock()),
        Some(file_name) => Box::new(io::BufWriter::new(
                File::open(file_name).expect(&format!("Couldn't open file \"{}\" for writing", file_name)))),
        None => unreachable!(),
    };

    // Process input
    let mut stats = SummStats::new();
    let mut percs = Percentiles::new();
    let mut mode = Mode::new();

    let add_mode = ["mode", "mode-count"].iter().any(|s| matches.is_present(s));
    let add_percs = ["percentiles", "median"].iter().any(|s| matches.is_present(s));
    let add_stats = ["count", "min", "max", "mean", "sum", "stddev", "var", "stderr"].iter().any(|s| matches.is_present(s)) || !(add_mode && add_percs);

    // XXX Possible to do with Iterators and mapping? Issues with lifetimes
    for line in input.lines() {
        for token in line.expect("Couldn't read from file").split(char::is_whitespace).filter(|s| !s.is_empty()) {
            let num = token.parse().expect(&format!("Could not parse \"{}\" as float", token));
            if add_mode {
                mode.add(num);
            }
            if add_percs {
                percs.add(num);
            }
            if add_stats {
                stats.add(num);
            }
        }
    }

    // Generate output
    let mut results = Vec::new();
    if matches.is_present("count") {
        results.push((String::from("count"), stats.count() as f64));
    }
    if matches.is_present("min") {
        results.push((String::from("min"), stats.min().unwrap_or(f64::NAN)));
    }
    if matches.is_present("max") {
        results.push((String::from("max"), stats.max().unwrap_or(f64::NAN)));
    }
    if matches.is_present("mean") {
        results.push((String::from("mean"), stats.mean().unwrap_or(f64::NAN)));
    }
    if matches.is_present("sum") {
        results.push((String::from("sum"), stats.sum()));
    }
    if matches.is_present("stddev") {
        results.push((String::from("stddev"), stats.standard_deviation().unwrap_or(f64::NAN)));
    }
    if matches.is_present("var") {
        results.push((String::from("var"), stats.variance().unwrap_or(f64::NAN)));
    }
    if matches.is_present("stderr") {
        results.push((String::from("stderr"), stats.standard_error().unwrap_or(f64::NAN)));
    }
    if matches.is_present("percentiles") {
        let percentiles: Vec<f64> = matches.values_of("percentiles").unwrap().map(|p| p.parse::<f64>().expect(&format!("Could not parse \"{}\" as float", p))).collect();
        let vals: Box<Iterator<Item=f64>> = match percs.percentiles(percentiles.iter().map(|p| p / 100.0)) {
            None => Box::new(iter::repeat(f64::NAN).take(percentiles.len())),
            Some(pvals) => Box::new(pvals.into_iter()),
        };
        for (perc, val) in percentiles.iter().zip(vals) {
            results.push((format!("{}%", perc), val));
        }
    }
    if matches.is_present("median") {
        results.push((String::from("median"), percs.median().unwrap_or(f64::NAN)));
    }
    if matches.is_present("mode") {
        results.push((String::from("mode"), mode.mode().unwrap_or(f64::NAN)));
    }
    if matches.is_present("mode-count") {
        results.push((String::from("mode #"), mode.mode_count() as f64));
    }

    // Defaults
    if results.is_empty() {
        results.push((String::from("count"), stats.count() as f64));
        results.push((String::from("min"), stats.min().unwrap_or(f64::NAN)));
        results.push((String::from("max"), stats.max().unwrap_or(f64::NAN)));
        results.push((String::from("mean"), stats.mean().unwrap_or(f64::NAN)));
        results.push((String::from("stddev"), stats.standard_deviation().unwrap_or(f64::NAN)));
    }

    // Write output
    if matches.is_present("tsv") {
        write_tsv(&results, &mut *output);
    } else if matches.is_present("json") {
        write_json(&results, &mut *output);
    } else if results.len() == 1 {
        let (_, val) = results[0];
        writeln!(output, "{}", val).expect("couldn't write to output");
    } else {
        write_tsv(&results, &mut *output);
    }
}

fn write_tsv(results: &Vec<(String, f64)>, output: &mut Write) {
    for &(ref name, ref val) in results {
        writeln!(output, "{}\t{}", name, val).expect("couldn't write to output");
    }
}

fn write_json(results: &Vec<(String, f64)>, output: &mut Write) {
    write!(output, "{{").expect("couldn't write to output");
    let mut iter = results.iter();
    let &(ref name, ref val) = iter.next().unwrap();
    write!(output, "\"{}\":{}", name, val).expect("couldn't write to output");
    for &(ref name, ref val) in iter {
        write!(output, ",\"{}\":{}", name, val).expect("couldn't write to output");
    }
    write!(output, "}}\n").expect("couldn't write to output");
}
<|endoftext|>


<commit_before>extern crate iron;
extern crate router;
extern crate mount;
extern crate staticfile;

use iron::prelude::*;
use iron::status;
use router::Router;
use staticfile::Static;
use mount::Mount;
use std::path::Path;
use std::env;

fn main() {
	// the router (for RESTfull actions)
	let mut router = Router::new();

	router.get("/wings", move_wings,"wings");
	router.get("/mouth", open_mouth,"mouth");
	router.get("/eyes", blink_eyes,"eyes");

	fn move_wings(_: &mut Request) -> IronResult<Response> {
		Ok(Response::with((status::Ok, "Wings moved!")))
	}

	fn open_mouth(_: &mut Request) -> IronResult<Response> {
		Ok(Response::with((status::Ok, "Mouth opened!")))
	}

	fn blink_eyes(_: &mut Request) -> IronResult<Response> {
		Ok(Response::with((status::Ok, "Eyes blinked!")))
	}

	// the mounter for static files
	let mut mount = Mount::new();
	mount
		.mount("/",Static::new(Path::new("static")))
		.mount("/api/",router);
	Iron::new(mount).http("0.0.0.0:3000").unwrap();
}
<commit_msg>clean up<commit_after>extern crate iron;
extern crate router;
extern crate mount;
extern crate staticfile;

use iron::prelude::*;
use iron::status;
use router::Router;
use staticfile::Static;
use mount::Mount;
use std::path::Path;

fn main() {
	// the router (for RESTfull actions)
	let mut router = Router::new();

	router.get("/wings", move_wings,"wings");
	router.get("/mouth", open_mouth,"mouth");
	router.get("/eyes", blink_eyes,"eyes");

	fn move_wings(_: &mut Request) -> IronResult<Response> {
		Ok(Response::with((status::Ok, "Wings moved!")))
	}

	fn open_mouth(_: &mut Request) -> IronResult<Response> {
		Ok(Response::with((status::Ok, "Mouth opened!")))
	}

	fn blink_eyes(_: &mut Request) -> IronResult<Response> {
		Ok(Response::with((status::Ok, "Eyes blinked!")))
	}

	// the mounter for static files
	let mut mount = Mount::new();
	mount
		.mount("/",Static::new(Path::new("static")))
		.mount("/api/",router);
	Iron::new(mount).http("0.0.0.0:3000").unwrap();
}
<|endoftext|>


<commit_before>#[macro_use]
extern crate nom;
#[macro_use]
extern crate log;
extern crate env_logger;
#[macro_use]
extern crate enum_primitive;
extern crate num_traits;
extern crate clap;
extern crate serde_yaml;
extern crate rand;

#[macro_use]
extern crate serde_derive;

#[cfg(test)]
#[macro_use]
extern crate quickcheck;

mod runner;
mod markov;
mod common;

use clap::{App, Arg, SubCommand};
use std::fs::File;
use std::io::{self, Write};

fn word_parser(items: Result<Vec<runner::Word>, io::Error>) -> Vec<runner::Word> {
    match items {
        Ok(val) => val,
        Err(err) => {
            if err.kind() == io::ErrorKind::InvalidData {
                println!("{}", err.get_ref().unwrap());
                std::process::exit(-1);
            } else {
                panic!("Error during parsing: {}", err);
            }
        }
    }
}

fn main() {
    env_logger::init().unwrap();
    let matches = App::new("peacenik")
        .version("1.0")
        .author("Tom Parker <palfrey@tevp.net>")
        .about("Beatnik language tools")
        .subcommand(SubCommand::with_name("run")
                        .about("Beatnik interpreter")
                        .arg(Arg::with_name("INPUT")
                                 .help("Sets the input file to use")
                                 .required(true)
                                 .index(1)))
        .subcommand(SubCommand::with_name("wottasquare")
                        .about("Wottasquare interpreter")
                        .arg(Arg::with_name("INPUT")
                                 .help("Sets the input file to use")
                                 .required(true)
                                 .index(1)))
        .subcommand(SubCommand::with_name("wottasquare-dumper")
                        .about("Wottasquare dumper")
                        .arg(Arg::with_name("INPUT")
                                 .help("Sets the input file to use")
                                 .required(true)
                                 .index(1)))
        .subcommand(SubCommand::with_name("generate-markov")
                        .about("Markov chain generator")
                        .arg(Arg::with_name("INPUT")
                                 .short("i")
                                 .takes_value(true)
                                 .help("Sets the input file to use")
                                 .required(true))
                        .arg(Arg::with_name("OUTPUT")
                                 .short("o")
                                 .takes_value(true)
                                 .help("Sets the output file to use")
                                 .required(true)))
        .subcommand(SubCommand::with_name("markov-beatnik")
                        .about("Beatnik from Wottasquare using Markov")
                        .arg(Arg::with_name("INPUT")
                                 .short("i")
                                 .takes_value(true)
                                 .help("Sets the input file to use")
                                 .required(true))
                        .arg(Arg::with_name("MARKOV")
                                 .short("m")
                                 .takes_value(true)
                                 .help("Sets the markov file to use")
                                 .required(true))
                        .arg(Arg::with_name("OUTPUT")
                                 .short("o")
                                 .takes_value(true)
                                 .help("Sets the output file to use")
                                 .required(true)))
        .get_matches();
    match matches.subcommand() {
        ("run", Some(args)) => {
            let input_fname = args.value_of("INPUT").expect("input filename");
            let items = runner::get_words_fn(input_fname);
            let words = word_parser(items);
            runner::run_beatnik(&words);
        }
        ("wottasquare", Some(args)) => {
            let input_fname = args.value_of("INPUT").expect("input filename");
            let items = runner::get_wottas_fn(input_fname);
            let words = word_parser(items);
            runner::run_beatnik(&words);
        }
        ("wottasquare-dumper", Some(args)) => {
            let input_fname = args.value_of("INPUT").expect("input filename");
            let items = runner::get_words_fn(input_fname);
            let words = word_parser(items);
            runner::output_wottasquare(words);
        }
        ("generate-markov", Some(args)) => {
            let input_fname = args.value_of("INPUT").unwrap();
            let markov = markov::generate_markov(input_fname).expect("markov");
            let output_fname = args.value_of("OUTPUT").expect("output name");
            let mut buffer = File::create(output_fname).unwrap();
            serde_yaml::to_writer(&mut buffer, &markov).unwrap();
        }
        ("markov-beatnik", Some(args)) => {
            let input_fname = args.value_of("INPUT").unwrap();
            let markov_fname = args.value_of("MARKOV").unwrap();
            let words = runner::get_wottas_fn(input_fname).expect("wottasquare data");
            let markov_data = markov::read_markov(markov_fname).expect("markov data");
            let markov_out = markov::make_beatnik(&words, &markov_data).expect("markov");
            let output_fname = args.value_of("OUTPUT").expect("output name");
            let mut buffer = File::create(output_fname).unwrap();
            buffer.write(markov_out.as_bytes()).unwrap();
        }
        _ => panic!("No command"),
    }
}

#[cfg(test)]
mod tests {
    use markov;
    use quickcheck::TestResult;
    use runner;

    quickcheck!{
        fn wotta_two_way(xs: Vec<u8>, source_words: String) -> TestResult {
            let mut info = String::new();
            info.push_str(&format!("Source_words: {:?}\n", source_words));
            let mut markov = markov::MarkovInfo::new();
            let tokens = markov::get_tokens(&source_words).unwrap();
            let mut last = markov::Token::Begin.string();
            for token in tokens {
                let str_token = token.string();
                markov.add_token(last, &str_token);
                last = str_token;
            }
            info.push_str(&format!("Markov: {:?}\n", markov));

            let words = xs.iter()
                .filter(|x| *x !=&0)
                .map(|x| runner::Word{score:*x, word:String::from("")})
                .collect();
            info.push_str(&format!("Score in: {:?}\n", xs));
            let markov_out = markov::make_beatnik(&words, &markov).unwrap();
            info.push_str(&format!("Markov out: {:?}\n", markov_out));
            let words_out = runner::get_words(&markov_out).unwrap();
            info.push_str(&format!("Words out: {:?}\n", words_out));
            let score_out: Vec<u8> = words_out.iter().map(|x|x.score).filter(|x| x !=&0).collect();
            info.push_str(&format!("Score out: {:?}\n", score_out));
            let res = xs.into_iter().filter(|x| x != &0).collect::<Vec<u8>>() == score_out;
            info.push_str(&format!("Res: {}\n", res));
            if res {
                TestResult::passed()
            }
            else {
                TestResult::error(info)
            }
        }
    }
}
<commit_msg>Add "no command" usage handler<commit_after>#[macro_use]
extern crate nom;
#[macro_use]
extern crate log;
extern crate env_logger;
#[macro_use]
extern crate enum_primitive;
extern crate num_traits;
extern crate clap;
extern crate serde_yaml;
extern crate rand;

#[macro_use]
extern crate serde_derive;

#[cfg(test)]
#[macro_use]
extern crate quickcheck;

mod runner;
mod markov;
mod common;

use clap::{App, Arg, SubCommand};
use std::fs::File;
use std::io::{self, Write};
use std::str;

fn word_parser(items: Result<Vec<runner::Word>, io::Error>) -> Vec<runner::Word> {
    match items {
        Ok(val) => val,
        Err(err) => {
            if err.kind() == io::ErrorKind::InvalidData {
                println!("{}", err.get_ref().unwrap());
                std::process::exit(-1);
            } else {
                panic!("Error during parsing: {}", err);
            }
        }
    }
}

fn main() {
    env_logger::init().unwrap();
    let app = App::new("peacenik")
        .version("1.0")
        .author("Tom Parker <palfrey@tevp.net>")
        .about("Beatnik language tools")
        .subcommand(SubCommand::with_name("run")
                        .about("Beatnik interpreter")
                        .arg(Arg::with_name("INPUT")
                                 .help("Sets the input file to use")
                                 .required(true)
                                 .index(1)))
        .subcommand(SubCommand::with_name("wottasquare")
                        .about("Wottasquare interpreter")
                        .arg(Arg::with_name("INPUT")
                                 .help("Sets the input file to use")
                                 .required(true)
                                 .index(1)))
        .subcommand(SubCommand::with_name("wottasquare-dumper")
                        .about("Wottasquare dumper")
                        .arg(Arg::with_name("INPUT")
                                 .help("Sets the input file to use")
                                 .required(true)
                                 .index(1)))
        .subcommand(SubCommand::with_name("generate-markov")
                        .about("Markov chain generator")
                        .arg(Arg::with_name("INPUT")
                                 .short("i")
                                 .takes_value(true)
                                 .help("Sets the input file to use")
                                 .required(true))
                        .arg(Arg::with_name("OUTPUT")
                                 .short("o")
                                 .takes_value(true)
                                 .help("Sets the output file to use")
                                 .required(true)))
        .subcommand(SubCommand::with_name("markov-beatnik")
                        .about("Beatnik from Wottasquare using Markov")
                        .arg(Arg::with_name("INPUT")
                                 .short("i")
                                 .takes_value(true)
                                 .help("Sets the input file to use")
                                 .required(true))
                        .arg(Arg::with_name("MARKOV")
                                 .short("m")
                                 .takes_value(true)
                                 .help("Sets the markov file to use")
                                 .required(true))
                        .arg(Arg::with_name("OUTPUT")
                                 .short("o")
                                 .takes_value(true)
                                 .help("Sets the output file to use")
                                 .required(true)));
    let mut usage = Vec::new();
    app.write_help(&mut usage).unwrap();
    match app.get_matches().subcommand() {
        ("run", Some(args)) => {
            let input_fname = args.value_of("INPUT").expect("input filename");
            let items = runner::get_words_fn(input_fname);
            let words = word_parser(items);
            runner::run_beatnik(&words);
        }
        ("wottasquare", Some(args)) => {
            let input_fname = args.value_of("INPUT").expect("input filename");
            let items = runner::get_wottas_fn(input_fname);
            let words = word_parser(items);
            runner::run_beatnik(&words);
        }
        ("wottasquare-dumper", Some(args)) => {
            let input_fname = args.value_of("INPUT").expect("input filename");
            let items = runner::get_words_fn(input_fname);
            let words = word_parser(items);
            runner::output_wottasquare(words);
        }
        ("generate-markov", Some(args)) => {
            let input_fname = args.value_of("INPUT").unwrap();
            let markov = markov::generate_markov(input_fname).expect("markov");
            let output_fname = args.value_of("OUTPUT").expect("output name");
            let mut buffer = File::create(output_fname).unwrap();
            serde_yaml::to_writer(&mut buffer, &markov).unwrap();
        }
        ("markov-beatnik", Some(args)) => {
            let input_fname = args.value_of("INPUT").unwrap();
            let markov_fname = args.value_of("MARKOV").unwrap();
            let words = runner::get_wottas_fn(input_fname).expect("wottasquare data");
            let markov_data = markov::read_markov(markov_fname).expect("markov data");
            let markov_out = markov::make_beatnik(&words, &markov_data).expect("markov");
            let output_fname = args.value_of("OUTPUT").expect("output name");
            let mut buffer = File::create(output_fname).unwrap();
            buffer.write(markov_out.as_bytes()).unwrap();
        }
        _ => {
            println!("No command!\n");
            println!("{}", str::from_utf8(&usage).unwrap());
        }
    }
}

#[cfg(test)]
mod tests {
    use markov;
    use quickcheck::TestResult;
    use runner;

    quickcheck!{
        fn wotta_two_way(xs: Vec<u8>, source_words: String) -> TestResult {
            let mut info = String::new();
            info.push_str(&format!("Source_words: {:?}\n", source_words));
            let mut markov = markov::MarkovInfo::new();
            let tokens = markov::get_tokens(&source_words).unwrap();
            let mut last = markov::Token::Begin.string();
            for token in tokens {
                let str_token = token.string();
                markov.add_token(last, &str_token);
                last = str_token;
            }
            info.push_str(&format!("Markov: {:?}\n", markov));

            let words = xs.iter()
                .filter(|x| *x !=&0)
                .map(|x| runner::Word{score:*x, word:String::from("")})
                .collect();
            info.push_str(&format!("Score in: {:?}\n", xs));
            let markov_out = markov::make_beatnik(&words, &markov).unwrap();
            info.push_str(&format!("Markov out: {:?}\n", markov_out));
            let words_out = runner::get_words(&markov_out).unwrap();
            info.push_str(&format!("Words out: {:?}\n", words_out));
            let score_out: Vec<u8> = words_out.iter().map(|x|x.score).filter(|x| x !=&0).collect();
            info.push_str(&format!("Score out: {:?}\n", score_out));
            let res = xs.into_iter().filter(|x| x != &0).collect::<Vec<u8>>() == score_out;
            info.push_str(&format!("Res: {}\n", res));
            if res {
                TestResult::passed()
            }
            else {
                TestResult::error(info)
            }
        }
    }
}
<|endoftext|>


<commit_before>// ZSnapMgr :: ZFS snapshot and backup manager
//
// Copyright (c) 2016 by William R. Fraser
//

use std::env;
use std::iter::Iterator;
use std::io;
use std::io::{Read, Write};
use std::ops::Deref;
use std::process;

extern crate termios;
use termios::*;

extern crate zsnapmgr;
use zsnapmgr::ZSnapMgr;

mod table;
use table::Table;

mod backups;
use backups::{Backup, Backups};

static USE_SUDO: bool = true;

// Print and flush.
macro_rules! printf {
    ( $fmt:expr ) => {
        print!($fmt);
        io::stdout().flush().unwrap();
    };
    ( $fmt:expr, $($arg:tt)* ) => {
        print!($fmt, $($arg)*);
        io::stdout().flush().unwrap();
    }
}

trait OptionDeref<T: Deref> {
    fn as_deref(&self) -> Option<&T::Target>;
}

impl<T: Deref> OptionDeref<T> for Option<T> {
    fn as_deref(&self) -> Option<&T::Target> {
        self.as_ref().map(Deref::deref)
    }
}

trait VecDeref<T: Deref> {
    fn as_deref(&self) -> Vec<&T::Target>;
}

impl<T: Deref> VecDeref<T> for Vec<T> {
    fn as_deref(&self) -> Vec<&T::Target> {
        self.iter().map(Deref::deref).collect()
    }
}

fn enumerate_files(path: &str) -> Box<Iterator<Item = String>> {
    let out = std::fs::read_dir(std::path::Path::new(path))
                  .expect("error reading directory")
                  .map(|entry_result| entry_result.expect("error enumerating files"))
                  .filter(|entry| {
                      match entry.file_type() {
                          Ok(ft) => ft.is_file(),
                          Err(e) => {
                              println!("error getting file type of \"{}\": {}",
                                       entry.path().to_string_lossy(),
                                       e);
                              false
                          }
                      }
                  })
                  .map(|entry| entry.file_name().to_string_lossy().into_owned());
    Box::new(out)
}

fn gather_volumes(path: &str) -> Vec<Backup> {
    let z = ZSnapMgr::new(USE_SUDO);
    let snapshots: Vec<String> = match z.get_snapshots(None) {
        Ok(s) => s,
        Err(e) => {
            println!("Error getting snapshots from ZFS: {}", e);
            return vec![];
        }
    };

    let volumes: Vec<String> = match z.get_volumes() {
        Ok(v) => v,
        Err(e) => {
            println!("Error getting volumes from ZFS: {}", e);
            return vec![];
        }
    };

    let mut backups = Backups::new();

    for filename in enumerate_files(path) {
        if let Some(zfs_pos) = filename.find(".zfs") {
            if !(&filename).ends_with("_partial") {
                let parts = filename[0..zfs_pos].splitn(2, '@').collect::<Vec<&str>>();
                let volume_name = parts[0].replace("_", "/");
                let backup_snap = parts[1];

                if volumes.binary_search(&volume_name).is_ok() {
                    backups.insert(parts[0].to_string(),
                                   volume_name.to_string(),
                                   Some(backup_snap.to_string()));
                } else {
                    let volume_name_mod = "/".to_string() + &volume_name;
                    let matches: Vec<&str> = volumes.iter()
                                                    .filter(|ref vol| {
                                                        vol.ends_with(&volume_name_mod)
                                                    })
                                                    .map(Deref::deref)
                                                    .collect();

                    if matches.len() == 1 {
                        backups.insert(volume_name.to_string(),
                                       matches[0].to_string(),
                                       Some(backup_snap.to_string()));
                    } else {
                        print!("Backup filename \"{}\" ", filename);
                        if matches.len() > 1 {
                            println!("matches more than one volume.\nIt could be any of: {:?}",
                                     matches);
                        } else {
                            println!("doesn't match any volumes.");
                        }
                        println!("Skipping it.\n");
                    }
                }
            }
        }
    }

    // Now fill in the latest snapshot available for each volume in the proposed backups.
    for backup in backups.iter_mut() {
        let volume_at = backup.volume.clone() + "@";
        let volume_snaps: Vec<&str> = snapshots.iter()
                                               .filter(|snap| snap.starts_with(&volume_at))
                                               .map(Deref::deref)
                                               .collect();

        if backup.start_snapshot.is_some() {
            // Check that the start snapshot still exists.
            let start_snapshot = volume_at.clone() + backup.start_snapshot.as_ref().unwrap();

            if !volume_snaps.binary_search(&start_snapshot.deref()).is_ok() {
                println!("Snapshot \"{}\" doesn't exist any more; doing full backup instead.\n",
                         start_snapshot);
                backup.start_snapshot = None;
            }
        }

        let last_snapshot: &str = volume_snaps.last().unwrap().splitn(2, '@').last().unwrap();

        if backup.start_snapshot
                 .as_ref()
                 .and_then(|start| Some(start != last_snapshot))
                 .unwrap_or(false) {
            backup.end_snapshot = Some(last_snapshot.to_string());
        } else {
            println!("Backup of \"{}\" is up to date (@{}). Skipping.\n",
                     backup.volume,
                     backup.start_snapshot.as_ref().unwrap());
            backup.end_snapshot = None;
        }
    }

    backups.values()
}

fn getpass(prompt: &str) -> io::Result<String> {
    let mut termios = Termios::from_fd(0).expect("failed to get termios settings");

    let old = termios.c_lflag;
    termios.c_lflag &= !ECHO;   // disable echo
    termios.c_lflag &= !ICANON; // disable line-buffering
    tcsetattr(0, TCSAFLUSH, &mut termios).expect("failed to set termios settings");

    printf!("{}", prompt);

    let stdin = io::stdin();
    let mut bytes = stdin.lock().bytes();
    let mut line = String::new();
    let mut utf8 = Vec::<u8>::new();
    loop {
        match bytes.next().or(Some(Err(io::Error::new(io::ErrorKind::Other, "EOF in getpass!")))) {
            Some(Ok(byte)) => {
                // 0x4 is EOT; aka ctrl-D
                if byte == 0x4 && utf8.is_empty() {
                    return Err(io::Error::new(io::ErrorKind::Other, "EOF in getpass!"));
                }

                utf8.push(byte);

                let mut valid_utf8 = false;
                if let Ok(c) = std::str::from_utf8(&utf8) {
                    if c == "\n" {
                        printf!("\n");
                        break;
                    } else {
                        valid_utf8 = true;
                        line.push_str(c);
                        printf!("*");
                    }
                }

                if valid_utf8 {
                    utf8.clear();
                }
            }
            Some(Err(e)) => {
                return Err(e);
            }
            _ => unreachable!(),
        }
    }

    termios.c_lflag = old;
    tcsetattr(0, TCSAFLUSH, &mut termios).expect("failed to reset termios settings");

    Ok(line)
}

fn do_backups(backups: &Vec<Backup>, path: &str) {
    if backups.is_empty() {
        println!("Nothing to do.");
        return;
    }

    let passphrase: String;
    loop {
        let pass1 = getpass("GPG passphrase: ").unwrap();
        let pass2 = getpass("again: ").unwrap();
        if pass1 != pass2 {
            println!("Passphrases do not match.");
        } else {
            passphrase = pass1;
            break;
        }
    }

    for backup in backups {
        let z = ZSnapMgr::new(USE_SUDO);

        let snapshot = format!("{}@{}",
                               backup.volume,
                               backup.end_snapshot.as_deref().unwrap());

        println!("\nBacking up: {}", snapshot);

        z.backup(path,
                 &snapshot,
                 &passphrase,
                 backup.start_snapshot.as_deref())
         .err()
         .and_then(|e| {
             println!("failed backup of {}: {}", backup.volume, e);
             Some(())
         });
    }
}

fn interactive_backup(backups_dir: &str) {
    let z = ZSnapMgr::new(USE_SUDO);
    let mut volumes: Vec<Backup> = gather_volumes(backups_dir);
    loop {
        let mut table = Table::new(&vec!["_", "volume", "incremental", "snapshot date"]);
        for i in 0..volumes.len() {
            let start = if volumes[i].start_snapshot.is_none() {
                "full backup".to_string()
            } else {
                volumes[i].start_snapshot.as_ref().unwrap().clone()
            };

            table.push(vec![(i + 1).to_string(),
                            volumes[i].volume.clone(),
                            start,
                            volumes[i].end_snapshot.as_ref().unwrap().clone()]);
        }

        println!("Volumes to backup:\n{}", table);

        printf!(concat!("Enter a number to make changes,\n",
                         "\t'+' to add a volume,\n",
                         "\t'-' to remove one,\n",
                         "\t'd' to change all dates,\n",
                         "\tor <return> to start backup: "));

        let mut input = String::new();
        match io::stdin().read_line(&mut input) {
            Ok(bytes_read) => {
                if bytes_read == 0 {
                    panic!("EOF");
                }
                input.pop();
            }
            Err(e) => panic!(e),
        }

        if input == "+" {
            printf!("Volume: ");

            let mut vol = String::new();
            io::stdin().read_line(&mut vol).unwrap();
            vol.pop();

            let latest_snap: String;
            match z.get_snapshots(Some(&vol))
                   .and_then(|ref mut snaps| {
                       Ok(snaps.pop())
                    }) {
                Ok(Some(snap)) => {
                    latest_snap = snap;
                }
                Ok(None) => {
                    println!("No snapshots available for that volume.\n");
                    continue;
                }
                Err(e) => {
                    println!("Error listing snapshots: {}\n", e);
                    continue;
                }
            }

            volumes.push(Backup {
                filename_base: vol.replace("/", "_").to_string(),
                volume: vol.clone(),
                start_snapshot: None,
                end_snapshot: Some(latest_snap),
            });

        } else if input.starts_with("-") {

            let index: usize;

            if input.len() > 1 {
                match input[1..].parse::<usize>() {
                    Ok(n) => {
                        index = n;
                    },
                    Err(e) => {
                        println!("Invalid number: {}\n", e);
                        continue;
                    }
                }
            } else {
                printf!("Remove which one?: ");

                input.clear();
                io::stdin().read_line(&mut input).unwrap();
                input.pop();

                match input.parse::<usize>() {
                    Ok(n) => {
                        index = n;
                    },
                    Err(e) => {
                        println!("Invalid number: {}\n", e);
                        continue;
                    }
                }
            }

            if volumes.len() < index || index == 0 {
                println!("Number out of range.\n");
                continue;
            }

            volumes.remove(index - 1);

        } else if input.starts_with("d") || input.starts_with("D") {

            printf!("Snapshot date (yyyy-MM-dd): ");

            let mut date = String::new();
            io::stdin().read_line(&mut date).unwrap();
            date.pop();

            for volume in volumes.iter_mut() {
                volume.end_snapshot = Some(date.clone());
                //TODO: check that the snapshot exists for that volume
            }

        } else if input.is_empty() {
            println!("Starting backups.\n");
            do_backups(&volumes, backups_dir);
            break;
        } else {
            let index: usize;

            match input.parse::<usize>() {
                Ok(n) => {
                    index = n;
                }
                Err(e) => {
                    println!("Invalid number: {}\n", e);
                    continue;
                }
            }

            if volumes.len() < index || index == 0 {
                println!("Number out of range.\n");
                continue;
            }

            let vol = volumes.get_mut(index - 1).unwrap();

            printf!("Change (I)ncremental starting snapshot, (S)napshot date: ");

            input.clear();
            io::stdin().read_line(&mut input).unwrap();
            input.pop();

            let start: bool;
            if input == "I" || input == "i" {
                start = true;
            } else if input == "S" || input == "s" {
                start = false;
            } else {
                println!("Invalid selection.\n");
                continue;
            }

            printf!("Date (yyyy-MM-dd): ");

            input.clear();
            io::stdin().read_line(&mut input).unwrap();
            input.pop();

            if start {
                vol.start_snapshot = Some(input.clone());
            } else {
                vol.end_snapshot = Some(input.clone());
            }
        }
    }
}

fn snapshot_automanage() {
    let z = ZSnapMgr::new(USE_SUDO);
    z.snapshot_automanage().unwrap();
}

fn main() {
    let args: Vec<String> = env::args().collect();
    let program_name = &args[0].rsplitn(2, '/').next().unwrap();

    let command = if args.len() < 2 {
        "help"
    } else {
        args[1].as_ref()
    };

    match command {
        "backup" => {
            if args.len() == 3 {
                interactive_backup(&args[2]);
            } else {
                println!("usage: {} backup <backups_location>", program_name);
                process::exit(-1);
            }
        }
        "automanage" => {
            snapshot_automanage();
        }
        _ => {
            if command != "help" {
                println!("unknown command \"{}\"", command);
            }
            println!("usage: {} <backup | automanage> [options]", program_name);
            process::exit(-1);
        }
    }
}
<commit_msg>allow modifying entries to make them full backups<commit_after>// ZSnapMgr :: ZFS snapshot and backup manager
//
// Copyright (c) 2016 by William R. Fraser
//

use std::env;
use std::iter::Iterator;
use std::io;
use std::io::{Read, Write};
use std::ops::Deref;
use std::process;

extern crate termios;
use termios::*;

extern crate zsnapmgr;
use zsnapmgr::ZSnapMgr;

mod table;
use table::Table;

mod backups;
use backups::{Backup, Backups};

static USE_SUDO: bool = true;

// Print and flush.
macro_rules! printf {
    ( $fmt:expr ) => {
        print!($fmt);
        io::stdout().flush().unwrap();
    };
    ( $fmt:expr, $($arg:tt)* ) => {
        print!($fmt, $($arg)*);
        io::stdout().flush().unwrap();
    }
}

trait OptionDeref<T: Deref> {
    fn as_deref(&self) -> Option<&T::Target>;
}

impl<T: Deref> OptionDeref<T> for Option<T> {
    fn as_deref(&self) -> Option<&T::Target> {
        self.as_ref().map(Deref::deref)
    }
}

trait VecDeref<T: Deref> {
    fn as_deref(&self) -> Vec<&T::Target>;
}

impl<T: Deref> VecDeref<T> for Vec<T> {
    fn as_deref(&self) -> Vec<&T::Target> {
        self.iter().map(Deref::deref).collect()
    }
}

fn enumerate_files(path: &str) -> Box<Iterator<Item = String>> {
    let out = std::fs::read_dir(std::path::Path::new(path))
                  .expect("error reading directory")
                  .map(|entry_result| entry_result.expect("error enumerating files"))
                  .filter(|entry| {
                      match entry.file_type() {
                          Ok(ft) => ft.is_file(),
                          Err(e) => {
                              println!("error getting file type of \"{}\": {}",
                                       entry.path().to_string_lossy(),
                                       e);
                              false
                          }
                      }
                  })
                  .map(|entry| entry.file_name().to_string_lossy().into_owned());
    Box::new(out)
}

fn gather_volumes(path: &str) -> Vec<Backup> {
    let z = ZSnapMgr::new(USE_SUDO);
    let snapshots: Vec<String> = match z.get_snapshots(None) {
        Ok(s) => s,
        Err(e) => {
            println!("Error getting snapshots from ZFS: {}", e);
            return vec![];
        }
    };

    let volumes: Vec<String> = match z.get_volumes() {
        Ok(v) => v,
        Err(e) => {
            println!("Error getting volumes from ZFS: {}", e);
            return vec![];
        }
    };

    let mut backups = Backups::new();

    for filename in enumerate_files(path) {
        if let Some(zfs_pos) = filename.find(".zfs") {
            if !(&filename).ends_with("_partial") {
                let parts = filename[0..zfs_pos].splitn(2, '@').collect::<Vec<&str>>();
                let volume_name = parts[0].replace("_", "/");
                let backup_snap = parts[1];

                if volumes.binary_search(&volume_name).is_ok() {
                    backups.insert(parts[0].to_string(),
                                   volume_name.to_string(),
                                   Some(backup_snap.to_string()));
                } else {
                    let volume_name_mod = "/".to_string() + &volume_name;
                    let matches: Vec<&str> = volumes.iter()
                                                    .filter(|ref vol| {
                                                        vol.ends_with(&volume_name_mod)
                                                    })
                                                    .map(Deref::deref)
                                                    .collect();

                    if matches.len() == 1 {
                        backups.insert(volume_name.to_string(),
                                       matches[0].to_string(),
                                       Some(backup_snap.to_string()));
                    } else {
                        print!("Backup filename \"{}\" ", filename);
                        if matches.len() > 1 {
                            println!("matches more than one volume.\nIt could be any of: {:?}",
                                     matches);
                        } else {
                            println!("doesn't match any volumes.");
                        }
                        println!("Skipping it.\n");
                    }
                }
            }
        }
    }

    // Now fill in the latest snapshot available for each volume in the proposed backups.
    for backup in backups.iter_mut() {
        let volume_at = backup.volume.clone() + "@";
        let volume_snaps: Vec<&str> = snapshots.iter()
                                               .filter(|snap| snap.starts_with(&volume_at))
                                               .map(Deref::deref)
                                               .collect();

        if backup.start_snapshot.is_some() {
            // Check that the start snapshot still exists.
            let start_snapshot = volume_at.clone() + backup.start_snapshot.as_ref().unwrap();

            if !volume_snaps.binary_search(&start_snapshot.deref()).is_ok() {
                println!("Snapshot \"{}\" doesn't exist any more; doing full backup instead.\n",
                         start_snapshot);
                backup.start_snapshot = None;
            }
        }

        let last_snapshot: &str = volume_snaps.last().unwrap().splitn(2, '@').last().unwrap();

        if backup.start_snapshot
                 .as_ref()
                 .and_then(|start| Some(start != last_snapshot))
                 .unwrap_or(false) {
            backup.end_snapshot = Some(last_snapshot.to_string());
        } else {
            println!("Backup of \"{}\" is up to date (@{}). Skipping.\n",
                     backup.volume,
                     backup.start_snapshot.as_ref().unwrap());
            backup.end_snapshot = None;
        }
    }

    backups.values()
}

fn getpass(prompt: &str) -> io::Result<String> {
    let mut termios = Termios::from_fd(0).expect("failed to get termios settings");

    let old = termios.c_lflag;
    termios.c_lflag &= !ECHO;   // disable echo
    termios.c_lflag &= !ICANON; // disable line-buffering
    tcsetattr(0, TCSAFLUSH, &mut termios).expect("failed to set termios settings");

    printf!("{}", prompt);

    let stdin = io::stdin();
    let mut bytes = stdin.lock().bytes();
    let mut line = String::new();
    let mut utf8 = Vec::<u8>::new();
    loop {
        match bytes.next().or(Some(Err(io::Error::new(io::ErrorKind::Other, "EOF in getpass!")))) {
            Some(Ok(byte)) => {
                // 0x4 is EOT; aka ctrl-D
                if byte == 0x4 && utf8.is_empty() {
                    return Err(io::Error::new(io::ErrorKind::Other, "EOF in getpass!"));
                }

                utf8.push(byte);

                let mut valid_utf8 = false;
                if let Ok(c) = std::str::from_utf8(&utf8) {
                    if c == "\n" {
                        printf!("\n");
                        break;
                    } else {
                        valid_utf8 = true;
                        line.push_str(c);
                        printf!("*");
                    }
                }

                if valid_utf8 {
                    utf8.clear();
                }
            }
            Some(Err(e)) => {
                return Err(e);
            }
            _ => unreachable!(),
        }
    }

    termios.c_lflag = old;
    tcsetattr(0, TCSAFLUSH, &mut termios).expect("failed to reset termios settings");

    Ok(line)
}

fn do_backups(backups: &Vec<Backup>, path: &str) {
    if backups.is_empty() {
        println!("Nothing to do.");
        return;
    }

    let passphrase: String;
    loop {
        let pass1 = getpass("GPG passphrase: ").unwrap();
        let pass2 = getpass("again: ").unwrap();
        if pass1 != pass2 {
            println!("Passphrases do not match.");
        } else {
            passphrase = pass1;
            break;
        }
    }

    for backup in backups {
        let z = ZSnapMgr::new(USE_SUDO);

        let snapshot = format!("{}@{}",
                               backup.volume,
                               backup.end_snapshot.as_deref().unwrap());

        println!("\nBacking up: {}", snapshot);

        z.backup(path,
                 &snapshot,
                 &passphrase,
                 backup.start_snapshot.as_deref())
         .err()
         .and_then(|e| {
             println!("failed backup of {}: {}", backup.volume, e);
             Some(())
         });
    }
}

fn interactive_backup(backups_dir: &str) {
    let z = ZSnapMgr::new(USE_SUDO);
    let mut volumes: Vec<Backup> = gather_volumes(backups_dir);
    loop {
        let mut table = Table::new(&vec!["_", "volume", "incremental", "snapshot date"]);
        for i in 0..volumes.len() {
            let start = if volumes[i].start_snapshot.is_none() {
                "full backup".to_string()
            } else {
                volumes[i].start_snapshot.as_ref().unwrap().clone()
            };

            table.push(vec![(i + 1).to_string(),
                            volumes[i].volume.clone(),
                            start,
                            volumes[i].end_snapshot.as_ref().unwrap().clone()]);
        }

        println!("Volumes to backup:\n{}", table);

        printf!(concat!("Enter a number to make changes,\n",
                         "\t'+' to add a volume,\n",
                         "\t'-' to remove one,\n",
                         "\t'd' to change all dates,\n",
                         "\tor <return> to start backup: "));

        let mut input = String::new();
        match io::stdin().read_line(&mut input) {
            Ok(bytes_read) => {
                if bytes_read == 0 {
                    panic!("EOF");
                }
                input.pop();
            }
            Err(e) => panic!(e),
        }

        if input == "+" {
            printf!("Volume: ");

            let mut vol = String::new();
            io::stdin().read_line(&mut vol).unwrap();
            vol.pop();

            let latest_snap: String;
            match z.get_snapshots(Some(&vol))
                   .and_then(|ref mut snaps| {
                       Ok(snaps.pop())
                    }) {
                Ok(Some(snap)) => {
                    latest_snap = snap;
                }
                Ok(None) => {
                    println!("No snapshots available for that volume.\n");
                    continue;
                }
                Err(e) => {
                    println!("Error listing snapshots: {}\n", e);
                    continue;
                }
            }

            volumes.push(Backup {
                filename_base: vol.replace("/", "_").to_string(),
                volume: vol.clone(),
                start_snapshot: None,
                end_snapshot: Some(latest_snap),
            });

        } else if input.starts_with("-") {

            let index: usize;

            if input.len() > 1 {
                match input[1..].parse::<usize>() {
                    Ok(n) => {
                        index = n;
                    },
                    Err(e) => {
                        println!("Invalid number: {}\n", e);
                        continue;
                    }
                }
            } else {
                printf!("Remove which one?: ");

                input.clear();
                io::stdin().read_line(&mut input).unwrap();
                input.pop();

                match input.parse::<usize>() {
                    Ok(n) => {
                        index = n;
                    },
                    Err(e) => {
                        println!("Invalid number: {}\n", e);
                        continue;
                    }
                }
            }

            if volumes.len() < index || index == 0 {
                println!("Number out of range.\n");
                continue;
            }

            volumes.remove(index - 1);

        } else if input.starts_with("d") || input.starts_with("D") {

            printf!("Snapshot date (yyyy-MM-dd): ");

            let mut date = String::new();
            io::stdin().read_line(&mut date).unwrap();
            date.pop();

            for volume in volumes.iter_mut() {
                volume.end_snapshot = Some(date.clone());
                //TODO: check that the snapshot exists for that volume
            }

        } else if input.is_empty() {
            println!("Starting backups.\n");
            do_backups(&volumes, backups_dir);
            break;
        } else {
            let index: usize;

            match input.parse::<usize>() {
                Ok(n) => {
                    index = n;
                }
                Err(e) => {
                    println!("Invalid number: {}\n", e);
                    continue;
                }
            }

            if volumes.len() < index || index == 0 {
                println!("Number out of range.\n");
                continue;
            }

            let vol = volumes.get_mut(index - 1).unwrap();

            printf!("Change (I)ncremental starting snapshot, (S)napshot date: ");

            input.clear();
            io::stdin().read_line(&mut input).unwrap();
            input.pop();

            let start: bool;
            if input == "I" || input == "i" {
                start = true;
            } else if input == "S" || input == "s" {
                start = false;
            } else {
                println!("Invalid selection.\n");
                continue;
            }

            printf!("Date (yyyy-MM-dd");
            if start {
                printf!(" or 'none' for full backup): ");
            } else {
                printf!("): ");
            }

            input.clear();
            io::stdin().read_line(&mut input).unwrap();
            input.pop();

            if start {
                vol.start_snapshot = if input == "none" {
                    None
                } else {
                    Some(input)
                }
            } else {
                vol.end_snapshot = Some(input);
            }
        }
    }
}

fn snapshot_automanage() {
    let z = ZSnapMgr::new(USE_SUDO);
    z.snapshot_automanage().unwrap();
}

fn main() {
    let args: Vec<String> = env::args().collect();
    let program_name = &args[0].rsplitn(2, '/').next().unwrap();

    let command = if args.len() < 2 {
        "help"
    } else {
        args[1].as_ref()
    };

    match command {
        "backup" => {
            if args.len() == 3 {
                interactive_backup(&args[2]);
            } else {
                println!("usage: {} backup <backups_location>", program_name);
                process::exit(-1);
            }
        }
        "automanage" => {
            snapshot_automanage();
        }
        _ => {
            if command != "help" {
                println!("unknown command \"{}\"", command);
            }
            println!("usage: {} <backup | automanage> [options]", program_name);
            process::exit(-1);
        }
    }
}
<|endoftext|>


<commit_before>#[cfg(windows)]
extern crate winapi;

#[macro_use]
extern crate clap;
#[macro_use]
extern crate failure;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;

extern crate base64;
extern crate chrono;
extern crate directories;
extern crate regex;
extern crate reqwest;
extern crate serde;
extern crate toml;

mod backend;
mod config;
mod error;
mod input;
mod process;
mod series;
mod util;

use backend::{anilist::AniList, SyncBackend};
use config::Config;
use error::{Error, SeriesError};
use series::{EpisodeData, SaveData, Series};
use std::path::PathBuf;

fn main() {
    match run() {
        Ok(_) => (),
        Err(Error::Series(SeriesError::RequestExit)) => (),
        Err(e) => {
            let e: failure::Error = e.into();
            eprintln!("fatal error: {}", e.cause());

            for cause in e.causes().skip(1) {
                eprintln!("cause: {}", cause);
            }

            eprintln!("{}", e.backtrace());
            std::process::exit(1);
        }
    }
}

fn run() -> Result<(), Error> {
    let args = clap_app!(anup =>
        (version: env!("CARGO_PKG_VERSION"))
        (author: env!("CARGO_PKG_AUTHORS"))
        (@arg NAME: "The name of the series to watch")
        (@arg PATH: -p --path +takes_value "Specifies the directory to look for video files in")
        (@arg SEASON: -s --season +takes_value "Specifies which season you want to watch")
        (@arg INFO: -i --info "Displays saved series information")
        (@arg OFFLINE: -o --offline "Launches the program in offline mode")
        (@arg SYNC: --sync "Synchronizes all changes made offline to AniList")
    ).get_matches();

    if args.is_present("INFO") {
        print_saved_series_info()
    } else if args.is_present("SYNC") {
        sync_offline_changes()
    } else {
        watch_series(&args)
    }
}

fn watch_series(args: &clap::ArgMatches) -> Result<(), Error> {
    let mut config = Config::load()?;
    config.remove_invalid_series();

    let path = get_series_path(&mut config, args)?;
    let offline_mode = args.is_present("OFFLINE");

    let sync_backend = AniList::init(offline_mode, &mut config)?;

    config.save()?;

    let season = {
        let value: usize = args
            .value_of("SEASON")
            .and_then(|s| s.parse().ok())
            .unwrap_or(0);

        value.saturating_sub(1)
    };

    Series::load(offline_mode, &path, sync_backend)?
        .load_season(season)?
        .play_all_episodes()?;

    Ok(())
}

fn print_saved_series_info() -> Result<(), Error> {
    let mut config = Config::load()?;
    config.remove_invalid_series();

    println!("found [{}] series:\n", config.series.len());

    for (name, path) in config.series {
        let ep_matcher = match SaveData::from_dir(&path) {
            Ok(save_data) => save_data.episode_matcher,
            _ => None,
        };

        let ep_data = match EpisodeData::parse_dir(&path, ep_matcher) {
            Ok(ep_data) => ep_data,
            Err(err) => {
                eprintln!("failed to parse episode data for [{}]: {}", name, err);
                continue;
            }
        };

        let mut ep_nums = ep_data.episodes.keys().cloned().collect::<Vec<_>>();
        ep_nums.sort_unstable();

        let ep_list =
            util::concat_sequential_values(&ep_nums, "..", " | ").unwrap_or_else(|| "none".into());

        println!("[{}]:", name);
        println!(
            "  path: {}\n  episodes on disk: {}",
            path.to_string_lossy(),
            ep_list
        );
    }

    Ok(())
}

fn sync_offline_changes() -> Result<(), Error> {
    let mut config = Config::load()?;
    config.remove_invalid_series();

    let backend = AniList::init(false, &mut config)?;

    for (_, path) in config.series {
        let mut save_data = match SaveData::from_dir(&path) {
            Ok(save_data) => save_data,
            Err(_) => continue,
        };

        for season_num in 0..save_data.season_states.len() {
            let season_state = &mut save_data.season_states[season_num];

            if !season_state.needs_sync {
                continue;
            }

            println!("[{}] syncing..", season_state.state.info.title);

            if season_state.needs_info {
                let ep_data = EpisodeData::parse_dir(&path, save_data.episode_matcher.clone())?;

                season_state.state.info =
                    series::search_for_series_info(&backend, &ep_data.series_name, season_num)?;

                season_state.needs_info = false;
            }

            backend.update_list_entry(&season_state.state)?;
            season_state.needs_sync = false;
        }

        save_data.write_to_file()?;
    }

    Ok(())
}

fn get_series_path(config: &mut Config, args: &clap::ArgMatches) -> Result<PathBuf, Error> {
    match args.value_of("PATH") {
        Some(path) => {
            if let Some(series_name) = args.value_of("NAME") {
                config.series.insert(series_name.into(), path.into());
            }

            Ok(path.into())
        }
        None => {
            let name = args.value_of("NAME").ok_or(Error::NoSeriesInfoProvided)?;

            config
                .series
                .get(name)
                .ok_or_else(|| Error::SeriesNotFound(name.into()))
                .map(|path| path.into())
        }
    }
}
<commit_msg>[core] Show info for a series that doesn't have any episodes on disk when using the -i flag<commit_after>#[cfg(windows)]
extern crate winapi;

#[macro_use]
extern crate clap;
#[macro_use]
extern crate failure;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;

extern crate base64;
extern crate chrono;
extern crate directories;
extern crate regex;
extern crate reqwest;
extern crate serde;
extern crate toml;

mod backend;
mod config;
mod error;
mod input;
mod process;
mod series;
mod util;

use backend::{anilist::AniList, SyncBackend};
use config::Config;
use error::{Error, SeriesError};
use series::{EpisodeData, SaveData, Series};
use std::path::PathBuf;

fn main() {
    match run() {
        Ok(_) => (),
        Err(Error::Series(SeriesError::RequestExit)) => (),
        Err(e) => {
            let e: failure::Error = e.into();
            eprintln!("fatal error: {}", e.cause());

            for cause in e.causes().skip(1) {
                eprintln!("cause: {}", cause);
            }

            eprintln!("{}", e.backtrace());
            std::process::exit(1);
        }
    }
}

fn run() -> Result<(), Error> {
    let args = clap_app!(anup =>
        (version: env!("CARGO_PKG_VERSION"))
        (author: env!("CARGO_PKG_AUTHORS"))
        (@arg NAME: "The name of the series to watch")
        (@arg PATH: -p --path +takes_value "Specifies the directory to look for video files in")
        (@arg SEASON: -s --season +takes_value "Specifies which season you want to watch")
        (@arg INFO: -i --info "Displays saved series information")
        (@arg OFFLINE: -o --offline "Launches the program in offline mode")
        (@arg SYNC: --sync "Synchronizes all changes made offline to AniList")
    ).get_matches();

    if args.is_present("INFO") {
        print_saved_series_info()
    } else if args.is_present("SYNC") {
        sync_offline_changes()
    } else {
        watch_series(&args)
    }
}

fn watch_series(args: &clap::ArgMatches) -> Result<(), Error> {
    let mut config = Config::load()?;
    config.remove_invalid_series();

    let path = get_series_path(&mut config, args)?;
    let offline_mode = args.is_present("OFFLINE");

    let sync_backend = AniList::init(offline_mode, &mut config)?;

    config.save()?;

    let season = {
        let value: usize = args
            .value_of("SEASON")
            .and_then(|s| s.parse().ok())
            .unwrap_or(0);

        value.saturating_sub(1)
    };

    Series::load(offline_mode, &path, sync_backend)?
        .load_season(season)?
        .play_all_episodes()?;

    Ok(())
}

fn print_saved_series_info() -> Result<(), Error> {
    let mut config = Config::load()?;
    config.remove_invalid_series();

    println!("found [{}] series:\n", config.series.len());

    for (name, path) in config.series {
        let ep_matcher = match SaveData::from_dir(&path) {
            Ok(save_data) => save_data.episode_matcher,
            _ => None,
        };

        let ep_list = match EpisodeData::parse_dir(&path, ep_matcher) {
            Ok(ep_data) => {
                let mut ep_nums = ep_data.episodes.keys().cloned().collect::<Vec<_>>();
                ep_nums.sort_unstable();

                util::concat_sequential_values(&ep_nums, "..", " | ")
                    .unwrap_or_else(|| "none".into())
            }
            Err(SeriesError::NoEpisodesFound) => "none".into(),
            Err(err) => {
                eprintln!("failed to parse episode data for [{}]: {}", name, err);
                continue;
            }
        };

        println!("[{}]:", name);
        println!(
            "  path: {}\n  episodes on disk: {}",
            path.to_string_lossy(),
            ep_list
        );
    }

    Ok(())
}

fn sync_offline_changes() -> Result<(), Error> {
    let mut config = Config::load()?;
    config.remove_invalid_series();

    let backend = AniList::init(false, &mut config)?;

    for (_, path) in config.series {
        let mut save_data = match SaveData::from_dir(&path) {
            Ok(save_data) => save_data,
            Err(_) => continue,
        };

        for season_num in 0..save_data.season_states.len() {
            let season_state = &mut save_data.season_states[season_num];

            if !season_state.needs_sync {
                continue;
            }

            println!("[{}] syncing..", season_state.state.info.title);

            if season_state.needs_info {
                let ep_data = EpisodeData::parse_dir(&path, save_data.episode_matcher.clone())?;

                season_state.state.info =
                    series::search_for_series_info(&backend, &ep_data.series_name, season_num)?;

                season_state.needs_info = false;
            }

            backend.update_list_entry(&season_state.state)?;
            season_state.needs_sync = false;
        }

        save_data.write_to_file()?;
    }

    Ok(())
}

fn get_series_path(config: &mut Config, args: &clap::ArgMatches) -> Result<PathBuf, Error> {
    match args.value_of("PATH") {
        Some(path) => {
            if let Some(series_name) = args.value_of("NAME") {
                config.series.insert(series_name.into(), path.into());
            }

            Ok(path.into())
        }
        None => {
            let name = args.value_of("NAME").ok_or(Error::NoSeriesInfoProvided)?;

            config
                .series
                .get(name)
                .ok_or_else(|| Error::SeriesNotFound(name.into()))
                .map(|path| path.into())
        }
    }
}
<|endoftext|>


<commit_before>mod logger;
mod toml_file;
mod git;
mod changelog;
mod commit_analyzer;

extern crate rustc_serialize;
extern crate toml;
extern crate regex;
extern crate semver;
extern crate docopt;
extern crate git2_commit;
extern crate git2;
extern crate clog;

use docopt::Docopt;
use commit_analyzer::CommitType;
use std::process;
use semver::Version;

const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const USAGE: &'static str = "
semantic.rs 🚀

Usage:
  semantic-rs [options]
  semantic-rs --version

Options:
  -h --help              Show this screen.
  --version              Show version.
  -p PATH, --path=PATH   Specifies the repository path. [default: .]
  -n, --dry-run          Run without writing, committing, pushing or publishing anything.
";

#[derive(Debug, RustcDecodable)]
struct Args {
    flag_path: String,
    flag_dry_run: bool,
    flag_version: bool,
}

fn version_bump(version: &Version, bump: CommitType) -> Option<Version> {
    let mut version = version.clone();
    match bump {
        CommitType::Unknown => return None,
        CommitType::Patch => version.increment_patch(),
        CommitType::Minor => version.increment_minor(),
        CommitType::Major => version.increment_major(),
    }

    Some(version)
}

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

    if args.flag_version {
        println!("semantic.rs 🚀 -- v{}", VERSION);
        process::exit(0);
    }

    println!("semantic.rs 🚀");

    logger::stdout("Analyzing your repository");
    let repository_path = &args.flag_path;

    match git2::Repository::open(repository_path) {
        Ok(_) => { },
        Err(e) => {
            logger::stderr(format!("Could not open the git repository: {:?}", e));
            process::exit(1);
        }
    };

    let version = match toml_file::read_from_file(repository_path) {
        Ok(toml) => toml,
        Err(e) => {
            logger::stderr(format!("Reading `Cargo.toml` failed: {:?}", e));
            process::exit(1);
        }
    };

    let version = Version::parse(&version).expect("Not a valid version");
    logger::stdout(format!("Current version: {}", version.to_string()));

    logger::stdout("Analyzing commits");

    let bump = git::version_bump_since_latest(repository_path);
    if args.flag_dry_run {
        logger::stdout(format!("Commits analyzed. Bump would be {:?}", bump));
    }
    else {
        logger::stdout(format!("Commits analyzed. Bump will be {:?}", bump));
    }
    let new_version = match version_bump(&version, bump) {
        Some(new_version) => new_version.to_string(),
        None => {
            logger::stdout("No version bump. Nothing to do.");
            process::exit(0);
        }
    };

    if args.flag_dry_run {
        logger::stdout(format!("New version would be: {}", new_version));
        logger::stdout("Would write the following Changelog:");
        let changelog = match changelog::generate(repository_path, &version.to_string(), &new_version.to_string()) {
            Ok(log) => log,
            Err(err) => {
                logger::stderr(format!("Generating Changelog failed: {:?}", err));
                process::exit(1);
            }
        };
        logger::stdout("====================================");
        logger::stdout(changelog);
        logger::stdout("====================================");
        logger::stdout("Would create annotated git tag");
    }
    else {
        logger::stdout(format!("New version: {}", new_version));

        match toml_file::write_new_version(repository_path, &new_version) {
            Ok(_)    => { },
            Err(err) => {
                logger::stderr(format!("Writing `Cargo.toml` failed: {:?}", err));
                process::exit(1);
            }
        }

        logger::stdout(format!("Writing Changelog"));
        match changelog::write(repository_path, &version.to_string(), &new_version.to_string()) {
            Ok(_)    => { },
            Err(err) => {
                logger::stderr(format!("Writing Changelog failed: {:?}", err));
                process::exit(1);
            }
        }

        match git::commit_files(repository_path, &new_version) {
            Ok(_)    => { },
            Err(err) => {
                logger::stderr(format!("Committing `Cargo.toml` and `Changelog.md` failed: {:?}", err));
                process::exit(1);
            }
        }

        logger::stdout("Creating annotated git tag");
        let tag_message = match changelog::generate(repository_path, &version.to_string(), &new_version) {
            Ok(msg) => msg,
            Err(err) => {
                logger::stderr(format!("Can't generate changelog: {:?}", err));
                process::exit(1);
            }
        };

        let tag_name = format!("v{}", new_version);
        match git::tag(repository_path, &tag_name, &tag_message) {
            Ok(_) => { },
            Err(err) => {
                logger::stderr(format!("Failed to create git tag: {:?}", err));
                process::exit(1);
            }
        }
    }
}
<commit_msg>feat: dry-run by default. Overwrite by passing -w<commit_after>mod logger;
mod toml_file;
mod git;
mod changelog;
mod commit_analyzer;

extern crate rustc_serialize;
extern crate toml;
extern crate regex;
extern crate semver;
extern crate docopt;
extern crate git2_commit;
extern crate git2;
extern crate clog;

use docopt::Docopt;
use commit_analyzer::CommitType;
use std::process;
use semver::Version;

const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const USAGE: &'static str = "
semantic.rs 🚀

Usage:
  semantic-rs [options]
  semantic-rs --version

Options:
  -h --help              Show this screen.
  --version              Show version.
  -p PATH, --path=PATH   Specifies the repository path. [default: .]
  -w, --write            Run with writing the changes afterwards.
";

#[derive(Debug, RustcDecodable)]
struct Args {
    flag_path: String,
    flag_write: bool,
    flag_version: bool,
}

fn version_bump(version: &Version, bump: CommitType) -> Option<Version> {
    let mut version = version.clone();
    match bump {
        CommitType::Unknown => return None,
        CommitType::Patch => version.increment_patch(),
        CommitType::Minor => version.increment_minor(),
        CommitType::Major => version.increment_major(),
    }

    Some(version)
}

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

    if args.flag_version {
        println!("semantic.rs 🚀 -- v{}", VERSION);
        process::exit(0);
    }

    let is_dry_run = !args.flag_write;

    println!("semantic.rs 🚀");

    logger::stdout("Analyzing your repository");
    let repository_path = &args.flag_path;

    match git2::Repository::open(repository_path) {
        Ok(_) => { },
        Err(e) => {
            logger::stderr(format!("Could not open the git repository: {:?}", e));
            process::exit(1);
        }
    };

    let version = match toml_file::read_from_file(repository_path) {
        Ok(toml) => toml,
        Err(e) => {
            logger::stderr(format!("Reading `Cargo.toml` failed: {:?}", e));
            process::exit(1);
        }
    };

    let version = Version::parse(&version).expect("Not a valid version");
    logger::stdout(format!("Current version: {}", version.to_string()));

    logger::stdout("Analyzing commits");

    let bump = git::version_bump_since_latest(repository_path);
    if is_dry_run {
        logger::stdout(format!("Commits analyzed. Bump would be {:?}", bump));
    }
    else {
        logger::stdout(format!("Commits analyzed. Bump will be {:?}", bump));
    }
    let new_version = match version_bump(&version, bump) {
        Some(new_version) => new_version.to_string(),
        None => {
            logger::stdout("No version bump. Nothing to do.");
            process::exit(0);
        }
    };

    if is_dry_run {
        logger::stdout(format!("New version would be: {}", new_version));
        logger::stdout("Would write the following Changelog:");
        let changelog = match changelog::generate(repository_path, &version.to_string(), &new_version.to_string()) {
            Ok(log) => log,
            Err(err) => {
                logger::stderr(format!("Generating Changelog failed: {:?}", err));
                process::exit(1);
            }
        };
        logger::stdout("====================================");
        logger::stdout(changelog);
        logger::stdout("====================================");
        logger::stdout("Would create annotated git tag");
    }
    else {
        logger::stdout(format!("New version: {}", new_version));

        match toml_file::write_new_version(repository_path, &new_version) {
            Ok(_)    => { },
            Err(err) => {
                logger::stderr(format!("Writing `Cargo.toml` failed: {:?}", err));
                process::exit(1);
            }
        }

        logger::stdout(format!("Writing Changelog"));
        match changelog::write(repository_path, &version.to_string(), &new_version.to_string()) {
            Ok(_)    => { },
            Err(err) => {
                logger::stderr(format!("Writing Changelog failed: {:?}", err));
                process::exit(1);
            }
        }

        match git::commit_files(repository_path, &new_version) {
            Ok(_)    => { },
            Err(err) => {
                logger::stderr(format!("Committing `Cargo.toml` and `Changelog.md` failed: {:?}", err));
                process::exit(1);
            }
        }

        logger::stdout("Creating annotated git tag");
        let tag_message = match changelog::generate(repository_path, &version.to_string(), &new_version) {
            Ok(msg) => msg,
            Err(err) => {
                logger::stderr(format!("Can't generate changelog: {:?}", err));
                process::exit(1);
            }
        };

        let tag_name = format!("v{}", new_version);
        match git::tag(repository_path, &tag_name, &tag_message) {
            Ok(_) => { },
            Err(err) => {
                logger::stderr(format!("Failed to create git tag: {:?}", err));
                process::exit(1);
            }
        }
    }
}
<|endoftext|>