diff options
author | Lars Wirzenius <liw@liw.fi> | 2021-04-17 14:06:10 +0300 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2021-04-17 15:53:21 +0300 |
commit | f236aef565c1f9c99dcf24979830b232ab6749bc (patch) | |
tree | 0d1053ad8ef0b6f7de9a74485b641b81dd85c0c4 /src | |
parent | e325fc5e47e5ef34969a30fc6568a58a6026f39b (diff) | |
download | clab-f236aef565c1f9c99dcf24979830b232ab6749bc.tar.gz |
rewrite in rust
Diffstat (limited to 'src')
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/main.rs | 189 |
2 files changed, 190 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..08fc655 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,189 @@ +use directories_next::ProjectDirs; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use structopt::StructOpt; + +const APP: &str = "clab"; + +fn main() -> anyhow::Result<()> { + let mut opt = Opt::from_args(); + let book = if let Some(filename) = &opt.db { + AddressBook::load(filename)? + } else { + let proj_dirs = ProjectDirs::from("", "", APP).expect("couldn't find home directory"); + let filename = proj_dirs.data_dir().join("address-book.yaml"); + opt.db = Some(filename.clone()); + if filename.exists() { + AddressBook::load(&filename)? + } else { + AddressBook::default() + } + }; + match &opt.cmd { + Cmd::Config(x) => x.run(&opt, &book), + Cmd::Lint(x) => x.run(&opt, &book), + Cmd::List(x) => x.run(&opt, &book)?, + Cmd::Search(x) => x.run(&opt, &book)?, + Cmd::MuttQuery(x) => x.run(&opt, &book), + } + Ok(()) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct Entry { + name: String, + org: Option<String>, + url: Option<Vec<String>>, + notes: Option<String>, + aliases: Option<Vec<String>>, + email: Option<HashMap<String, String>>, + phone: Option<HashMap<String, String>>, + irc: Option<HashMap<String, String>>, + address: Option<HashMap<String, String>>, +} + +impl Entry { + fn is_match(&self, needle: &str) -> bool { + contains(&self.name, needle) + } + + fn emails(&self) -> Vec<String> { + if let Some(map) = &self.email { + map.values().map(|x| x.to_string()).collect() + } else { + vec![] + } + } +} + +fn output_entries(entries: &[Entry]) -> anyhow::Result<()> { + if !entries.is_empty() { + serde_yaml::to_writer(std::io::stdout(), entries)?; + } + Ok(()) +} + +fn contains(haystack: &str, needle: &str) -> bool { + let haystack = haystack.to_lowercase(); + let needle = needle.to_lowercase(); + haystack.contains(&needle) +} + +#[derive(std::default::Default)] +struct AddressBook { + entries: Vec<Entry>, +} + +impl AddressBook { + fn load(db: &Path) -> anyhow::Result<Self> { + let mut book = Self::default(); + book.add_from(db)?; + Ok(book) + } + + fn add_from(&mut self, filename: &Path) -> anyhow::Result<()> { + let text = std::fs::read(&filename)?; + let mut entries: Vec<Entry> = serde_yaml::from_slice(&text)?; + self.entries.append(&mut entries); + Ok(()) + } + + fn entries(&self) -> &[Entry] { + &self.entries + } + + fn iter(&self) -> impl Iterator<Item = &Entry> { + self.entries.iter() + } +} + +#[derive(Debug, StructOpt)] +struct Opt { + #[structopt(long, parse(from_os_str))] + db: Option<PathBuf>, + + #[structopt(subcommand)] + cmd: Cmd, +} + +#[derive(Debug, StructOpt)] +enum Cmd { + Config(ConfigCommand), + Lint(LintCommand), + List(ListCommand), + Search(SearchCommand), + MuttQuery(MuttCommand), +} + +#[derive(Debug, StructOpt)] +struct ConfigCommand {} + +impl ConfigCommand { + fn run(&self, opt: &Opt, _book: &AddressBook) { + println!("{:#?}", opt); + } +} + +#[derive(Debug, StructOpt)] +struct LintCommand { + #[structopt(parse(from_os_str))] + filenames: Vec<PathBuf>, +} + +impl LintCommand { + fn run(&self, _opt: &Opt, _book: &AddressBook) {} +} + +#[derive(Debug, StructOpt)] +struct ListCommand {} + +impl ListCommand { + fn run(&self, _opt: &Opt, book: &AddressBook) -> anyhow::Result<()> { + output_entries(book.entries()) + } +} + +#[derive(Debug, StructOpt)] +#[structopt(alias = "find")] +struct SearchCommand { + #[structopt()] + words: Vec<String>, +} + +impl SearchCommand { + fn run(&self, _opt: &Opt, book: &AddressBook) -> anyhow::Result<()> { + let matches: Vec<Entry> = book.iter().filter(|e| self.is_match(e)).cloned().collect(); + output_entries(&matches) + } + + fn is_match(&self, entry: &Entry) -> bool { + for word in self.words.iter() { + if !entry.is_match(word) { + return false; + } + } + true + } +} + +#[derive(Debug, StructOpt)] +struct MuttCommand { + #[structopt()] + word: String, +} + +impl MuttCommand { + fn run(&self, _opt: &Opt, book: &AddressBook) { + for e in book.iter().filter(|e| self.is_match(e)) { + for email in e.emails() { + println!("{}\t{}", e.name, email); + } + } + } + + fn is_match(&self, entry: &Entry) -> bool { + entry.is_match(&self.word) + } +} |