Add Config System instead of hardcoding values

Config is read from `$XDG_CONFIG_DIRECTORY/ibis/config.toml`
falling back to `~/.config/ibis/config.toml` if `$XDG_CONFIG_DIRECTORY`
is not set.
This commit is contained in:
Jana Lemke 2022-04-15 20:07:12 +02:00
parent 246d5e6688
commit 71cfee95bc
7 changed files with 110 additions and 42 deletions

View file

@ -16,6 +16,8 @@ glob = "0.3"
anyhow = "1" anyhow = "1"
chrono = {version = "0.4.19", features = ["serde"]} chrono = {version = "0.4.19", features = ["serde"]}
rofi = {version = "0.2", optional = true} rofi = {version = "0.2", optional = true}
lazy_static = "1.4.0"
directories = "4.0.1"
[lib] [lib]
name = "ibis" name = "ibis"

View file

@ -2,7 +2,6 @@
- `check` complains about some of the defaults. Make sure default settings conform with opinions of `check` - `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 - refactor to move functionality like rofi to shell scripts
Instead provide more scriptable functionality Instead provide more scriptable functionality
- actual config instead of hardcoding values
# Things to think about # Things to think about
- Do I want to copy templates when creating a course? - Do I want to copy templates when creating a course?

View file

@ -1,14 +1,83 @@
use serde::{Deserialize, Serialize}; use directories::{BaseDirs, ProjectDirs};
use serde::{de::Error, Deserialize, Deserializer, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt; 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 { 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, 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, pub files_root: PathBuf,
/// File extensions that should be treated as lecture notes
pub note_extensions: Vec<String>, pub note_extensions: Vec<String>,
} }
fn ensure_absolute_path<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
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)] #[derive(StructOpt, Default)]
pub struct ListOptions { pub struct ListOptions {
#[structopt(short, long)] #[structopt(short, long)]

View file

@ -1,4 +1,4 @@
use crate::config::{Config, ListOptions}; use crate::config::{ListOptions, CONFIG};
use crate::util; use crate::util;
use anyhow::Result; use anyhow::Result;
use glob::glob; use glob::glob;
@ -53,16 +53,17 @@ impl Course {
&self.meetings &self.meetings
} }
pub fn set(&self, conf: &Config) -> Result<()> { pub fn set(&self) -> Result<()> {
remove_file(&conf.link_path)?; // TODO: make sure this is actually a link, and we are not removing some other file of the user
symlink(&self.path, &conf.link_path)?; remove_file(&CONFIG.link_path)?;
symlink(&self.path, &CONFIG.link_path)?;
Ok(()) Ok(())
} }
pub fn list(conf: &Config, options: &ListOptions) -> Result<CourseList> { pub fn list(options: &ListOptions) -> Result<CourseList> {
let mut courses: Vec<Course> = vec![]; let mut courses: Vec<Course> = vec![];
let pattern = [ let pattern = [
conf.files_root.canonicalize().unwrap().to_str().unwrap(), CONFIG.files_root.canonicalize().unwrap().to_str().unwrap(),
"/*/course.toml", "/*/course.toml",
] ]
.concat(); .concat();
@ -82,9 +83,9 @@ impl Course {
Ok(CourseList::new(courses)) 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 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"); println!("Checking course.toml's");
for c in toml_list.iter() { for c in toml_list.iter() {
let mut problems = Vec::new(); let mut problems = Vec::new();

View file

@ -1,3 +1,6 @@
#[macro_use]
extern crate lazy_static;
pub mod config; pub mod config;
pub mod courses; pub mod courses;
pub mod rofi; pub mod rofi;

View file

@ -4,8 +4,8 @@ use std::io::prelude::*;
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt; use structopt::StructOpt;
use ibis::config::{Config, ListOptions}; use ibis::config::{ListOptions, CONFIG};
use ibis::courses::Course; use ibis::courses::{Course, Meeting};
use ibis::{rofi, util}; use ibis::{rofi, util};
#[derive(StructOpt)] #[derive(StructOpt)]
@ -18,6 +18,7 @@ struct Cli {
#[derive(StructOpt)] #[derive(StructOpt)]
enum Command { enum Command {
ShowConfig,
ListCourses(ListOptions), ListCourses(ListOptions),
ListLectures(ListOptions), ListLectures(ListOptions),
SelectCourse(ListOptions), SelectCourse(ListOptions),
@ -41,42 +42,36 @@ enum Object {
} }
fn main() -> Result<()> { 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(); let args = Cli::from_args();
match args.cmd { match args.cmd {
// Some(subcommand) => match subcommand {
Command::ListCourses(options) => { Command::ListCourses(options) => {
for c in Course::list(&conf, &options)?.iter() { for c in Course::list(&options)?.iter() {
println!("{}", c); println!("{}", c);
} }
} }
Command::ListLectures(options) => { Command::ListLectures(options) => {
for l in list_lectures(&conf, options)? { for l in list_lectures(options)? {
println!("{:?}", l); println!("{:?}", l);
} }
} }
Command::AddCourse(course) => add(course, &conf, args.debug)?, Command::AddCourse(course) => add(course, args.debug)?,
Command::AddLecture => (), Command::AddLecture => (),
Command::SelectCourse(options) => rofi::select_course(&conf, &options)?, Command::SelectCourse(options) => rofi::select_course(&options)?,
Command::SelectLecture => (), Command::SelectLecture => (),
Command::Init => (), Command::Init => (),
Command::Check(list_options) => Course::check(&conf, &list_options)?, Command::Check(list_options) => Course::check(&list_options)?,
Command::Meetings(options) => rofi::select_meeting(&conf, &options)?, Command::Meetings(options) => rofi::select_meeting(&options)?,
//}, Command::ShowConfig => println!("{:?}", *CONFIG),
// None => println!("No command given"),
}; };
Ok(()) Ok(())
} }
fn add(course: Course, conf: &Config, debug: bool) -> Result<()> { fn add(course: Course, debug: bool) -> Result<()> {
let toml = toml::to_string(&course)?; let toml = toml::to_string(&course)?;
println!("{}", toml); println!("{}", toml);
let target_dir = [ let target_dir = [
conf.files_root CONFIG
.files_root
.canonicalize()? .canonicalize()?
.to_str() .to_str()
.expect("I hate paths"), // TODO .expect("I hate paths"), // TODO
@ -86,9 +81,9 @@ fn add(course: Course, conf: &Config, debug: bool) -> Result<()> {
.concat(); .concat();
let toml_path = PathBuf::from([&target_dir, "/course.toml"].concat()); let toml_path = PathBuf::from([&target_dir, "/course.toml"].concat());
if !(cfg!(debug_assertions) || debug) { if !(cfg!(debug_assertions) || debug) {
if !conf.files_root.is_dir() { if !CONFIG.files_root.is_dir() {
std::fs::create_dir_all(conf.files_root.canonicalize().with_context(|| { std::fs::create_dir_all(CONFIG.files_root.canonicalize().with_context(|| {
format!("The root path {:?} seems malformed", conf.files_root) format!("The root path {:?} seems malformed", CONFIG.files_root)
})?)?; })?)?;
} }
if !PathBuf::from(&target_dir).is_dir() { if !PathBuf::from(&target_dir).is_dir() {
@ -104,12 +99,12 @@ fn add(course: Course, conf: &Config, debug: bool) -> Result<()> {
Ok(()) Ok(())
} }
fn list_lectures(conf: &Config, options: ListOptions) -> Result<Vec<PathBuf>> { fn list_lectures(options: ListOptions) -> Result<Vec<PathBuf>> {
let lectures = util::glob_paths( let lectures = util::glob_paths(
if options.all { if options.all {
&conf.files_root &CONFIG.files_root
} else { } else {
&conf.link_path &CONFIG.link_path
}, },
"/**/lecture*", "/**/lecture*",
); );
@ -119,7 +114,7 @@ fn list_lectures(conf: &Config, options: ListOptions) -> Result<Vec<PathBuf>> {
.filter(|path| { .filter(|path| {
if let Some(ext) = path.extension() { if let Some(ext) = path.extension() {
let ext = ext.to_owned().to_string_lossy().into(); let ext = ext.to_owned().to_string_lossy().into();
conf.note_extensions.contains(&ext) CONFIG.note_extensions.contains(&ext)
} else { } else {
false false
} }

View file

@ -1,4 +1,3 @@
use crate::config::Config;
use crate::{config::ListOptions, courses::*}; use crate::{config::ListOptions, courses::*};
use anyhow::Result; use anyhow::Result;
use rofi::Rofi; use rofi::Rofi;
@ -6,18 +5,18 @@ use std::io::Write;
use std::process::Command; use std::process::Command;
use std::process::Stdio; use std::process::Stdio;
pub fn select_course(conf: &Config, options: &ListOptions) -> Result<()> { pub fn select_course(options: &ListOptions) -> Result<()> {
let courses = Course::list(conf, options)?; let courses = Course::list(options)?;
let course_names: Vec<_> = courses.iter().map(|c| &c.name).collect(); let course_names: Vec<_> = courses.iter().map(|c| &c.name).collect();
let element = Rofi::new(&course_names).run_index()?; let element = Rofi::new(&course_names).run_index()?;
courses courses
.get(element) .get(element)
.expect("Could not find selected Course") .expect("Could not find selected Course")
.set(conf) .set()
} }
pub fn select_meeting(conf: &Config, options: &ListOptions) -> Result<()> { pub fn select_meeting(options: &ListOptions) -> Result<()> {
let meetings: Vec<_> = Course::list(conf, options)? let meetings: Vec<_> = Course::list(options)?
.iter() .iter()
.cloned() .cloned()
.flat_map(|c: Course| { .flat_map(|c: Course| {