Add lots of functionality

This is in a usable state now and I have been using it for the last
months.
This commit is contained in:
Jana Lemke 2023-05-24 11:11:12 +02:00
parent 526d6a0c7d
commit 06d283b17c
8 changed files with 429 additions and 60 deletions

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target /target
Cargo.lock Cargo.lock
.vscode

View file

@ -4,10 +4,23 @@ version = "0.1.0"
authors = ["Jana Lemke <dev@jana-lemke.de>"] authors = ["Jana Lemke <dev@jana-lemke.de>"]
edition = "2018" 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] [dependencies]
structopt = "0.3.16" structopt = "0.3"
serde = { version = "1.0", features = ["derive"]} serde = { version = "1.0", features = ["derive"]}
toml = "0.5.7" toml = "0.5"
glob = "0.3.0" 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"

22
src/config.rs Normal file
View file

@ -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<String>,
}
#[derive(StructOpt)]
pub struct ListOptions {
#[structopt(short, long)]
pub all: bool,
}
impl Default for ListOptions {
fn default() -> Self {
ListOptions { all: false }
}
}

188
src/courses.rs Normal file
View file

@ -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<String>,
#[serde(default)]
#[structopt(long)]
moodle: Option<String>,
#[serde(deserialize_with = "string_or_vec", alias = "semester")]
semesters: Vec<String>,
#[structopt(skip)]
meetings: Option<Vec<Meeting>>,
}
#[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<String> {
&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<Vec<Meeting>> {
&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<CourseList> {
let mut courses: Vec<Course> = 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<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrVec {
String(String),
Vec(Vec<String>),
}
match StringOrVec::deserialize(deserializer)? {
StringOrVec::String(s) => Ok(vec![s]),
StringOrVec::Vec(vec) => Ok(vec),
}
}
pub struct CourseList(Vec<Course>);
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<Course>) -> Self {
CourseList(inner)
}
pub fn iter(&self) -> std::slice::Iter<Course> {
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<ParseIntError> for ParseSemesterError{
// fn from(e: ParseIntError) -> Self {
// Self::ParseIntError(e)
// }
// }

4
src/lib.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod config;
pub mod courses;
pub mod rofi;
pub mod util;

View file

@ -1,90 +1,133 @@
use serde::{Deserialize, Serialize}; use anyhow::{Context, Result};
use structopt::StructOpt;
use std::path::{Path, PathBuf};
use glob::glob;
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
use std::path::PathBuf;
use structopt::StructOpt;
#[derive(Serialize, Deserialize)] use ibis::config::{Config, ListOptions};
struct Config { use ibis::courses::Course;
link_path: PathBuf, use ibis::{rofi, util};
files_root: PathBuf,
}
#[derive(Serialize, Deserialize)]
struct Course{
name: String,
short: String,
semester: String,
url: Option<String>,
moodle: Option<String>
}
#[derive(StructOpt)] #[derive(StructOpt)]
struct Cli { struct Cli {
#[structopt(short, long)]
debug: bool,
#[structopt(subcommand)] #[structopt(subcommand)]
cmd: Option<Command>, cmd: Option<Command>,
} }
#[derive(StructOpt)] #[derive(StructOpt)]
enum Command { enum Command {
List(ObjectStruct), ListCourses(ListOptions),
Select(ObjectStruct), ListLectures(ListOptions),
Add(ObjectStruct), SelectCourse(ListOptions),
SelectLecture,
AddCourse(Course),
AddLecture,
Init, Init,
Check(ListOptions),
Meetings(ListOptions),
} }
#[derive(StructOpt)] // #[derive(StructOpt)]
struct ObjectStruct { // struct ObjectStruct {
#[structopt(subcommand)] // #[structopt(subcommand)]
object: Option<Object>, // object: Option<Object>,
} // }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
enum Object { enum Object {
Courses, Courses,
Lectures, Lectures,
} }
fn main() { fn main() -> Result<()> {
let conf = Config{link_path: PathBuf::from("/home/jana/current-course"), files_root: PathBuf::from("/home/jana/uni")}; 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 { Some(subcommand) => match subcommand {
Command::List(object) => list(object.object, conf), Command::ListCourses(options) => {
Command::Add(object) => (), for c in Course::list(&conf, &options)?.iter() {
Command::Select(object) => (), 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::Init => (),
Command::Check(list_options) => Course::check(&conf, &list_options)?,
Command::Meetings(options) => rofi::select_meeting(&conf, &options)?,
}, },
None => println!("No command given"), None => println!("No command given"),
} };
Ok(())
} }
fn list(object: Option<Object>, conf: Config) { fn add(course: Course, conf: &Config, debug: bool) -> Result<()> {
if object.is_some() { let toml = toml::to_string(&course)?;
let object = object.unwrap(); println!("{}", toml);
match object { let target_dir = [
Object::Courses => {list_courses(conf);}, conf.files_root
Object::Lectures => list_lectures(), .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<Course>{ fn list_lectures(conf: &Config, options: ListOptions) -> Result<Vec<PathBuf>> {
let mut courses: Vec<Course> = vec![]; let lectures = util::glob_paths(
let pattern = [conf.files_root.canonicalize().unwrap().to_str().unwrap(), "/**/course.toml"].concat(); if options.all {
let mut handle; &conf.files_root
for entry in glob(pattern.as_str()).unwrap(){ } else {
let path = entry.unwrap(); &conf.link_path
let mut coursestring = String::new(); },
handle = File::open(path).unwrap(); "/**/lecture*",
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() { let lectures = lectures?
println!("Listing 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)
} }

53
src/rofi.rs Normal file
View file

@ -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::<Vec<_>>()
})
.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(())
}

45
src/util.rs Normal file
View file

@ -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>) -> 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::<Local>::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<P: AsRef<Path>, S: AsRef<str>>(path: P, pattern: S) -> Result<Vec<PathBuf>> {
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)
}