use clap::ArgMatches;
use clap_complete::Shell::*;
use rayon::prelude::*;
use regex::Regex;
use rusqlite::Error;
use crate::config::ConfigOptions;
use crate::graph::zk_graph_dot_output;
use crate::Database;
use crate::Zettel;
use crate::cli;
use crate::io::file_exists;
pub fn sync(matches: &ArgMatches, cfg: &ConfigOptions) -> Result<(), Error>
{
let project = matches.value_of("PROJECT").unwrap_or_default();
if let Some(title) = matches.value_of("CREATE") {
new(cfg, title, project)?;
} else if let Some(path) = matches.value_of("UPDATE") {
update(cfg, path)?;
} else if let Some(title) = matches.value_of("MOVE") {
mv(cfg, title, project)?;
} else if let Some(args_arr) = matches.values_of("RENAME") {
rename(cfg, args_arr)?;
} else if matches.is_present("GENERATE") {
generate(cfg)?;
}
Ok(())
}
pub fn query(matches: &ArgMatches, cfg: &ConfigOptions) -> Result<(), Error>
{
let db = Database::new(&cfg.db_file())?;
let mut zs: Vec<Zettel> = db.all()?;
if let Some(title) = matches.value_of("TITLE") {
zs = filter_title(zs, title);
}
if let Some(project) = matches.value_of("PROJECT") {
zs = filter_project(zs, project);
}
if let Some(text) = matches.value_of("TEXT_REGEX") {
zs = filter_text(zs, text, cfg);
}
if let Some(tag) = matches.value_of("TAG") {
zs = filter_tag(zs, tag);
}
if let Some(linked_from) = matches.value_of("LINKS") {
zs = intersect(&zs, &fwlinks(&db.all()?, linked_from));
}
if let Some(links_to) = matches.value_of("BACKLINKS") {
zs = intersect(&zs, &backlinks(&zs, links_to));
}
if matches.is_present("LONERS") {
zs = filter_isolated(zs);
}
if matches.is_present("GRAPH") {
zk_graph_dot_output(&zs);
} else if let Some(format) = matches.value_of("FORMAT") {
let link_sep = matches.value_of("LINK_SEP").unwrap_or(" | ");
zettelkasten_format(cfg,
&zs,
&replace_literals(format),
&replace_literals(link_sep))
} else {
zettelkasten_format(cfg, &zs, "[%p] %t", "")
}
Ok(())
}
fn replace_literals(s: &str) -> String
{
s.replace(r"\n", "\n").replace(r"\t", "\t")
}
pub fn ls(matches: &ArgMatches, cfg: &ConfigOptions) -> Result<(), Error>
{
let db = Database::new(&cfg.db_file())?;
let m = matches.value_of("OBJECT").unwrap_or_default();
match m {
"tags" => print_list_of_strings(&db.list_tags()?),
"ghosts" => print_list_of_strings(&db.zettel_not_yet_created()?),
"projects" => print_list_of_strings(&db.list_projects()?),
"path" => println!("{}", cfg.zettelkasten),
_ => eprintln!("error: expected one of: 'tags', 'ghosts', 'projects', 'path'; got '{}'",
m),
}
Ok(())
}
pub fn compl(matches: &ArgMatches) -> Result<(), Error>
{
let shell = matches.value_of("SHELL").unwrap_or_default();
let sh = match shell {
"zsh" => Some(Zsh),
"bash" => Some(Bash),
"fish" => Some(Fish),
_ => None,
};
if let Some(sh) = sh {
let app = &mut cli::build();
clap_complete::generate(sh, app, app.get_name().to_string(), &mut std::io::stdout());
} else {
eprintln!("error: '{}' isn't a (supported) shell", shell);
}
Ok(())
}
fn zettelkasten_format(cfg: &ConfigOptions, zs: &[Zettel], fmt: &str, link_sep: &str)
{
zs.iter().for_each(|z| {
zettel_format(cfg, z, fmt, link_sep);
});
}
fn zettel_format(cfg: &ConfigOptions, z: &Zettel, fmt: &str, link_sep: &str)
{
let mut result = fmt.to_string();
result = result.replace("%t", &z.title);
result = result.replace("%p", &z.project);
result = result.replace("%P", &z.filename(cfg));
result = result.replace("%l", &z.links.join(link_sep));
if result.contains("%b") {
let maybe_get_backlinks = || -> Result<Vec<String>, Error> {
let all = Database::new(&cfg.db_file())?.all()?;
let bks = backlinks(&all, &z.title);
Ok(bks.iter().map(|z| z.title.clone()).collect())
};
if let Ok(bks) = maybe_get_backlinks() {
result = result.replace("%b", &bks.join(link_sep));
}
}
println!("{}", result);
}
fn print_list_of_strings(elems: &[String])
{
elems.iter().for_each(|e| {
println!("{}", e);
})
}
fn filter_title(zs: Vec<Zettel>, pattern: &str) -> Vec<Zettel>
{
let re = Regex::new(&format!("^{}$", pattern)).unwrap();
zs.into_iter().filter(|z| re.is_match(&z.title)).collect()
}
fn filter_project(zs: Vec<Zettel>, pattern: &str) -> Vec<Zettel>
{
let re = Regex::new(&format!("^{}$", pattern)).unwrap();
zs.into_iter().filter(|z| re.is_match(&z.project)).collect()
}
fn filter_text(zs: Vec<Zettel>, pattern: &str, cfg: &ConfigOptions) -> Vec<Zettel>
{
zs.into_iter()
.filter(|z| z.has_text(cfg, pattern))
.collect()
}
fn filter_tag(zs: Vec<Zettel>, pattern: &str) -> Vec<Zettel>
{
let re = Regex::new(&format!("^{}(/.*)?$", pattern)).unwrap();
zs.into_iter()
.filter(|z| {
for t in &z.tags {
if re.is_match(t) {
return true;
}
}
false
})
.collect()
}
fn filter_isolated(zs: Vec<Zettel>) -> Vec<Zettel>
{
zs.clone()
.into_iter()
.filter(|z| z.links.is_empty() && backlinks(&zs, &z.title).is_empty())
.collect()
}
fn intersect<T: Eq + Clone>(a: &[T], b: &[T]) -> Vec<T>
{
a.to_owned()
.iter()
.cloned()
.filter(|z| b.contains(z))
.collect::<Vec<_>>()
}
fn fwlinks(all: &[Zettel], linked_from: &str) -> Vec<Zettel>
{
let fwlinks = &filter_title(all.to_owned(), linked_from);
all.iter()
.cloned()
.filter(|z| {
for fw in fwlinks {
if fw.links.contains(&z.title) {
return true;
}
}
false
})
.collect()
}
fn backlinks(all: &[Zettel], links_to: &str) -> Vec<Zettel>
{
let re = Regex::new(&format!("^{}$", links_to)).unwrap();
all.iter()
.cloned()
.filter(|z| {
for l in &z.links {
if re.is_match(l) {
return true;
}
}
false
})
.collect()
}
fn new(cfg: &ConfigOptions, title: &str, project: &str) -> Result<(), Error>
{
let db = Database::new(&cfg.db_file())?;
db.init()?;
let zettel = Zettel::new(title, project);
let exists_in_fs = file_exists(&zettel.filename(cfg));
let exists_in_db = db.all()?.into_par_iter().any(|z| z == zettel);
if exists_in_fs && exists_in_db {
eprintln!("error: couldn't create new Zettel: one with the same title already exists");
return Ok(());
} else if exists_in_fs {
println!("file exists in the filesystem but not in the database; added entry");
} else {
zettel.create(cfg);
zettel_format(cfg, &zettel, "[%p] %t", "")
}
db.save(&zettel)?;
Ok(())
}
fn rename(cfg: &ConfigOptions, arr: clap::Values<'_>) -> Result<(), Error>
{
let db = Database::new(&cfg.db_file())?;
let old_title = arr.clone()
.into_iter()
.find(|x| !db.find_by_title(x).unwrap_or_default().is_empty())
.unwrap_or("");
let new_title = arr.clone().next_back().unwrap_or_default();
if old_title == new_title {
eprintln!("error: first match is the same as the new title ('{}'), so no rename",
old_title);
return Ok(());
}
let results = db.find_by_title(old_title)?;
let overwrite_failsafe = db.find_by_title(new_title)?;
if overwrite_failsafe.first().is_some() {
eprintln!("error: a note with the new title already exists: won't overwrite");
return Ok(());
}
let old_zettel = if results.first().is_none() {
eprintln!("error: no Zettel with that title");
return Ok(());
} else {
results.first().unwrap()
};
let new_zettel = Zettel::new(new_title, &old_zettel.project);
let mut dial = dialoguer::Confirm::new();
let prompt = dial.with_prompt(format!("{} --> {}", old_title, new_title));
if prompt.interact().unwrap_or_default() {
crate::io::rename(&old_zettel.filename(cfg), &new_zettel.filename(cfg));
db.change_title(old_zettel, new_title).unwrap();
let backlinks = backlinks(&db.all()?, old_title);
backlinks.iter().for_each(|bl| {
let contents = crate::io::file_to_string(&bl.filename(cfg));
let regex_string =
&format!(r"\[\[{}\]\]", old_title).replace(' ', r"[\n\t ]");
let old_title_reg = Regex::new(regex_string).unwrap();
let new_contents =
old_title_reg.replace_all(&contents, format!(r"[[{}]]", new_title));
crate::io::write_to_file(&bl.filename(cfg), &new_contents);
db.update(cfg, bl).unwrap();
})
}
Ok(())
}
fn mv(cfg: &ConfigOptions, pattern: &str, project: &str) -> Result<(), Error>
{
let db = Database::new(&cfg.db_file())?;
let zs = db.find_by_title(pattern)?;
zettelkasten_format(cfg, &zs, "[%p] %t", "");
let mut dial = dialoguer::Confirm::new();
let prompt =
dial.with_prompt(format!(">> These notes will be transferred to the {}. Proceed?",
if project.is_empty() {
"main zettelkasten".to_string()
} else {
format!("'{}' project", project)
}));
if prompt.interact().unwrap_or_default() {
crate::io::mkdir(&format!("{}/{}", cfg.zettelkasten, project));
let new_notes = zs.iter().map(|z| Zettel { title: z.title.clone(),
project: project.to_string(),
links: z.links.clone(),
tags: z.tags.clone() });
let pairs = zs.iter().zip(new_notes);
pairs.for_each(|(old, new)| {
crate::io::rename(&old.filename(cfg), &new.filename(cfg));
db.change_project(old, project).unwrap();
});
}
Ok(())
}
fn update(cfg: &ConfigOptions, path: &str) -> Result<(), Error>
{
let db = Database::new(&cfg.db_file())?;
if file_exists(path) {
let zettel = Zettel::from_file(cfg, path);
db.update(cfg, &zettel)?;
} else {
eprintln!("error: provided path isn't a file");
}
Ok(())
}
fn generate(cfg: &ConfigOptions) -> Result<(), Error>
{
let start = std::time::Instant::now();
let mem_db = Database::in_memory(&cfg.db_file())?;
mem_db.init()?;
mem_db.generate(cfg);
mem_db.write_to(&cfg.db_file())?;
println!("database generated successfully, took {}ms",
start.elapsed().as_millis());
Ok(())
}