main.rs
1 // This file is part of digest. 2 // 3 // digest is free software: you can redistribute it and/or modify it under the terms of the GNU 4 // General Public License as published by the Free Software Foundation, either version 3 of the 5 // License, or (at your option) any later version. 6 // 7 // digest is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even 8 // the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 9 // Public License for more details. 10 // 11 // You should have received a copy of the GNU General Public License along with Foobar. If not, 12 // see <https://www.gnu.org/licenses/>. 13 use digest::database::Database; 14 use digest::indexer::index; 15 use clap::{Parser, Subcommand}; 16 use directories::ProjectDirs; 17 use serde::{Deserialize, Deserializer}; 18 use std::path::PathBuf; 19 use pfmt::{Fmt, FormatTable}; 20 use std::collections::HashMap; 21 22 #[derive(Parser)] 23 #[command(version, about, long_about = None, subcommand_required = true)] 24 struct Cli { 25 #[command(subcommand)] 26 command: Option<Commands>, 27 #[arg(short, long)] 28 quiet: bool, 29 } 30 31 #[derive(Subcommand)] 32 enum Commands { 33 #[clap(about="Show the current configuration")] 34 Config, 35 #[clap(about="Show configured zettle folder ")] 36 Folder, 37 #[clap(about="List existing notes")] 38 Zlist, 39 #[clap(about="List existing links")] 40 Zlinks, 41 #[clap(about="List missing notes")] 42 Zmissing, 43 #[clap(about="Create a new note")] 44 New, 45 #[clap(about="Update database")] 46 Update, 47 #[clap(about="List todo tasks")] 48 Tasks { 49 #[arg(short, long)] 50 line_format: Option<String>, 51 }, 52 #[clap(about="Search notes")] 53 Zsearch { 54 query: String, 55 #[arg(short, long)] 56 path_relative_to: Option<String>, 57 #[arg(short, long)] 58 line_format: Option<String>, 59 }, 60 } 61 62 fn deserialize_folder<'de, D>(deserializer: D) -> Result<PathBuf, D::Error> 63 where D: Deserializer<'de> { 64 let mut s = String::deserialize(deserializer)?; 65 s = shellexpand::tilde(&s).to_string(); 66 Ok(PathBuf::from(s)) 67 } 68 69 fn default_document_name() -> String { "document.gmi".to_string() } 70 71 #[derive(Deserialize, Debug)] 72 struct Config { 73 #[serde(deserialize_with = "deserialize_folder")] 74 folder: PathBuf, 75 #[serde(default = "default_document_name")] 76 document_name: String, 77 } 78 79 impl Config { 80 fn load(quiet: bool) -> Config { 81 let config_path = ProjectDirs::from("org", "equalsraf", "digest") 82 .expect("Unable to determine user config path") 83 .config_dir() 84 .join("config.toml"); 85 86 if config_path.exists() { 87 if !quiet { 88 println!("Loading {}", config_path.display()); 89 } 90 let s = std::fs::read_to_string(&config_path) 91 .expect("Failed to read config file"); 92 toml::from_str(&s) 93 .expect("Failed to parse config file") 94 } else { 95 if !quiet { 96 println!("Missing config, using defaults instead of {}", config_path.display()); 97 } 98 Config::default() 99 } 100 } 101 } 102 103 impl Default for Config { 104 fn default() -> Self { 105 Config { 106 folder: shellexpand::tilde(&"~/zettel").to_string().into(), 107 document_name: default_document_name(), 108 } 109 } 110 } 111 112 fn app_base_dir() -> ProjectDirs { 113 ProjectDirs::from("org", "equalsraf", "digest") 114 .expect("Unable to determine user config path") 115 } 116 117 fn main() { 118 let cli = Cli::parse(); 119 let cfg = Config::load(cli.quiet); 120 121 let state_path = app_base_dir() 122 .state_dir() 123 .expect("Unable to determine XDG state dir") 124 .to_path_buf(); 125 std::fs::create_dir_all(&state_path) 126 .expect("Failed to create XDG state path"); 127 128 129 let dbpath = state_path.join("digest.db"); 130 if !cli.quiet { 131 println!("Opening {}", dbpath.display()); 132 } 133 let mut db = Database::from_file(dbpath) 134 .expect("Unable to open DB"); 135 136 match &cli.command { 137 Some(Commands::Config) => { 138 println!("{:#?}", cfg); 139 } 140 Some(Commands::Folder) => { 141 println!("{}", cfg.folder.display()); 142 } 143 Some(Commands::Zlist) => { 144 for zettel in &db.all_zettels().expect("Failed to read DB") { 145 println!("{} | {}", zettel.id, zettel.title); 146 } 147 } 148 Some(Commands::Zlinks) => { 149 for link in &db.all_links().expect("Failed to read DB") { 150 println!("{} -> {}", link.from, link.to); 151 } 152 } 153 Some(Commands::Zmissing) => { 154 if !cli.quiet { 155 println!("List of links without a zettel:"); 156 } 157 for id in &db.missing_zettels().expect("Failed to read DB") { 158 println!("- {}", id); 159 } 160 } 161 Some(Commands::New) => { 162 let id = uuid::Uuid::new_v4(); 163 let folder = cfg.folder 164 .join(id.to_string()); 165 166 std::fs::create_dir_all(&folder) 167 .expect("Failed to create folder"); 168 let path = folder.join(&cfg.document_name); 169 println!("{}", path.display()); 170 } 171 Some(Commands::Zsearch { query, line_format, path_relative_to }) => { 172 173 let zettels = if query.is_empty() { 174 db.all_zettels().expect("Failed to read DB") 175 } else { 176 db.search(&query).expect("Failed to read DB") 177 }; 178 179 for z in zettels { 180 let path = match path_relative_to { 181 None => z.path, 182 Some(base) => { 183 pathdiff::diff_paths(&z.path, base) 184 .and_then(|p| p.to_str().map(String::from) ) 185 .unwrap_or(z.path) 186 } 187 }; 188 189 let line_format = line_format.as_ref() 190 .map(|s| s.as_str()) 191 .unwrap_or("=> {path} {title}"); 192 193 let mut fields: HashMap<&str, &dyn Fmt> = HashMap::new(); 194 fields.insert("path", &path); 195 fields.insert("title", &z.title); 196 fields.insert("NULL", &"\0"); 197 fields.insert("NL", &"\n"); 198 199 let line = fields.format(&line_format).expect("Unable to format output with fmt string"); 200 201 println!("{}", line); 202 } 203 } 204 Some(Commands::Tasks { line_format }) => { 205 for t in db.all_tasks().expect("Failed to read DB") { 206 207 let line_format = line_format.as_ref() 208 .map(|s| s.as_str()) 209 .unwrap_or("=> {path} {description}"); 210 211 212 let path = cfg.folder 213 .join(&t.id) 214 .join(&cfg.document_name) 215 .display() 216 .to_string(); 217 218 219 let mut fields: HashMap<&str, &dyn Fmt> = HashMap::new(); 220 fields.insert("id", &t.id); 221 fields.insert("description", &t.description); 222 fields.insert("path", &path); 223 fields.insert("NULL", &"\0"); 224 fields.insert("NL", &"\n"); 225 226 let line = fields.format(&line_format).expect("Unable to format output with fmt string"); 227 228 println!("{}", line); 229 } 230 } 231 Some(Commands::Update) => { 232 println!("Updating DB from {}", cfg.folder.display()); 233 let stats = index(cfg.folder, 234 &mut db, 235 &cfg.document_name, 236 ) 237 .expect("Failed to index files"); 238 println!("Indexed {} entries in {}s", stats.count, stats.duration.as_secs()); 239 } 240 None => {} 241 } 242 }