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:
parent
526d6a0c7d
commit
06d283b17c
8 changed files with 429 additions and 60 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
.vscode
|
||||
21
Cargo.toml
21
Cargo.toml
|
|
@ -4,10 +4,23 @@ version = "0.1.0"
|
|||
authors = ["Jana Lemke <dev@jana-lemke.de>"]
|
||||
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"
|
||||
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"
|
||||
|
|
|
|||
22
src/config.rs
Normal file
22
src/config.rs
Normal 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
188
src/courses.rs
Normal 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
4
src/lib.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod config;
|
||||
pub mod courses;
|
||||
pub mod rofi;
|
||||
pub mod util;
|
||||
153
src/main.rs
153
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<String>,
|
||||
moodle: Option<String>
|
||||
}
|
||||
#[derive(StructOpt)]
|
||||
struct Cli {
|
||||
#[structopt(short, long)]
|
||||
debug: bool,
|
||||
#[structopt(subcommand)]
|
||||
cmd: Option<Command>,
|
||||
}
|
||||
|
||||
#[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<Object>,
|
||||
}
|
||||
// #[derive(StructOpt)]
|
||||
// struct ObjectStruct {
|
||||
// #[structopt(subcommand)]
|
||||
// object: Option<Object>,
|
||||
// }
|
||||
#[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<Object>, 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<Course>{
|
||||
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);
|
||||
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<Vec<PathBuf>> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
53
src/rofi.rs
Normal file
53
src/rofi.rs
Normal 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
45
src/util.rs
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue