1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
use clap::{App, Arg, ArgMatches, AppSettings};
use directories::ProjectDirs;
use std::path::PathBuf;
use std::process;
use log::error;

/// Returns Ok(_) if input string  can be parsed as an int.
/// Used for argument validation.
fn is_int(s: String) -> Result<(), String> {
    match s.parse::<i64>() {
        Ok(_) => Ok(()),
        Err(_e) => Err(format!("invalid integer {}", s)),
    }
}

/// Returns an instance of the application with arguments.
pub fn get_app() -> App<'static, 'static> {
    App::new(env!("CARGO_PKG_NAME"))
        .version(env!("CARGO_PKG_VERSION"))
        .author(env!("CARGO_PKG_AUTHORS"))
        .setting(AppSettings::ColorAuto)
        .setting(AppSettings::ColoredHelp)
        .setting(AppSettings::DeriveDisplayOrder)
        .setting(AppSettings::NextLineHelp)
        .arg(Arg::with_name("add")
            .short("a")
            .long("add")
            .conflicts_with_all(&["increase", "decrease"])
            .requires("item")
            .help("Add a visit to ITEM to the store"))
        .arg(Arg::with_name("increase")
            .short("i")
            .long("increase")
            .help("Increase the weight of an item by WEIGHT")
            .conflicts_with_all(&["add", "decrease"])
            .requires("item")
            .value_name("WEIGHT")
            .takes_value(true))
        .arg(Arg::with_name("decrease")
            .short("d")
            .long("decrease")
            .conflicts_with_all(&["increase", "add"])
            .requires("item")
            .help("Decrease the weight of a path by WEIGHT")
            .value_name("WEIGHT")
            .takes_value(true))
        .arg(Arg::with_name("sorted")
            .long("sorted")
            .group("lists")
            .help("Print the stored directories in order of highest to lowest score"))
        .arg(Arg::with_name("stat")
            .short("s")
            .group("lists")
            .long("stat")
            .help("Print statistics about the stored directories"))
        .arg(Arg::with_name("sort_method")
            .long("sort_method")
            .help("The method to sort by most used")
            .takes_value(true)
            .possible_values(&["frecent", "frequent", "recent"])
            .default_value("frecent"))
        .arg(Arg::with_name("limit")
            .long("limit")
            .short("l")
            .takes_value(true)
            .requires("lists")
            .help("Limit the number of results printed --sorted"))
        .arg(Arg::with_name("store")
            .long("store")
            .value_name("FILE")
            .conflicts_with_all(&["store_name"])
            .help("Use a non-default store file")
            .takes_value(true))
        .arg(Arg::with_name("store_name")
            .long("store_name")
            .value_name("FILE")
            .conflicts_with_all(&["store"])
            .help("Use a non-default filename for the store file in the default store directory")
            .takes_value(true))
        .arg(Arg::with_name("truncate")
            .short("T")
            .long("truncate")
            .help("Truncate the stored directories to only the top N")
            .value_name("N")
            .validator(is_int)
            .takes_value(true))
        .arg(Arg::with_name("halflife")
            .long("halflife")
            .help("Change the halflife to N seconds")
            .value_name("N")
            .takes_value(true))
        .arg(Arg::with_name("item")
            .index(1)
            .value_name("ITEM")
            .help("The item to update"))
}

/// Given the argument matches, return the path of the store file.
pub fn get_store_path(matches: &ArgMatches) -> PathBuf {
    match (matches.value_of("store"), matches.value_of("store_name")) {
        (Some(dir), None) => PathBuf::from(dir),
        (None, file) => default_store(file),
        _ => unreachable!(),
    }
}

/// Return a path to a store file in the default location.
/// Uses filename as the name of the file if it is not `None`.
pub fn default_store(filename: Option<&str>) -> PathBuf {
    let store_dir = match ProjectDirs::from("", "", env!("CARGO_PKG_NAME")) {
        Some(dir) => dir.data_dir().to_path_buf(),
        None => {
            error!("Failed to detect default data directory");
            process::exit(1);
        }
    };

    let mut store_file = store_dir.clone();
    let default = format!("{}.json", env!("CARGO_PKG_NAME"));
    let filename = filename.unwrap_or(&default);
    store_file.push(filename);

    store_file.to_path_buf()
}

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

    #[test]
    fn get_store_path_full() {
        let arg_vec = vec!["fre", "--store", "/test/path"];
        let matches = get_app().get_matches_from_safe(arg_vec).unwrap();

        let store_path = get_store_path(&matches);

        assert_that!(store_path).is_equal_to(PathBuf::from("/test/path"));
    }

    #[test]
    fn get_store_path_file() {
        let arg_vec = vec!["fre", "--store_name", "test.path"];
        let matches = get_app().get_matches_from_safe(arg_vec).unwrap();

        let store_path = get_store_path(&matches);

        assert_that!(store_path.to_str().unwrap()).ends_with("test.path");
    }
}