use crate::{ cass::{Cassiopeia, TimeFile}, Address, Client, Invoice, InvoiceId, Io, }; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::{ fs::{self, File, OpenOptions as Oo}, io::{Read, Write}, path::PathBuf, }; use xdg::BaseDirectories as BaseDirs; #[derive(Debug)] pub struct Meta { clients: BTreeMap, pub dir: BaseDirs, pub invoice_dir: PathBuf, pub template: Option, pub revisioning: bool, /// Optional current timefile path pub timefile: Option, pub project_id: Option, } /// #[derive(Debug, Serialize, Deserialize)] pub struct Config { pub revisioning: bool, pub invoice_dir: PathBuf, } impl Config { fn load(path: PathBuf) -> Self { let mut buf = String::new(); let mut f = File::open(path).unwrap(); f.read_to_string(&mut buf).unwrap(); Self::from_yaml(buf) } fn store(&self, path: PathBuf) { let mut f = Oo::new().truncate(true).write(true).open(path).unwrap(); let buf = self.to_yaml(); f.write_all(buf.as_bytes()).unwrap(); } } impl Meta { pub fn new(dir: BaseDirs) -> Self { // Get the path to the configuration, and make sure a default // configuration is created if none exists yet. let path = config_path(&dir); let yml = Config::load(path); Self { dir, clients: BTreeMap::new(), invoice_dir: yml.invoice_dir, template: None, revisioning: yml.revisioning, timefile: None, project_id: None, } } pub fn load_timefile(&mut self, path: &str) { self.timefile = Some(Cassiopeia::load(path) .map(|s| s.timefile()) .unwrap_or_else(|e| fatal!("Failed reading timefile: {}", e.to_string()))); } pub fn load_config(&self) -> Config { let path = config_path(&self.dir); Config::load(path) } pub fn store_config(&self, cfg: Config) { let path = config_path(&self.dir); cfg.store(path); } pub fn client_mut(&mut self, name: &str) -> Option<&mut Client> { self.clients.get_mut(name) } pub fn new_client(&mut self, name: &str, address: Address) { self.clients.insert( name.to_string(), Client { name: name.to_string(), address, last_project: None, }, ); } pub fn new_invoice_id(&self) -> InvoiceId { let files = match fs::read_dir(self.invoice_dir.as_path()) { Ok(f) => f, Err(_) => { let path = self .dir .create_config_directory("invoices") .unwrap_or_else(|_| fatal!("Failed to create invoice directory")); fs::read_dir(path).unwrap() } }; files.into_iter().fold(InvoiceId::date(), |inv, f| { let f = f.unwrap(); let i = Invoice::load(&f.path()).unwrap(); inv.find_next(&i) }) } /// Update a configuration value and write it back to disk pub fn update_config(&self, key: String, value: Option) { let mut cfg = self.load_config(); match (key.as_str(), value) { ("invoice_dir", Some(val)) => cfg.invoice_dir = PathBuf::new().join(val), ("invoice_dir", None) => fatal!("Can not unset 'invoice_dir' configuration key"), (key, _) => fatal!("Unrecognised configuration key '{}'", key), } self.store_config(cfg); } } /// Initialise a k-office application state pub fn initialise() -> Meta { let dir = BaseDirs::with_prefix("k-koffice").unwrap(); dir.create_config_directory("") .unwrap_or_else(|_| fatal!("Couldn't create config directory")); Meta::new(dir) } fn config_path(dir: &BaseDirs) -> PathBuf { dir.find_config_file("config.yml").unwrap_or_else(|| { let path = dir.place_config_file("config.yml").unwrap(); let mut cfg = File::create(path.clone()).unwrap(); let invoice_dir: String = dir .get_config_home() .join("invoices") .to_str() .map(Into::into) .unwrap_or_else(|| fatal!("XDG_CONFIG_HOME contained non UTF-8 characters :(")); let buf = format!( "revisioning: true invoice_dir: {}", invoice_dir ); cfg.write_all(buf.as_bytes()).unwrap(); path }) }