From d030712eeee03ad00f464a09af15229bca05ef50 Mon Sep 17 00:00:00 2001 From: ottjk Date: Sat, 20 Jan 2024 02:01:41 -0500 Subject: initial commit --- .gitignore | 1 + Cargo.lock | 584 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 14 ++ README.md | 3 + src/lecture.rs | 91 +++++++++ src/main.rs | 271 ++++++++++++++++++++++++++ 6 files changed, 964 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/lecture.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..efc45de --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,584 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd2405b3ac1faab2990b74d728624cd9fd115651fcecc7c2d8daf01376275ba" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e54881c004cec7895b0068a0a954cd5d62da01aef83fa35b1e594497bf5445" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cb82d7f531603d2fd1f507441cdd35184fa81beff7bd489570de7f773460bb" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "lectern" +version = "0.1.0" +dependencies = [ + "clap", + "dirs", + "regex", + "serde", + "tinytemplate", + "toml", +] + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "proc-macro2" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5fe890d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "lectern" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.4.16", features = ["derive"] } +dirs = "5.0.1" +regex = "1.10.2" +serde = { version = "1.0.195", features = ["derive"] } +tinytemplate = "1.2.1" +toml = "0.8.8" diff --git a/README.md b/README.md new file mode 100644 index 0000000..3dd5ca8 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# lectern + +My lecture note manager. homework management coming soon tm diff --git a/src/lecture.rs b/src/lecture.rs new file mode 100644 index 0000000..462fd86 --- /dev/null +++ b/src/lecture.rs @@ -0,0 +1,91 @@ +use std::{ + fs, + io, + path::PathBuf, +}; +use tinytemplate::TinyTemplate; +use regex::Regex; + +use crate::{Course, CourseContext, Config}; + +fn init_lecture(course: &Course, config: &Config) { + let template_stream = fs::read(&config.template) + .expect("Couldn't open template."); + let template_string = String::from_utf8_lossy(&template_stream); + + let mut template = TinyTemplate::new(); + template.add_template("main", &template_string) + .expect("Failed initializing template."); + + let context = CourseContext::from(&course, &config); + + let rendered = template.render("main", &context) + .expect("Failed applying template"); + + let lecture_directory = config.root.join(&course.semester).join(&course.name) + .join("lecture"); + fs::write(lecture_directory.join("main.tex"), rendered) + .expect("Failed making main.tex for course."); +} + +fn detect_lessons(path: &PathBuf) -> io::Result> { + let mut lesson_numbers: Vec = Vec::new(); + let dir_iter = fs::read_dir(path)?; + let pattern = Regex::new(r"les(\d+)\.tex").unwrap(); + + for entry in dir_iter { + let entry_path = entry?.path(); + let name = entry_path.file_name().unwrap().to_string_lossy(); + if let Some(caps) = pattern.captures(&name) { + let lesson_number = caps[1].parse::().unwrap(); + lesson_numbers.push(lesson_number); + } + } + + lesson_numbers.sort(); + + return Ok(lesson_numbers); +} + +fn update_main(main_path: &PathBuf, new_lessons: String) -> io::Result<()> { + let main = fs::read(main_path)?; + let pattern = Regex::new(r"% start lessons\n( {4}\\input\{les(\d+)\.tex\}\n)* {4}% end lessons") + .unwrap(); + let fmt_lessons = format!("% start lessons\n{} %end lessons", new_lessons); + let main_string = String::from_utf8_lossy(&main); + let new_main = pattern.replace(&main_string, fmt_lessons); + fs::write(main_path, new_main.as_bytes())?; + + Ok(()) +} + +pub fn new_lesson(course: &Course, config: &Config) -> String { + let lecture_directory = config.root.join(&course.semester).join(&course.name) + .join("lecture"); + + if !lecture_directory.try_exists().unwrap() { + fs::create_dir_all(&lecture_directory).unwrap(); + init_lecture(course, config); + } + + let mut lessons = detect_lessons(&lecture_directory) + .expect("Could not make sense of the lesson situation."); + let new_lesson = lessons.last().unwrap_or(&0).clone() + 1; + + let lesson_file = format!("les{new_lesson}.tex"); + let lesson_path = lecture_directory.join(lesson_file.clone()); + fs::write(&lesson_path, format!("\\lesson{{{new_lesson}}}{{}}\n\n").as_bytes()) + .expect("Unable to write to lesson file."); + + lessons.push(new_lesson); + + let mut lessons_string = String::new(); + + for num in lessons { + lessons_string.push_str(format!(" \\input{{les{num}.tex}}\n").as_str()); + } + + update_main(&lecture_directory.join("main.tex"), lessons_string) + .expect("Unable to update main.tex"); + return lesson_file; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..847d096 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,271 @@ +use std::{ + path::PathBuf, + io::{self, Write}, + process::{Command, Stdio}, + collections::HashMap, + fs, +}; +use lecture::new_lesson; +use serde::{Deserialize, Serialize}; +use clap::{Parser, Subcommand, Args}; + +mod lecture; + +#[derive(Parser)] +struct Cli { + #[command(subcommand)] + command: Commands, + + /// Configuration file location + #[arg(short, long)] + config: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// Initialize a course + Init(InitArgs), + + /// Open notes for a course + Open(OpenArgs), +} + +#[derive(Args)] +struct InitArgs { + /// Course prefix and number + name: String, + + /// Course title + title: String, + + /// Professor's name + prof: String, + + /// Semester id for grouping courses + semester: String, +} + +#[derive(Args)] +struct OpenArgs { + /// Course prefix and number + name: Option, +} + +#[derive(Deserialize, Serialize)] +struct Config { + root: PathBuf, + template: PathBuf, +} + +#[derive(Deserialize, Serialize)] +struct Course { + name: String, + title: String, + prof: String, + semester: String, +} + +#[derive(Serialize)] +struct CourseContext<'a> { + name: &'a String, + title: &'a String, + prof: &'a String, + semester: &'a String, + notebook: String, +} + +impl<'a> CourseContext<'a> { + fn from(course: &'a Course, config: &'a Config) -> CourseContext<'a> { + let notebook = config.root.to_str() + .unwrap().to_owned(); + + CourseContext { + name: &course.name, + title: &course.title, + prof: &course.prof, + semester: &course.semester, + notebook, + } + } +} + +fn resolve_home(path: &mut PathBuf) { + if path.starts_with("~") { + let temp = path.strip_prefix("~").unwrap(); + *path = dirs::home_dir().expect("Could not resolve home directory.") + .join(temp); + } +} + +fn create_config(path: &PathBuf) -> Config { + let mut root: PathBuf; + let mut input = String::new(); + + print!("Where should lecture notes go? "); + io::stdout().flush().unwrap(); + io::stdin() + .read_line(&mut input) + .expect("Failed to read user input."); + + root = PathBuf::from(input.trim()); + resolve_home(&mut root); + + let template = path.join("lecture_template.tex"); + let config = Config { root, template }; + + let toml = toml::to_string(&config) + .expect("Unable to convert config struct to toml string."); + fs::write(path.join("config.toml"), toml) + .expect("Unable to initialize config."); + + return config; +} + +fn read_config(path: &PathBuf) -> Config { + let file_path = path.join("config.toml"); + + if !file_path.try_exists().unwrap() { + return create_config(&path); + } + + let file = fs::read_to_string(file_path) + .expect("Error reading config file."); + return toml::from_str(&file) + .expect("Error parsing config file."); +} + +fn get_courses(path: &PathBuf) -> HashMap { + let file_path = path.join("courses.toml"); + let file = fs::read_to_string(file_path) + .unwrap_or(String::new()); + + return toml::from_str(&file).unwrap(); +} + +fn save_courses(courses: &HashMap, path: &PathBuf) { + let toml = toml::to_string(courses) + .expect("Unable to convert courses to toml string."); + fs::write(path.join("courses.toml"), toml) + .expect("Unable save courses information."); +} + +fn pick_course<'a>(courses: &'a HashMap) -> Result<&'a Course, &'static str> { + let mut options = String::new(); + let courses_seq: Vec<&Course> = courses.values().collect(); + for course in &courses_seq { + options.push_str(&course.name.to_uppercase()); + options.push_str(": "); + options.push_str(&course.title); + options.push_str("\n"); + } + + let course_idx = rofi_picker("Courses", options)?; + + return Ok(&courses_seq[course_idx as usize]); +} + +fn rofi_picker(title: &'static str, input: String) -> Result { + let mut rofi = Command::new("rofi") + .args(["-dmenu", "-i"]) + .args(["-p", title]) + .args(["-format", "i"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn().map_err(|_| "Rofi failed to launch.")?; + + rofi.stdin.as_mut().unwrap().write(input.as_bytes()) + .map_err(|_| "Could not send input to Rofi process.")?; + + let rofi_output = rofi.wait_with_output() + .map_err(|_| "Waiting on Rofi failed.")?; + let choice = String::from_utf8(rofi_output.stdout) + .map_err(|_| "Couldn't make sense of Rofi output.")?; + + if choice.trim().is_empty() { return Err("Rofi was quit without a choice."); } + + let choice_idx = choice.trim().parse::().unwrap(); + + return Ok(choice_idx); +} + +fn launch_tex(directory: &PathBuf, file_name: &String) { + let mut xoppdog = Command::new("xoppdog") + .arg("sit") + .arg(directory.join("figures").as_os_str()) + .stdout(io::stdout()) + .stderr(io::stderr()) + .spawn() + .expect("Failed to start xoppdog."); + + let mut wezterm = Command::new("wezterm") + .args(["start", "--always-new-process", "--cwd"]) + .arg(directory.as_os_str()) + .args(["nvim", file_name]) + .spawn() + .expect("Failed to start neovim terminal."); + + wezterm.wait().expect("Where did the terminal go??"); + xoppdog.kill().expect("Couldn't kill xoppdog."); +} + +fn init_command(args: InitArgs, config: &Config) -> Course { + let course = Course { + name: args.name, + title: args.title, + prof: args.prof, + semester: args.semester + }; + + let lecture_directory = config.root.join(&course.semester).join(&course.name) + .join("lecture"); + fs::create_dir_all(&lecture_directory) + .expect("Failed creating course dir."); + + return course; +} + +fn open_command(args: OpenArgs, courses: &HashMap, config: &Config) -> Result<(), &'static str> { + let course = match &args.name { + Some(name) => courses.get(name).expect("Course not found."), + None => pick_course(courses)?, + }; + + let in_lecture = rofi_picker("Type", String::from("Lecture\nHomework"))? == 0; + let new_file = rofi_picker("Action", String::from("New\nEdit"))? == 0; + + let course_directory = config.root.join(&course.semester).join(&course.name).join("lecture"); + let file_name = match (in_lecture, new_file) { + (true, true) => new_lesson(&course, &config), + (_, _) => String::from("main.tex"), + }; + + launch_tex(&course_directory, &file_name); + + Ok(()) +} + +fn main() { + let cli = Cli::parse(); + + let config_path = dirs::config_dir() + .expect("Could not resolve config directory.") + .join("lectern"); + + if !config_path.try_exists().unwrap() { + fs::create_dir_all(&config_path).unwrap(); + } + + let config = read_config(&config_path); + let mut courses = get_courses(&config_path); + + match cli.command { + Commands::Init(args) => { + let course = init_command(args, &config); + let name = course.name.clone(); + courses.insert(name, course); + save_courses(&courses, &config_path); + }, + Commands::Open(args) => open_command(args, &courses, &config).unwrap(), + + }; +} -- cgit v1.3