From 06d283b17cefe1de884e578d134cef1ee7b634fe Mon Sep 17 00:00:00 2001 From: Jana Lemke Date: Wed, 24 May 2023 11:11:12 +0200 Subject: [PATCH] Add lots of functionality This is in a usable state now and I have been using it for the last months. --- .gitignore | 3 +- Cargo.toml | 21 ++++-- src/config.rs | 22 ++++++ src/courses.rs | 188 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 ++ src/main.rs | 153 +++++++++++++++++++++++++--------------- src/rofi.rs | 53 ++++++++++++++ src/util.rs | 45 ++++++++++++ 8 files changed, 429 insertions(+), 60 deletions(-) create mode 100644 src/config.rs create mode 100644 src/courses.rs create mode 100644 src/lib.rs create mode 100644 src/rofi.rs create mode 100644 src/util.rs diff --git a/.gitignore b/.gitignore index 869df07..eb1d4ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -Cargo.lock \ No newline at end of file +Cargo.lock +.vscode \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 524c131..93b0fcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,23 @@ version = "0.1.0" authors = ["Jana Lemke "] edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["rofi-menu"] +rofi-menu = ["rofi"] [dependencies] -structopt = "0.3.16" +structopt = "0.3" serde = { version = "1.0", features = ["derive"]} -toml = "0.5.7" -glob = "0.3.0" \ No newline at end of file +toml = "0.5" +glob = "0.3" +anyhow = "1" +chrono = {version = "0.4.19", features = ["serde"]} +rofi = {version = "0.2", optional = true} + +[lib] +name = "ibis" +path = "src/lib.rs" + +[[bin]] +name = "ibis" +path = "src/main.rs" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..bffce13 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use structopt::StructOpt; + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub link_path: PathBuf, + pub files_root: PathBuf, + pub note_extensions: Vec, +} + +#[derive(StructOpt)] +pub struct ListOptions { + #[structopt(short, long)] + pub all: bool, +} + +impl Default for ListOptions { + fn default() -> Self { + ListOptions { all: false } + } +} diff --git a/src/courses.rs b/src/courses.rs new file mode 100644 index 0000000..f85e5cd --- /dev/null +++ b/src/courses.rs @@ -0,0 +1,188 @@ +use crate::config::{Config, ListOptions}; +use crate::util; +use anyhow::Result; +use glob::glob; +use serde::{Deserialize, Deserializer, Serialize}; +use std::io::prelude::*; +use std::os::unix::fs::symlink; +use std::{ + fs::{remove_file, File}, + path::PathBuf, +}; +use structopt::StructOpt; + +#[derive(Serialize, Deserialize, StructOpt, Debug, Clone)] +pub struct Course { + pub name: String, + #[serde(skip)] + #[structopt(skip)] + pub path: PathBuf, + short: String, + #[serde(default)] + #[structopt(long)] + url: Option, + #[serde(default)] + #[structopt(long)] + moodle: Option, + #[serde(deserialize_with = "string_or_vec", alias = "semester")] + semesters: Vec, + #[structopt(skip)] + meetings: Option>, +} + +#[derive(Serialize, Deserialize, StructOpt, Debug, Clone)] +pub struct Meeting { + pub name: String, + pub url: String, +} + +impl Course { + /// Get a reference to the course's semesters. + pub fn semesters(&self) -> &Vec { + &self.semesters + } + + /// Check if course is part of current semester + pub fn is_current(&self) -> bool { + self.semesters() + .iter() + .any(|x| x == &util::get_current_semester()) + } + + pub fn meetings(&self) -> &Option> { + &self.meetings + } + + pub fn set(&self, conf: &Config) -> Result<()> { + remove_file(&conf.link_path)?; + symlink(&self.path, &conf.link_path)?; + Ok(()) + } + + pub fn list(conf: &Config, options: &ListOptions) -> Result { + let mut courses: Vec = vec![]; + let pattern = [ + conf.files_root.canonicalize().unwrap().to_str().unwrap(), + "/*/course.toml", + ] + .concat(); + let mut handle; + for entry in glob(pattern.as_str()).unwrap() { + let path = entry.unwrap(); + let mut coursestring = String::new(); + handle = File::open(&path).unwrap(); + handle.read_to_string(&mut coursestring).unwrap(); + let mut course: Course = toml::from_str(&coursestring).expect(&format!("{:?}", &path)); + course.path = path.parent().unwrap().to_path_buf(); + if options.all || course.is_current() { + courses.push(course); + } + } + + Ok(CourseList::new(courses)) + } + + pub fn check(conf: &Config, list_options: &ListOptions) -> Result<()> { + let mut problematic_course_count = 0; + let toml_list = Self::list(conf, list_options)?; + println!("Checking course.toml's"); + for c in toml_list.iter() { + let mut problems = Vec::new(); + if c.name.is_empty() { + problems.push(" Name is empty") + } + if c.short.is_empty() { + problems.push(" Short is empty") + } + if c.url.is_none() { + problems.push(" No URL (set empty string to mark nonexistant website)") + } + if c.moodle.is_none() { + problems.push(" No Moodle-URL (set empty string to mark nonexistant website)") + } + if c.semesters.is_empty() { + problems.push(" Course is not in any semesters") + } + if !problems.is_empty() { + eprintln!( + "Found {} problems with {}/course.toml", + problems.len(), + c.path.to_str().unwrap() + ); + for p in problems { + eprintln!("{}", p) + } + problematic_course_count += 1; + } else { + println!("No problems found with {:?}", c.path); + } + } + println!("Found {} courses with problems", problematic_course_count); + Ok(()) + } +} + +fn string_or_vec<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrVec { + String(String), + Vec(Vec), + } + + match StringOrVec::deserialize(deserializer)? { + StringOrVec::String(s) => Ok(vec![s]), + StringOrVec::Vec(vec) => Ok(vec), + } +} +pub struct CourseList(Vec); + +impl std::fmt::Display for CourseList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for c in &self.0 { + writeln!(f, "{}", c)?; + } + Ok(()) + } +} + +impl CourseList { + pub fn new(inner: Vec) -> Self { + CourseList(inner) + } + + pub fn iter(&self) -> std::slice::Iter { + self.0.iter() + } + + pub fn get(&self, element: usize) -> Option<&Course> { + self.0.get(element) + } +} + +impl std::fmt::Display for Course { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +// #[derive(Debug)] +// pub enum ParseSemesterError{ +// ParseIntError(ParseIntError), +// NoFormatFound, +// } + +// impl std::fmt::Display for ParseSemesterError{ +// fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// todo!() +// } +// } + +// impl std::convert::From for ParseSemesterError{ +// fn from(e: ParseIntError) -> Self { +// Self::ParseIntError(e) +// } +// } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1354c63 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod courses; +pub mod rofi; +pub mod util; diff --git a/src/main.rs b/src/main.rs index 81dc610..dbef314 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,90 +1,133 @@ -use serde::{Deserialize, Serialize}; -use structopt::StructOpt; -use std::path::{Path, PathBuf}; -use glob::glob; +use anyhow::{Context, Result}; use std::fs::File; use std::io::prelude::*; +use std::path::PathBuf; +use structopt::StructOpt; -#[derive(Serialize, Deserialize)] -struct Config { - link_path: PathBuf, - files_root: PathBuf, -} +use ibis::config::{Config, ListOptions}; +use ibis::courses::Course; +use ibis::{rofi, util}; -#[derive(Serialize, Deserialize)] -struct Course{ - name: String, - short: String, - semester: String, - url: Option, - moodle: Option -} #[derive(StructOpt)] struct Cli { + #[structopt(short, long)] + debug: bool, #[structopt(subcommand)] cmd: Option, } #[derive(StructOpt)] enum Command { - List(ObjectStruct), - Select(ObjectStruct), - Add(ObjectStruct), + ListCourses(ListOptions), + ListLectures(ListOptions), + SelectCourse(ListOptions), + SelectLecture, + AddCourse(Course), + AddLecture, Init, + Check(ListOptions), + Meetings(ListOptions), } -#[derive(StructOpt)] -struct ObjectStruct { - #[structopt(subcommand)] - object: Option, -} +// #[derive(StructOpt)] +// struct ObjectStruct { +// #[structopt(subcommand)] +// object: Option, +// } #[derive(StructOpt, Debug)] enum Object { Courses, Lectures, } -fn main() { - let conf = Config{link_path: PathBuf::from("/home/jana/current-course"), files_root: PathBuf::from("/home/jana/uni")}; +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::List(object) => list(object.object, conf), - Command::Add(object) => (), - Command::Select(object) => (), + Command::ListCourses(options) => { + for c in Course::list(&conf, &options)?.iter() { + println!("{}", c); + } + } + Command::ListLectures(options) => { + for l in list_lectures(&conf, options)? { + println!("{:?}", l); + } + } + Command::AddCourse(course) => add(course, &conf, args.debug)?, + Command::AddLecture => (), + Command::SelectCourse(options) => rofi::select_course(&conf, &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"), - } + }; + Ok(()) } -fn list(object: Option, conf: Config) { - if object.is_some() { - let object = object.unwrap(); - match object { - Object::Courses => {list_courses(conf);}, - Object::Lectures => list_lectures(), +fn add(course: Course, conf: &Config, debug: bool) -> Result<()> { + let toml = toml::to_string(&course)?; + println!("{}", toml); + let target_dir = [ + conf.files_root + .canonicalize()? + .to_str() + .expect("I hate paths"), // TODO + "/", + util::to_snail_case(&course.name).as_ref(), + ] + .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 !PathBuf::from(&target_dir).is_dir() { + std::fs::create_dir_all(&target_dir)?; + } else { + panic!("Folder {} already exists!", &target_dir); + } + let mut toml_handle = File::create(&toml_path).unwrap(); + write!(toml_handle, "{}", toml).unwrap(); + println!("Created files!") } + println!("Path was {:?}", &toml_path); + Ok(()) } -fn list_courses(conf: Config) -> Vec{ - let mut courses: Vec = vec![]; - let pattern = [conf.files_root.canonicalize().unwrap().to_str().unwrap(), "/**/course.toml"].concat(); - let mut handle; - for entry in glob(pattern.as_str()).unwrap(){ - let path = entry.unwrap(); - let mut coursestring = String::new(); - handle = File::open(path).unwrap(); - handle.read_to_string(&mut coursestring); - courses.push(toml::from_str(&coursestring).unwrap()); - } - for c in &courses{ - println!("Course {}({}) in {}, url {:?}, moodle {:?}", c.name, c.short, c.semester, c.url, c.moodle); - } - courses -} +fn list_lectures(conf: &Config, options: ListOptions) -> Result> { + let lectures = util::glob_paths( + if options.all { + &conf.files_root + } else { + &conf.link_path + }, + "/**/lecture*", + ); -fn list_lectures() { - println!("Listing lectures") + let lectures = lectures? + .into_iter() + .filter_map(|path| { + if let Some(ext) = path.extension() { + let ext = ext.to_owned().to_string_lossy().into(); + if conf.note_extensions.contains(&ext) { + Some(path) + } else { + None + } + } else { + None + } + }) + .collect(); + Ok(lectures) } diff --git a/src/rofi.rs b/src/rofi.rs new file mode 100644 index 0000000..2d060c2 --- /dev/null +++ b/src/rofi.rs @@ -0,0 +1,53 @@ +use crate::config::Config; +use crate::{config::ListOptions, courses::*}; +use anyhow::Result; +use rofi::Rofi; +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)?; + 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) +} + +pub fn select_meeting(conf: &Config, options: &ListOptions) -> Result<()> { + let meetings: Vec<_> = Course::list(conf, options)? + .iter() + .cloned() + .map(|c: Course| { + if let Some(meetings) = c.meetings() { + meetings.clone() + } else { + Vec::new() + } + .iter() + .map(move |m| (c.clone(), m.clone())) + .collect::>() + }) + .flatten() + .collect(); + let meeting_names: Vec<_> = meetings + .iter() + .map(|(c, m)| format!("{:<30} {:>30}", &c.name, &m.name)) + .collect(); + let element = Rofi::new(&meeting_names).run_index()?; + let mut clip = Command::new("xclip") + .args(&["-sel", "clip"]) + .stdin(Stdio::piped()) + .spawn() + .expect("Could not spawn xclip process"); + let url = &meetings[element].1.url; + { + let pipe = clip.stdin.as_mut().expect("Could not write to xclip stdin"); + pipe.write_all(url.as_bytes()) + .expect("Writing to xclip stdin failed"); + } + clip.wait()?; + Ok(()) +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..4b39424 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,45 @@ +use anyhow::Result; +use chrono::{DateTime, Datelike, Local}; +use std::path::{Path, PathBuf}; +use std::time::SystemTime; + +pub fn to_snail_case(from: impl Into) -> String { + from.into().replace(" ", "_").to_lowercase() +} + +// pub fn to_title_case(from: &str) -> String { +// from.to_string().split(|c| c ==' ' || c == '_').map(|s| {if s.len() >= 1 {s[0 as usize] = s[0 as usize].to_uppercase()}}).collect() +// } + +pub fn get_current_semester() -> String { + let today = DateTime::::from(SystemTime::now()) + .date() + .naive_local(); + let (part, year) = if today.month() >= 4 && today.month() < 10 { + ("SS", today.year()) + } else { + ( + "WS", + if today.month() >= 10 { + today.year() + } else { + today.year() - 1 + }, + ) + }; + format!("{}{}", part, year - 2000) +} + +pub fn glob_paths, S: AsRef>(path: P, pattern: S) -> Result> { + let mut paths = vec![]; + let pattern = [ + (*(path.as_ref())).canonicalize()?.to_str().unwrap(), + pattern.as_ref(), + ] + .concat(); + for entry in glob::glob(pattern.as_str())? { + let path = entry?; + paths.push(path); + } + Ok(paths) +}