/ src / main.rs
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  }