diff --git a/Cargo.toml b/Cargo.toml index 93b0fcd..8771638 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ glob = "0.3" anyhow = "1" chrono = {version = "0.4.19", features = ["serde"]} rofi = {version = "0.2", optional = true} +lazy_static = "1.4.0" +directories = "4.0.1" [lib] name = "ibis" diff --git a/notes.md b/notes.md index 599c971..9814c63 100644 --- a/notes.md +++ b/notes.md @@ -2,7 +2,6 @@ - `check` complains about some of the defaults. Make sure default settings conform with opinions of `check` - refactor to move functionality like rofi to shell scripts Instead provide more scriptable functionality -- actual config instead of hardcoding values # Things to think about - Do I want to copy templates when creating a course? diff --git a/src/config.rs b/src/config.rs index 5add9f4..07af429 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,14 +1,83 @@ -use serde::{Deserialize, Serialize}; +use directories::{BaseDirs, ProjectDirs}; +use serde::{de::Error, Deserialize, Deserializer, Serialize}; use std::path::PathBuf; use structopt::StructOpt; -#[derive(Serialize, Deserialize)] +lazy_static! { + pub static ref CONFIG: Config = { + let config_path = ProjectDirs::from("", "Phobos", "ibis") + .expect("no valid home directory found") + .config_dir() + .to_owned() + .join("config.toml"); + if !config_path.exists() { + return Config::default(); + } + let config_str = std::fs::read_to_string(&config_path); + if let Ok(config_str) = config_str { + toml::from_str(&config_str).unwrap_or_else(|e| { + panic!("Error while parsing config at {:?} \n{}", &config_path, e); + }) + } else { + panic!( + "Error while reading config at {:?}\n{}", + &config_path, + config_str.unwrap_err() + ); + } + }; +} + +/// A struct representing the config options +#[derive(Serialize, Deserialize, Debug)] pub struct Config { + /// The location of the link pointing to the current course. + /// This is treated as the single source of truth as to what the current course is. + /// This needs to be a absolute path. + #[serde(deserialize_with = "ensure_absolute_path")] pub link_path: PathBuf, + /// The folder under which all other folders of "courses" are located. + /// "courses" are folders containing a `course.toml` + /// This needs to be an absolute path. + #[serde(deserialize_with = "ensure_absolute_path")] pub files_root: PathBuf, + /// File extensions that should be treated as lecture notes pub note_extensions: Vec, } +fn ensure_absolute_path<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, + D::Error: Error, +{ + let path = PathBuf::deserialize(deserializer)?; + if path.is_absolute() { + Ok(path) + } else { + Err(D::Error::invalid_value( + serde::de::Unexpected::Str(path.to_string_lossy().as_ref()), + &"an absolute path", + )) + } +} + +impl Default for Config { + fn default() -> Self { + let home = BaseDirs::new() + .expect("no valid home directory found") + .home_dir() + .to_owned(); + Self { + link_path: home.join("current-course"), + files_root: home + .join("uni") + .canonicalize() + .expect("\"~/uni\" could not be canonicalized"), + note_extensions: vec![String::from("tex"), String::from("md"), String::from("org")], + } + } +} + #[derive(StructOpt, Default)] pub struct ListOptions { #[structopt(short, long)] diff --git a/src/courses.rs b/src/courses.rs index a17959a..a28c247 100644 --- a/src/courses.rs +++ b/src/courses.rs @@ -1,4 +1,4 @@ -use crate::config::{Config, ListOptions}; +use crate::config::{ListOptions, CONFIG}; use crate::util; use anyhow::Result; use glob::glob; @@ -53,16 +53,17 @@ impl Course { &self.meetings } - pub fn set(&self, conf: &Config) -> Result<()> { - remove_file(&conf.link_path)?; - symlink(&self.path, &conf.link_path)?; + pub fn set(&self) -> Result<()> { + // TODO: make sure this is actually a link, and we are not removing some other file of the user + remove_file(&CONFIG.link_path)?; + symlink(&self.path, &CONFIG.link_path)?; Ok(()) } - pub fn list(conf: &Config, options: &ListOptions) -> Result { + pub fn list(options: &ListOptions) -> Result { let mut courses: Vec = vec![]; let pattern = [ - conf.files_root.canonicalize().unwrap().to_str().unwrap(), + CONFIG.files_root.canonicalize().unwrap().to_str().unwrap(), "/*/course.toml", ] .concat(); @@ -82,9 +83,9 @@ impl Course { Ok(CourseList::new(courses)) } - pub fn check(conf: &Config, list_options: &ListOptions) -> Result<()> { + pub fn check(list_options: &ListOptions) -> Result<()> { let mut problematic_course_count = 0; - let toml_list = Self::list(conf, list_options)?; + let toml_list = Self::list(list_options)?; println!("Checking course.toml's"); for c in toml_list.iter() { let mut problems = Vec::new(); diff --git a/src/lib.rs b/src/lib.rs index 1354c63..ff3100d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +#[macro_use] +extern crate lazy_static; + pub mod config; pub mod courses; pub mod rofi; diff --git a/src/main.rs b/src/main.rs index eb6d9dd..dfb0dd3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,8 @@ use std::io::prelude::*; use std::path::PathBuf; use structopt::StructOpt; -use ibis::config::{Config, ListOptions}; -use ibis::courses::Course; +use ibis::config::{ListOptions, CONFIG}; +use ibis::courses::{Course, Meeting}; use ibis::{rofi, util}; #[derive(StructOpt)] @@ -18,6 +18,7 @@ struct Cli { #[derive(StructOpt)] enum Command { + ShowConfig, ListCourses(ListOptions), ListLectures(ListOptions), SelectCourse(ListOptions), @@ -41,42 +42,36 @@ enum Object { } fn main() -> Result<()> { - let conf = Config { - link_path: PathBuf::from("/home/jana/current-course"), - files_root: PathBuf::from("/home/jana/uni"), - note_extensions: vec![String::from("tex"), String::from("md"), String::from("org")], - }; let args = Cli::from_args(); match args.cmd { - // Some(subcommand) => match subcommand { Command::ListCourses(options) => { - for c in Course::list(&conf, &options)?.iter() { + for c in Course::list(&options)?.iter() { println!("{}", c); } } Command::ListLectures(options) => { - for l in list_lectures(&conf, options)? { + for l in list_lectures(options)? { println!("{:?}", l); } } - Command::AddCourse(course) => add(course, &conf, args.debug)?, + Command::AddCourse(course) => add(course, args.debug)?, Command::AddLecture => (), - Command::SelectCourse(options) => rofi::select_course(&conf, &options)?, + Command::SelectCourse(options) => rofi::select_course(&options)?, Command::SelectLecture => (), Command::Init => (), - Command::Check(list_options) => Course::check(&conf, &list_options)?, - Command::Meetings(options) => rofi::select_meeting(&conf, &options)?, - //}, - // None => println!("No command given"), + Command::Check(list_options) => Course::check(&list_options)?, + Command::Meetings(options) => rofi::select_meeting(&options)?, + Command::ShowConfig => println!("{:?}", *CONFIG), }; Ok(()) } -fn add(course: Course, conf: &Config, debug: bool) -> Result<()> { +fn add(course: Course, debug: bool) -> Result<()> { let toml = toml::to_string(&course)?; println!("{}", toml); let target_dir = [ - conf.files_root + CONFIG + .files_root .canonicalize()? .to_str() .expect("I hate paths"), // TODO @@ -86,9 +81,9 @@ fn add(course: Course, conf: &Config, debug: bool) -> Result<()> { .concat(); let toml_path = PathBuf::from([&target_dir, "/course.toml"].concat()); if !(cfg!(debug_assertions) || debug) { - if !conf.files_root.is_dir() { - std::fs::create_dir_all(conf.files_root.canonicalize().with_context(|| { - format!("The root path {:?} seems malformed", conf.files_root) + if !CONFIG.files_root.is_dir() { + std::fs::create_dir_all(CONFIG.files_root.canonicalize().with_context(|| { + format!("The root path {:?} seems malformed", CONFIG.files_root) })?)?; } if !PathBuf::from(&target_dir).is_dir() { @@ -104,12 +99,12 @@ fn add(course: Course, conf: &Config, debug: bool) -> Result<()> { Ok(()) } -fn list_lectures(conf: &Config, options: ListOptions) -> Result> { +fn list_lectures(options: ListOptions) -> Result> { let lectures = util::glob_paths( if options.all { - &conf.files_root + &CONFIG.files_root } else { - &conf.link_path + &CONFIG.link_path }, "/**/lecture*", ); @@ -119,7 +114,7 @@ fn list_lectures(conf: &Config, options: ListOptions) -> Result> { .filter(|path| { if let Some(ext) = path.extension() { let ext = ext.to_owned().to_string_lossy().into(); - conf.note_extensions.contains(&ext) + CONFIG.note_extensions.contains(&ext) } else { false } diff --git a/src/rofi.rs b/src/rofi.rs index e304e66..ef56100 100644 --- a/src/rofi.rs +++ b/src/rofi.rs @@ -1,4 +1,3 @@ -use crate::config::Config; use crate::{config::ListOptions, courses::*}; use anyhow::Result; use rofi::Rofi; @@ -6,18 +5,18 @@ use std::io::Write; use std::process::Command; use std::process::Stdio; -pub fn select_course(conf: &Config, options: &ListOptions) -> Result<()> { - let courses = Course::list(conf, options)?; +pub fn select_course(options: &ListOptions) -> Result<()> { + let courses = Course::list(options)?; let course_names: Vec<_> = courses.iter().map(|c| &c.name).collect(); let element = Rofi::new(&course_names).run_index()?; courses .get(element) .expect("Could not find selected Course") - .set(conf) + .set() } -pub fn select_meeting(conf: &Config, options: &ListOptions) -> Result<()> { - let meetings: Vec<_> = Course::list(conf, options)? +pub fn select_meeting(options: &ListOptions) -> Result<()> { + let meetings: Vec<_> = Course::list(options)? .iter() .cloned() .flat_map(|c: Course| {