summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-04-17 14:06:10 +0300
committerLars Wirzenius <liw@liw.fi>2021-04-17 15:53:21 +0300
commitf236aef565c1f9c99dcf24979830b232ab6749bc (patch)
tree0d1053ad8ef0b6f7de9a74485b641b81dd85c0c4 /src
parente325fc5e47e5ef34969a30fc6568a58a6026f39b (diff)
downloadclab-f236aef565c1f9c99dcf24979830b232ab6749bc.tar.gz
rewrite in rust
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs1
-rw-r--r--src/main.rs189
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)
+ }
+}