mates/
cli.rs

1use std::borrow::ToOwned;
2use std::env;
3use std::error::Error;
4use std::fmt;use std::fs;
5use std::io::{Read,Write};
6use std::io;
7use std::path;
8use std::process;
9
10use atomicwrites::{AtomicFile,AllowOverwrite};
11
12use utils;
13use utils::CustomPathExt;
14use app;
15use editor;
16
17
18#[inline]
19fn get_pwd() -> path::PathBuf {
20    env::current_dir().ok().expect("Failed to get CWD")
21}
22
23#[inline]
24fn get_envvar(key: &str) -> Option<String> {
25    match env::var(key) {
26        Ok(x) => Some(x),
27        Err(env::VarError::NotPresent) => None,
28        Err(env::VarError::NotUnicode(_)) => panic!(format!("{} is not unicode.", key)),
29    }
30}
31
32fn build_index(outfile: &path::Path, dir: &path::Path) -> MainResult<()> {
33    if !dir.is_dir() {
34        return Err(MainError::new("MATES_DIR must be a directory.").into());
35    };
36
37    let af = AtomicFile::new(&outfile, AllowOverwrite);
38    let mut errors = false;
39
40    try!(af.write::<(), io::Error, _>(|outf| {
41        for entry in try!(fs::read_dir(dir)) {
42            let entry = match entry {
43                Ok(x) => x,
44                Err(e) => {
45                    println!("Error while listing directory: {}", e);
46                    errors = true;
47                    continue;
48                }
49            };
50
51            let pathbuf = entry.path();
52
53            if pathbuf.str_extension().unwrap_or("") != "vcf" || !pathbuf.is_file() {
54                continue;
55            };
56
57            let contact = match utils::Contact::from_file(&pathbuf) {
58                Ok(x) => x,
59                Err(e) => {
60                    println!("Error while reading {}: {}", pathbuf.display(), e);
61                    errors = true;
62                    continue
63                }
64            };
65
66            match utils::index_item_from_contact(&contact) {
67                Ok(index_string) => {
68                    try!(outf.write_all(index_string.as_bytes()));
69                },
70                Err(e) => {
71                    println!("Error while indexing {}: {}", pathbuf.display(), e);
72                    errors = true;
73                    continue
74                }
75            };
76        };
77        Ok(())
78    }));
79
80    if errors {
81        Err(MainError::new("Several errors happened while generating the index.").into())
82    } else {
83        Ok(())
84    }
85}
86
87pub fn cli_main() {
88    match cli_main_raw() {
89        Err(e) => {
90            writeln!(&mut io::stderr(), "{}", e).unwrap();
91            process::exit(1);
92        },
93        _ => ()
94    };
95}
96
97pub fn cli_main_raw() -> MainResult<()> {
98    let matches = app::app().get_matches();
99
100    let command = matches.subcommand_name().unwrap();
101
102    let config = match Configuration::new() {
103        Ok(x) => x,
104        Err(e) => {
105            return Err(MainError::new(format!("Error while reading configuration: {}", e)).into());
106        }
107    };
108
109    let submatches = matches.subcommand_matches(command).expect("Internal error.");
110
111    match command {
112        "index" => {
113            println!("Rebuilding index file \"{}\"...", config.index_path.display());
114            try!(build_index(&config.index_path, &config.vdir_path));
115        },
116        "mutt-query" => {
117            let query = submatches.value_of("query").unwrap_or("");
118            try!(mutt_query(&config, &query[..]));
119        },
120        "file-query" => {
121            let query = submatches.value_of("query").unwrap_or("");
122            try!(file_query(&config, &query[..]));
123        },
124        "email-query" => {
125            let query = submatches.value_of("query").unwrap_or("");
126            try!(email_query(&config, &query[..]));
127        },
128        "add" => {
129            let stdin = io::stdin();
130            let mut email = String::new();
131            try!(stdin.lock().read_to_string(&mut email));
132            let contact = try!(utils::add_contact_from_email(
133                &config.vdir_path,
134                &email[..]
135            ));
136            println!("{}", contact.path.display());
137
138            let mut index_fp = try!(fs::OpenOptions::new()
139                                    .append(true)
140                                    .write(true)
141                                    .open(&config.index_path));
142
143            let index_entry = try!(utils::index_item_from_contact(&contact));
144            try!(index_fp.write_all(index_entry.as_bytes()));
145        },
146        "edit" => {
147            let query = submatches.value_of("file-or-query").unwrap_or("");
148            try!(edit_contact(&config, &query[..]));
149        },
150        _ => {
151            return Err(MainError::new(format!("Invalid command: {}", command)).into());
152        }
153    };
154    Ok(())
155}
156
157fn edit_contact(config: &Configuration, query: &str) -> MainResult<()> {
158    let results = if get_pwd().join(query).is_file() {
159        vec![path::PathBuf::from(query)]
160    } else {
161        try!(utils::file_query(config, query)).into_iter().collect()
162    };
163
164    if results.len() < 1 {
165        return Err(MainError::new("No such contact.").into());
166    } else if results.len() > 1 {
167        return Err(MainError::new("Ambiguous query.").into());
168    }
169
170    let fpath = &results[0];
171    editor::cli_main(fpath);
172
173    let fcontent = {
174        let mut fcontent = String::new();
175        let mut file = try!(fs::File::open(fpath));
176        try!(file.read_to_string(&mut fcontent));
177        fcontent
178    };
179
180    if (&fcontent[..]).trim().len() == 0 {
181        try!(fs::remove_file(fpath));
182        return Err(MainError::new("Contact emptied, file removed.").into());
183    };
184
185    Ok(())
186}
187
188fn mutt_query<'a>(config: &Configuration, query: &str) -> MainResult<()> {
189    println!("");  // For some reason mutt requires an empty line
190    // We need to ignore errors here, otherwise mutt's UI will glitch
191    if let Ok(items) = utils::index_query(config, query) {
192        for item in items {
193            if item.email.len() > 0 && item.name.len() > 0 {
194                println!("{}\t{}", item.email, item.name);
195            };
196        };
197    };
198    Ok(())
199}
200
201fn file_query<'a>(config: &Configuration, query: &str) -> MainResult<()> {
202    for path in try!(utils::file_query(config, query)).iter() {
203        println!("{}", path.display());
204    };
205    Ok(())
206}
207
208fn email_query<'a>(config: &Configuration, query: &str) -> MainResult<()> {
209    for item in try!(utils::index_query(config, query)) {
210        if item.name.len() > 0 && item.email.len() > 0 {
211            println!("{} <{}>", item.name, item.email);
212        };
213    };
214    Ok(())
215}
216
217pub struct Configuration {
218    pub index_path: path::PathBuf,
219    pub vdir_path: path::PathBuf,
220    pub grep_cmd: String
221}
222
223impl Configuration {
224    pub fn new() -> Result<Configuration, String> {
225        Ok(Configuration {
226            index_path: match get_envvar("MATES_INDEX") {
227                Some(x) => path::PathBuf::from(&x),
228                None => match get_envvar("HOME") {
229                    Some(home) => get_pwd().join(&home).join(".mates_index"),
230                    None => return Err("Unable to determine user's home directory.".to_owned())
231                }
232            },
233            vdir_path: match get_envvar("MATES_DIR") {
234                Some(x) => path::PathBuf::from(&x),
235                None => return Err("MATES_DIR must be set to your vdir path (directory of vcf-files).".to_owned())
236            },
237            grep_cmd: match get_envvar("MATES_GREP") {
238                Some(x) => x,
239                None => "grep -i".to_owned()
240            }
241        })
242    }
243}
244
245
246#[derive(PartialEq, Eq, Debug)]
247pub struct MainError {
248    desc: String,
249}
250
251pub type MainResult<T> = Result<T, Box<Error>>;
252
253impl Error for MainError {
254    fn description(&self) -> &str {
255        &self.desc[..]
256    }
257
258    fn cause(&self) -> Option<&Error> {
259        None
260    }
261}
262
263impl fmt::Display for MainError {
264    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
265        self.description().fmt(f)
266    }
267}
268
269impl MainError {
270    pub fn new<T: Into<String>>(desc: T) -> Self {
271        MainError {
272            desc: desc.into(),
273        }
274    }
275}