Compare commits

...

74 commits

Author SHA1 Message Date
c516684e11 update link to PKGBUILD 2023-10-14 17:09:20 +01:00
152fe36797
fix passing servername 2023-04-04 22:26:42 +02:00
cf90c1f2ed
make writeup script executable 2023-04-04 22:26:30 +02:00
f2212b4f71
make open.py script executable 2023-04-04 22:12:08 +02:00
ce0b8edf47
rework selection of writeup folders: no ignore list 2022-10-16 09:48:18 +02:00
98e661f571
fix wrong fileback 2022-10-16 09:32:53 +02:00
2ebd7f4e8c
update readme for install instructions 2022-08-19 14:57:48 +02:00
ce8dd726e5
add makefile with install/uninstall targets 2022-08-19 14:54:55 +02:00
64564562b8 remove makefile 2022-07-26 23:37:42 +02:00
308a877251 clean up directory structure 2022-07-26 20:51:03 +02:00
f3896398d4 add makefile to install 2022-07-26 20:49:15 +02:00
252e845505 restructure configuration handling 2022-07-26 20:45:14 +02:00
28c841e7f4 add requirements.txt 2022-07-26 20:09:11 +02:00
693c9d37e0 use python configuration file now 2022-07-26 20:02:57 +02:00
231b57df91 update readme 2022-06-25 15:11:34 +02:00
2aa8cd2142 add build folder for notes into config file 2022-05-01 22:49:48 +02:00
6cba512c01 add editor into config 2022-05-01 21:07:15 +02:00
592f436fd4 correctly set parameters for subprocess starting vim 2022-05-01 21:05:05 +02:00
e8c0830f7d correct location for config file. create default config if not existent 2022-05-01 21:02:44 +02:00
db9b26dcbf parse a config file to get terimnal 2022-05-01 20:41:26 +02:00
9631f273e9 launch i3-sensible-terminal instead of termite 2022-05-01 19:25:01 +02:00
79d7e17b5f adjust counter file for build directories 2022-05-01 18:13:10 +02:00
6d7e96f43b update labels script for build folders 2022-04-26 09:02:36 +02:00
fa6bec2835 set to semester 6 2022-04-05 11:48:31 +02:00
0ff7799dee ignore latexpackagesbuild and .git folder 2022-04-05 11:48:18 +02:00
e575d61c2b correctly update lectures in full file on creation of new lecture 2021-10-27 19:36:40 +02:00
Maximilian Keßler
8cf70e724d add option for parsing and opening zoom links 2021-10-11 10:11:10 +02:00
Maximilian Keßler
0d7d1cc2f3 handle error 2021-10-10 21:06:34 +02:00
Maximilian Keßler
a4f973da22 add python file for new writeup. 2021-10-10 20:58:36 +02:00
Maximilian Keßler
6305fced47 adjust fallback.yaml file 2021-10-10 20:46:56 +02:00
Maximilian Keßler
b2fded51e6 add method for making a new exercise sheet write up 2021-10-10 20:45:47 +02:00
Maximilian Keßler
8659206ee6 add rofi opening script for exercises 2021-10-10 20:16:19 +02:00
Maximilian Keßler
18621cf9b8 add exercise write up class 2021-10-10 19:46:47 +02:00
Maximilian Keßler
e5c40a54a0 outline exercises class (new attribute of course) 2021-10-10 19:24:06 +02:00
Maximilian Keßler
0aafc87ed1 fix importing of fallback course info file: use current file path to be independent of execution directory 2021-10-10 15:45:24 +02:00
Maximilian Keßler
65efb9c854 add FileHandle class for lists of pdf files (e.g. exercise sheets) 2021-10-01 19:08:56 +02:00
Maximilian Keßler
f2e0ffc05d rename edit file, add some comments 2021-10-01 19:08:36 +02:00
Maximilian Keßler
8d5af2f340 reformat file 2021-09-28 19:32:19 +02:00
Maximilian Keßler
96a3650663 add script to parse labels from aux files that can be called from within vim 2021-09-19 12:30:16 +02:00
Maximilian Keßler
f82b732d38 fix bug when creating new lectures 2021-09-18 16:25:07 +02:00
Maximilian Keßler
ab9e4e665f add fallback texinputs variable 2021-09-18 14:49:40 +02:00
Maximilian Keßler
ced953bb82 add method to open a terminal in the current course 2021-09-18 14:43:18 +02:00
Maximilian Keßler
6e9f6c0c06 add method to generate lecture environment, adapt all edit methods to new editing 2021-09-18 14:39:05 +02:00
Maximilian Keßler
7728ec97ca take environment directly in edit method 2021-09-18 14:38:20 +02:00
Maximilian Keßler
32487937a9 set texinputs variable if editing master file 2021-09-18 14:17:58 +02:00
Maximilian Keßler
b1504fe2ea save texinputs variable 2021-09-18 14:17:42 +02:00
Maximilian Keßler
19cd1ef603 fix some stuff 2021-09-18 14:17:21 +02:00
Maximilian Keßler
d028139860 provide optional arguments to set the path root and thetexinputs variable in edit command 2021-09-18 14:16:53 +02:00
Maximilian Keßler
49689e4dd0 add shortcut keys for editing master and / or full file 2021-09-18 13:43:19 +02:00
Maximilian Keßler
86bb3babe5 return resultcode of subprocess 2021-09-18 13:43:04 +02:00
Maximilian Keßler
adc8b84f3f switch back to 4th semester 2021-09-17 14:13:30 +02:00
Maximilian Keßler
fc9978b484 ignore lines not fitting the regex when parsing lines from .cnt file 2021-09-17 14:12:46 +02:00
Maximilian Keßler
3407f03091 add config entry for latex aux file extension 2021-09-17 14:11:46 +02:00
Maximilian Keßler
c46cb220ae change entry for from 'url' to 'webpage' 2021-09-17 13:07:20 +02:00
Maximilian Keßler
7f20952fb4 add open.py file to easily open course-related stuff 2021-09-17 13:07:20 +02:00
Maximilian Keßler
9b528a91a2 add Links class to courses to manage course links 2021-09-17 13:07:20 +02:00
Maximilian Keßler
2f646b0780 add some more type hints. add methods to open master or full pdf file 2021-09-17 13:07:20 +02:00
Maximilian Keßler
c1e2fe5500 move scheduler cycle delay into config file. reformat config file 2021-09-17 12:20:08 +02:00
Maximilian Keßler
73cae4f2f1 ignore credentials and token.pickle files 2021-09-17 12:08:49 +02:00
Maximilian Keßler
2f967f57cc make timezone a setting in config.py. Translate countdown file to use english for specifying next lectures etc. 2021-09-17 12:04:33 +02:00
Maximilian Keßler
fc7c77e5d7 optimize imports 2021-09-17 10:49:48 +02:00
Maximilian Keßler
d952a22591 remove deprecated default master file name setting from config file 2021-09-17 10:47:02 +02:00
Maximilian Keßler
78ff6c693a move default new lecture title into config 2021-09-17 10:45:14 +02:00
Maximilian Keßler
10a8a42b84 some more error handling when parsing lectures for their title / date 2021-09-17 10:43:14 +02:00
Maximilian Keßler
602f6e4323 add fallback.yaml to git. Get rid of nasty error handling in Notes class since now we have a fallback file 2021-09-17 10:37:47 +02:00
Maximilian Keßler
5a2fa4494e provide default info file and merge found info files with default info.yaml file 2021-09-17 10:32:28 +02:00
Maximilian Keßler
56637a3b16 add util method to merge dictionaries recursively with priority 2021-09-17 10:22:04 +02:00
Maximilian Keßler
758ace4ffe change \lecture command and corresponding regex 2021-09-17 09:54:31 +02:00
Maximilian Keßler
4e71f12254 handle invalid lecture file format with invalid title 2021-09-17 09:53:26 +02:00
Maximilian Keßler
12b1632f8e add edit file for central place of edit method. add methods to edit master file or full file 2021-09-17 09:53:18 +02:00
Maximilian Keßler
9f498a7c61 add config parameter for indentation 2021-09-17 09:51:45 +02:00
Maximilian Keßler
1b82dfd627 make some files non-executable 2021-09-17 09:50:48 +02:00
Maximilian Keßler
239899b75a set counters in master file with aux file from full file 2021-09-17 09:50:39 +02:00
Maximilian Keßler
b98b23457b add file to parse counters of tex auxiliary file 2021-09-17 09:50:23 +02:00
34 changed files with 901 additions and 481 deletions

3
.gitignore vendored
View file

@ -2,3 +2,6 @@
scripts/__pycache__ scripts/__pycache__
scripts/test.py scripts/test.py
scripts/.idea scripts/.idea
scripts/credentials.json
scripts/token.pickle
__pycache__

View file

@ -23,7 +23,7 @@ SOFTWARE.
MIT License MIT License
Copyright (c) 2021 Maximilian Keßler Copyright (c) 2021,2022 Maximilian Keßler
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

16
Makefile Normal file
View file

@ -0,0 +1,16 @@
PKGNAME=university-setup
PREFIX=mkessler
install:
mkdir -p $(DESTDIR)/opt/$(PREFIX)/${PKGNAME}
mkdir -p $(DESTDIR)/etc/opt/$(PREFIX)/${PKGNAME}
cp src/* $(DESTDIR)/opt/$(PREFIX)/${PKGNAME}
cp config/* $(DESTDIR)/etc/opt/$(PREFIX)/${PKGNAME}
install -Dm644 "LICENSE" $(DESTDIR)/usr/share/licenses/$(PREFIX)/${PKGNAME}/LICENSE
uninstall:
rm -rf $(DESTDIR)/opt/$(PREFIX)/${PKGNAME}
rm -rf $(DESTDIR)/etc/opt/$(PREFIX)/${PKGNAME}
rm -rf $(DESTDIR)/usr/share//licenses/$(PREFIX)/${PKGNAME}

View file

@ -1,9 +1,16 @@
# Fork # Fork
This fork contains my personal customatizations to the setup Gilles uses. The main point is that the `info.yaml` file now contains options for specifying the subfolders for the notes, as well as subfolders for the lectures. This helps me re-use the `current_course` directory for other data as well (e.g. my exercise sheets) while having a single dedicated folder for my lecture notes. It is implemented with an additional `Notes` class that handles this. Possibly, one can extend this to e.g. an `Exercises` class that one can add to each course etc. This fork of [gillescastell/university-setup][setup] contains my personal customatizations to the setup Gilles uses. The main point is that the `info.yaml` file now contains options for specifying the subfolders for the notes, as well as subfolders for the lectures. This helps me re-use the `current_course` directory for other data as well (e.g. my exercise sheets) while having a single dedicated folder for my lecture notes. It is implemented with an additional `Notes` class that handles this. Possibly, one can extend this to e.g. an `Exercises` class that one can add to each course etc.
Additionally, this version features a `.courseignore` file you can place in the `ROOT` folder to ignore some directories for the courses. Additionally, this version features a `.courseignore` file you can place in the `ROOT` folder to ignore some directories for the courses.
##
If you want to install this, run `make install` on your favourite unix operating system.
This installs into `/opt/mkessler/university-setup`.
There is also a [PKGBUILD][pkgbuild] for Arch linux available.
# Managing LaTeX lecture notes # Managing LaTeX lecture notes
This repository complements my [third blog post about my note taking setup](https://castel.dev/post/lecture-notes-3). This repository complements my [third blog post about my note taking setup](https://castel.dev/post/lecture-notes-3).
@ -14,7 +21,7 @@ This repository complements my [third blog post about my note taking setup](http
ROOT ROOT
├── riemann-surfaces ├── riemann-surfaces
│   ├── info.yaml │   ├── info.yaml
│   ├── master.tex │   ├── master.texvllt sollte ich
│   ├── lec_01.tex │   ├── lec_01.tex
│   ├── ... │   ├── ...
│   ├── lec_13.tex │   ├── lec_13.tex
@ -152,3 +159,7 @@ Some utility functions
#### `compile-all-masters.py` #### `compile-all-masters.py`
This script updates the `master.tex` files to include all lectures and compiles them. I use when syncing my notes to the cloud. This way I always have access to my compiles notes on my phone. This script updates the `master.tex` files to include all lectures and compiles them. I use when syncing my notes to the cloud. This way I always have access to my compiles notes on my phone.
[setup]: https://github.com/gillescastel/university-setup
[pkgbuild]: https://git.abstractnonsen.se/arch/university-setup-git

50
config/config.py Normal file
View file

@ -0,0 +1,50 @@
from pathlib import Path
import pytz
# default is 'primary', if you are using a separate calendar for your course schedule,
# your calendarId (which you can find by going to your Google Calendar settings, selecting
# the relevant calendar and scrolling down to Calendar ID) probably looks like
# xxxxxxxxxxxxxxxxxxxxxxxxxg@group.calendar.google.com
# example:
# USERCALENDARID = 'xxxxxxxxxxxxxxxxxxxxxxxxxg@group.calendar.google.com'
USERCALENDARID = 'primary'
CURRENT_COURSE_SYMLINK = Path('~/current_course').expanduser()
CURRENT_COURSE_ROOT = CURRENT_COURSE_SYMLINK.resolve()
CURRENT_COURSE_WATCH_FILE = Path('/tmp/current_course').resolve()
ROOT = Path('~/uni/semester-6').expanduser()
DATE_FORMAT = '%a %d %b %Y'
LOCALE = "de_DE.utf8"
COURSE_IGNORE_FILE = '.courseignore'
COURSE_INFO_FILE_NAME = 'info.yaml'
MAX_LEN = 40
LECTURE_START_MARKER = 'start lectures'
LECTURE_END_MARKER = 'end lectures'
DEFAULT_NEW_LECTURE_HEADER = r'\lecture[]{{{date}}}{{{title}}}'
DEFAULT_NEW_LECTURE_TITLE = 'Untitled'
DEFAULT_LECTURE_SEARCH_REGEX = r'lecture.*({\d*})?{(.*?)}{(.*)}'
DEFAULT_IMPORT_INDENTATION = 4
TIMEZONE = pytz.timezone('CET')
SCHEDULER_DELAY = 60
DEFAULT_LATEX_COUNTER_AUX_FILE_EXTENSION = '.cnt'
TERMINAL = 'i3-sensible-terminal'
EDITOR = 'vim'
NEW_EXERCISE_SHEET_HEADER = '\n'.join([
r"%! TEX root = ./*.tex",
r"\documentclass[{language}]{{mkessler-sheet}}",
"",
r"\usepackage{{babel}}",
r"\usepackage{{mkessler-math}}",
r"\usepackage{{mkessler-enumerate}}",
r"\usepackage{{mkessler-figures}}",
"",
r"\author{{{author}}}",
r"\course{{{course}}}",
r"\sheetnumber{{{number}}}",
"",
r"\begin{{document}}",
r"\maketitle",
"",
"",
r"\end{{document}}"
])

24
config/fallback.yaml Normal file
View file

@ -0,0 +1,24 @@
title: 'Unnamed course'
short: 'unnamed'
language: 'english'
links:
webpage: ''
ecampus: ''
sciebo: ''
basis: ''
github: ''
exercises:
path: 'ub'
name: 'Maximilian Keßler'
sheets: 'sheets'
solutions: 'solutions'
literature:
path: 'doc'
notes:
path: '.'
texinputs: '.'
build_dir: '.'
master_file: 'master.tex'
full_file: 'full.tex'
lectures:
path: '.'

View file

@ -1,265 +0,0 @@
% Some basic packages
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{textcomp}
\usepackage[dutch]{babel}
\usepackage{url}
\usepackage{graphicx}
\usepackage{float}
\usepackage{booktabs}
\usepackage{enumitem}
\pdfminorversion=7
% Don't indent paragraphs, leave some space between them
\usepackage{parskip}
% Hide page number when page is empty
\usepackage{emptypage}
\usepackage{subcaption}
\usepackage{multicol}
\usepackage{xcolor}
% Other font I sometimes use.
% \usepackage{cmbright}
% Math stuff
\usepackage{amsmath, amsfonts, mathtools, amsthm, amssymb}
% Fancy script capitals
\usepackage{mathrsfs}
\usepackage{cancel}
% Bold math
\usepackage{bm}
% Some shortcuts
\newcommand\N{\ensuremath{\mathbb{N}}}
\newcommand\R{\ensuremath{\mathbb{R}}}
\newcommand\Z{\ensuremath{\mathbb{Z}}}
\renewcommand\O{\ensuremath{\emptyset}}
\newcommand\Q{\ensuremath{\mathbb{Q}}}
\newcommand\C{\ensuremath{\mathbb{C}}}
% Easily typeset systems of equations (French package)
\usepackage{systeme}
% Put x \to \infty below \lim
\let\svlim\lim\def\lim{\svlim\limits}
%Make implies and impliedby shorter
\let\implies\Rightarrow
\let\impliedby\Leftarrow
\let\iff\Leftrightarrow
\let\epsilon\varepsilon
% Add \contra symbol to denote contradiction
\usepackage{stmaryrd} % for \lightning
\newcommand\contra{\scalebox{1.5}{$\lightning$}}
% \let\phi\varphi
% Command for short corrections
% Usage: 1+1=\correct{3}{2}
\definecolor{correct}{HTML}{009900}
\newcommand\correct[2]{\ensuremath{\:}{\color{red}{#1}}\ensuremath{\to }{\color{correct}{#2}}\ensuremath{\:}}
\newcommand\green[1]{{\color{correct}{#1}}}
% horizontal rule
\newcommand\hr{
\noindent\rule[0.5ex]{\linewidth}{0.5pt}
}
% hide parts
\newcommand\hide[1]{}
% si unitx
\usepackage{siunitx}
\sisetup{locale = FR}
% Environments
\makeatother
% For box around Definition, Theorem, \ldots
\usepackage{mdframed}
\mdfsetup{skipabove=1em,skipbelow=0em}
\theoremstyle{definition}
\newmdtheoremenv[nobreak=true]{definitie}{Definitie}
\newmdtheoremenv[nobreak=true]{eigenschap}{Eigenschap}
\newmdtheoremenv[nobreak=true]{gevolg}{Gevolg}
\newmdtheoremenv[nobreak=true]{lemma}{Lemma}
\newmdtheoremenv[nobreak=true]{propositie}{Propositie}
\newmdtheoremenv[nobreak=true]{stelling}{Stelling}
\newmdtheoremenv[nobreak=true]{wet}{Wet}
\newmdtheoremenv[nobreak=true]{postulaat}{Postulaat}
\newmdtheoremenv{conclusie}{Conclusie}
\newmdtheoremenv{toemaatje}{Toemaatje}
\newmdtheoremenv{vermoeden}{Vermoeden}
\newtheorem*{herhaling}{Herhaling}
\newtheorem*{intermezzo}{Intermezzo}
\newtheorem*{notatie}{Notatie}
\newtheorem*{observatie}{Observatie}
\newtheorem*{oef}{Oefening}
\newtheorem*{opmerking}{Opmerking}
\newtheorem*{praktisch}{Praktisch}
\newtheorem*{probleem}{Probleem}
\newtheorem*{terminologie}{Terminologie}
\newtheorem*{toepassing}{Toepassing}
\newtheorem*{uovt}{UOVT}
\newtheorem*{vb}{Voorbeeld}
\newtheorem*{vraag}{Vraag}
\newmdtheoremenv[nobreak=true]{definition}{Definition}
\newtheorem*{eg}{Example}
\newtheorem*{notation}{Notation}
\newtheorem*{previouslyseen}{As previously seen}
\newtheorem*{remark}{Remark}
\newtheorem*{note}{Note}
\newtheorem*{problem}{Problem}
\newtheorem*{observe}{Observe}
\newtheorem*{property}{Property}
\newtheorem*{intuition}{Intuition}
\newmdtheoremenv[nobreak=true]{prop}{Proposition}
\newmdtheoremenv[nobreak=true]{theorem}{Theorem}
\newmdtheoremenv[nobreak=true]{corollary}{Corollary}
% End example and intermezzo environments with a small diamond (just like proof
% environments end with a small square)
\usepackage{etoolbox}
\AtEndEnvironment{vb}{\null\hfill$\diamond$}%
\AtEndEnvironment{intermezzo}{\null\hfill$\diamond$}%
% \AtEndEnvironment{opmerking}{\null\hfill$\diamond$}%
% Fix some spacing
% http://tex.stackexchange.com/questions/22119/how-can-i-change-the-spacing-before-theorems-with-amsthm
\makeatletter
\def\thm@space@setup{%
\thm@preskip=\parskip \thm@postskip=0pt
}
% Exercise
% Usage:
% \oefening{5}
% \suboefening{1}
% \suboefening{2}
% \suboefening{3}
% gives
% Oefening 5
% Oefening 5.1
% Oefening 5.2
% Oefening 5.3
\newcommand{\oefening}[1]{%
\def\@oefening{#1}%
\subsection*{Oefening #1}
}
\newcommand{\suboefening}[1]{%
\subsubsection*{Oefening \@oefening.#1}
}
% \lecture starts a new lecture (les in dutch)
%
% Usage:
% \lecture{1}{di 12 feb 2019 16:00}{Inleiding}
%
% This adds a section heading with the number / title of the lecture and a
% margin paragraph with the date.
% I use \dateparts here to hide the year (2019). This way, I can easily parse
% the date of each lecture unambiguously while still having a human-friendly
% short format printed to the pdf.
\usepackage{xifthen}
\def\testdateparts#1{\dateparts#1\relax}
\def\dateparts#1 #2 #3 #4 #5\relax{
\marginpar{\small\textsf{\mbox{#1 #2 #3 #5}}}
}
\def\@lecture{}%
\newcommand{\lecture}[3]{
\ifthenelse{\isempty{#3}}{%
\def\@lecture{Lecture #1}%
}{%
\def\@lecture{Lecture #1: #3}%
}%
\subsection*{\@lecture}
\marginpar{\small\textsf{\mbox{#2}}}
}
% These are the fancy headers
\usepackage{fancyhdr}
\pagestyle{fancy}
% LE: left even
% RO: right odd
% CE, CO: center even, center odd
% My name for when I print my lecture notes to use for an open book exam.
% \fancyhead[LE,RO]{Gilles Castel}
\fancyhead[RO,LE]{\@lecture} % Right odd, Left even
\fancyhead[RE,LO]{} % Right even, Left odd
\fancyfoot[RO,LE]{\thepage} % Right odd, Left even
\fancyfoot[RE,LO]{} % Right even, Left odd
\fancyfoot[C]{\leftmark} % Center
\makeatother
% Todonotes and inline notes in fancy boxes
\usepackage{todonotes}
\usepackage{tcolorbox}
% Make boxes breakable
\tcbuselibrary{breakable}
% Verbetering is correction in Dutch
% Usage:
% \begin{verbetering}
% Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod
% tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At
% vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren,
% no sea takimata sanctus est Lorem ipsum dolor sit amet.
% \end{verbetering}
\newenvironment{verbetering}{\begin{tcolorbox}[
arc=0mm,
colback=white,
colframe=green!60!black,
title=Opmerking,
fonttitle=\sffamily,
breakable
]}{\end{tcolorbox}}
% Noot is note in Dutch. Same as 'verbetering' but color of box is different
\newenvironment{noot}[1]{\begin{tcolorbox}[
arc=0mm,
colback=white,
colframe=white!60!black,
title=#1,
fonttitle=\sffamily,
breakable
]}{\end{tcolorbox}}
% Figure support as explained in my blog post.
\usepackage{import}
\usepackage{xifthen}
\usepackage{pdfpages}
\usepackage{transparent}
\newcommand{\incfig}[1]{%
\def\svgwidth{\columnwidth}
\import{./figures/}{#1.pdf_tex}
}
% Fix some stuff
% %http://tex.stackexchange.com/questions/76273/multiple-pdfs-with-page-group-included-in-a-single-page-warning
\pdfsuppresswarningpagegroup=1
% My name
\author{Gilles Castel}

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
google-api-python-client
google-auth-oauthlib
python-dateutil
pytz
PyYAML

View file

@ -1,10 +0,0 @@
#!/bin/python3
from courses import Courses
for course in Courses():
script = course.notes
lectures = script.lectures
r = lectures.parse_range_string('all')
script.update_lectures_in_master(r)
script.compile_master()

View file

@ -1,23 +0,0 @@
from pathlib import Path
# default is 'primary', if you are using a separate calendar for your course schedule,
# your calendarId (which you can find by going to your Google Calendar settings, selecting
# the relevant calendar and scrolling down to Calendar ID) probably looks like
# xxxxxxxxxxxxxxxxxxxxxxxxxg@group.calendar.google.com
# example:
# USERCALENDARID = 'xxxxxxxxxxxxxxxxxxxxxxxxxg@group.calendar.google.com'
USERCALENDARID = 'primary'
CURRENT_COURSE_SYMLINK = Path('~/current_course').expanduser()
CURRENT_COURSE_ROOT = CURRENT_COURSE_SYMLINK.resolve()
CURRENT_COURSE_WATCH_FILE = Path('/tmp/current_course').resolve()
ROOT = Path('~/Uni/semester-5').expanduser()
DATE_FORMAT = '%a %d %b %Y'
LOCALE = "de_DE.utf8"
COURSE_IGNORE_FILE = '.courseignore'
COURSE_INFO_FILE = 'info.yaml'
DEFAULT_MASTER_FILE_NAME = 'master.tex'
MAX_LEN = 40
LECTURE_START_MARKER = 'start lectures'
LECTURE_END_MARKER = 'end lectures'
DEFAULT_NEW_LECTURE_HEADER = r'\lecture{{{number}}}{{{date}}}{{{title}}}'
DEFAULT_LECTURE_SEARCH_REGEX = r'lecture{(.*?)}{(.*?)}{(.*)}'

View file

@ -1,90 +0,0 @@
#!/usr/bin/python3
import subprocess
from pathlib import Path
from lectures import Lectures, number2filename
from config import DEFAULT_MASTER_FILE_NAME, LECTURE_START_MARKER, LECTURE_END_MARKER
class Notes:
def __init__(self, course):
self.course = course
if 'notes' in course.info:
self.info = course.info['notes']
else:
self.info = []
if 'path' in self.info:
self.root = course.path / self.info['path']
self.root.mkdir(parents=True, exist_ok=True)
else:
self.root = course.path
if 'master_file' in self.info:
self.master_file = self.root / self.info['master_file']
else:
self.master_file = self.root / DEFAULT_MASTER_FILE_NAME
if 'full_file' in self.info:
self.full_file = self.root / self.info['full_file']
else:
self.full_file = None
self._lectures = None
@staticmethod
def get_header_footer(filepath):
part = 0
header = ''
footer = ''
with filepath.open() as f:
for line in f:
# order of if-statements is important here!
if LECTURE_END_MARKER in line:
part = 2
if part == 0:
header += line
if part == 2:
footer += line
if LECTURE_START_MARKER in line:
part = 1
return header, footer
def new_lecture(self):
lec = self.lectures.new_lecture()
if lec.number == 1:
self.update_lectures_in_master([1])
else:
self.update_lectures_in_master([lec.number - 1, lec.number])
self.update_lectures_in_full(self.lectures.parse_range_string('all'))
return lec
def update_lectures_in_file(self, filename, lecture_list):
header, footer = self.get_header_footer(filename)
if self.lectures.root.relative_to(self.root) == Path('.'):
input_command = r'\input{'
else:
input_command = r'\import{' + str(self.lectures.root.relative_to(self.root)) + '/}{'
body = ''.join(
' ' * 4 + input_command + number2filename(number) + '}\n' for number in lecture_list)
filename.write_text(header + body + footer)
def update_lectures_in_master(self, lecture_list):
self.update_lectures_in_file(self.master_file, lecture_list)
def update_lectures_in_full(self, lecture_list):
if self.full_file:
self.update_lectures_in_file(self.full_file, lecture_list)
def compile_master(self):
result = subprocess.run(
['latexmk', '-f', '-interaction=nonstopmode', str(self.master_file)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=str(self.root)
)
return result.returncode
@property
def lectures(self):
if not self._lectures:
self._lectures = Lectures(self)
return self._lectures

View file

@ -1,23 +0,0 @@
from datetime import datetime
from config import MAX_LEN
def beautify(string):
return string.replace('_', ' ').replace('-', ' ').title()
def unbeautify(string):
return string.replace(' ', '-').lower()
def generate_short_title(title):
short_title = title or 'Untitled'
if len(title) >= MAX_LEN:
short_title = title[:MAX_LEN - len(' ... ')] + ' ... '
short_title = short_title.replace('$', '')
return short_title
def get_week(d=datetime.today()):
return (int(d.strftime("%W")) + 52 - 5) % 52

View file

@ -0,0 +1,13 @@
#!/bin/python3
from courses import Courses
for course in Courses():
notes = course.notes
if notes.full_file:
notes.compile_full()
continue
else:
lectures = notes.lectures
r = lectures.parse_range_string('all')
notes.update_lectures_in_master(r)
notes.compile_master()

53
src/config_loader.py Normal file
View file

@ -0,0 +1,53 @@
import shutil
from pathlib import Path
import os
import sys
# We read a configuration file for university setup that is located
# in $XDG_CONFIG_HOME/university-setup/config.cfg
def get_default_file(name: str) -> Path:
# System installation
f1 = Path('/etc/opt/mkessler/university-setup') / name
# no installation, try relative path
f2 = Path(__file__).parent.parent / 'config' / name
if f1.exists():
return f1
if f2.exists():
return f2
raise FileNotFoundError(f'Default file {name} not found, bad installation.')
# Ensure that config.py and fallback.yaml are present
# in the config directory and returns this directory
def get_config_dir() -> Path:
if 'XDG_CONFIG_HOME' in os.environ.keys():
xdg_config_home = Path(os.environ['XDG_CONFIG_HOME']).resolve()
else:
xdg_config_home = Path('~/.config').expanduser().resolve()
config_file = xdg_config_home / 'university-setup' / 'config.py'
fallback_file = xdg_config_home / 'university-setup' / 'fallback.yaml'
config_file.parent.mkdir(exist_ok=True, parents=True)
# Copy defaults if not present already
if not config_file.exists():
shutil.copy(get_default_file('config.py'), config_file)
print(f'Initialized default config file at {str(config_file)}.')
if not fallback_file.exists():
shutil.copy(get_default_file('fallback.yaml'), fallback_file)
print(f'Initialized default fallback file at {str(fallback_file)}.')
return config_file.parent.absolute()
FALLBACK_COURSE_INFO_FILE = get_config_dir() / 'fallback.yaml'
sys.path.append(str(get_config_dir()))
# Note that IDEs will probably complain about this, since they cannot find the module right now
# they might also flag this as an unused import, but the imported config values
# are in turn imported by all the oder scripts
from config import *
# future: potentially check that config in fact defines all symbols that we need

View file

@ -11,7 +11,6 @@ import math
import sched import sched
import datetime import datetime
import time import time
import pytz
from dateutil.parser import parse from dateutil.parser import parse
import http.client as httplib import http.client as httplib
@ -21,14 +20,15 @@ from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request from google.auth.transport.requests import Request
from courses import Courses from courses import Courses
from config import USERCALENDARID from config_loader import USERCALENDARID, TIMEZONE, SCHEDULER_DELAY
courses = Courses() courses = Courses()
def authenticate(): def authenticate():
print('Authenticating') print('Authenticating')
# If modifying these scopes, delete the file token.pickle. # If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'] scopes = ['https://www.googleapis.com/auth/calendar.readonly']
creds = None creds = None
# The file token.pickle stores the user's access and refresh tokens, and is # The file token.pickle stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first # created automatically when the authorization flow completes for the first
@ -44,7 +44,7 @@ def authenticate():
creds.refresh(Request()) creds.refresh(Request())
else: else:
print('Need to allow access') print('Need to allow access')
flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES) flow = InstalledAppFlow.from_client_secrets_file('credentials.json', scopes)
creds = flow.run_local_server(port=0) creds = flow.run_local_server(port=0)
# Save the credentials for the next run # Save the credentials for the next run
with open('token.pickle', 'wb') as token: with open('token.pickle', 'wb') as token:
@ -53,26 +53,31 @@ def authenticate():
service = build('calendar', 'v3', credentials=creds) service = build('calendar', 'v3', credentials=creds)
return service return service
def join(*args): def join(*args):
return ' '.join(str(e) for e in args if e) return ' '.join(str(e) for e in args if e)
def truncate(string, length): def truncate(string, length):
ellipsis = ' ...' ldots = ' ...'
if len(string) < length: if len(string) < length:
return string return string
return string[:length - len(ellipsis)] + ellipsis return string[:length - len(ldots)] + ldots
def summary(text): def summary(text):
return truncate(re.sub(r'X[0-9A-Za-z]+', '', text).strip(), 50) return truncate(re.sub(r'X[0-9A-Za-z]+', '', text).strip(), 50)
def gray(text): def gray(text):
return '%{F#999999}' + text + '%{F-}' return '%{F#999999}' + text + '%{F-}'
def formatdd(begin, end): def formatdd(begin, end):
minutes = math.ceil((end - begin).seconds / 60) minutes = math.ceil((end - begin).seconds / 60)
if minutes == 1: if minutes == 1:
return '1 minuut' return '1 minute'
if minutes < 60: if minutes < 60:
return f'{minutes} min' return f'{minutes} min'
@ -81,9 +86,10 @@ def formatdd(begin, end):
rest_minutes = minutes % 60 rest_minutes = minutes % 60
if hours > 5 or rest_minutes == 0: if hours > 5 or rest_minutes == 0:
return f'{hours} uur' return f'{hours} hours'
return '{}:{:02d} h'.format(hours, rest_minutes)
return '{}:{:02d} uur'.format(hours, rest_minutes)
def location(text): def location(text):
if not text: if not text:
@ -95,15 +101,16 @@ def location(text):
return f'{gray("in")} {match.group(1)}' return f'{gray("in")} {match.group(1)}'
def text(events, now): def text(events, now):
current = next((e for e in events if e['start'] < now and now < e['end']), None) current = next((e for e in events if e['start'] < now < e['end']), None)
if not current: if not current:
nxt = next((e for e in events if now <= e['start']), None) nxt = next((e for e in events if now <= e['start']), None)
if nxt: if nxt:
return join( return join(
summary(nxt['summary']), summary(nxt['summary']),
gray('over'), gray('in'),
formatdd(now, nxt['start']), formatdd(now, nxt['start']),
location(nxt['location']) location(nxt['location'])
) )
@ -111,24 +118,24 @@ def text(events, now):
nxt = next((e for e in events if e['start'] >= current['end']), None) nxt = next((e for e in events if e['start'] >= current['end']), None)
if not nxt: if not nxt:
return join(gray('Einde over'), formatdd(now, current['end']) + '!') return join(gray('Ends in'), formatdd(now, current['end']) + '!')
if current['end'] == nxt['start']: if current['end'] == nxt['start']:
return join( return join(
gray('Einde over'), gray('Ends in'),
formatdd(now, current['end']) + gray('.'), formatdd(now, current['end']) + gray('.'),
gray('Hierna'), gray('Afterwards'),
summary(nxt['summary']), summary(nxt['summary']),
location(nxt['location']) location(nxt['location'])
) )
return join( return join(
gray('Einde over'), gray('Ends in'),
formatdd(now, current['end']) + gray('.'), formatdd(now, current['end']) + gray('.'),
gray('Hierna'), gray('Afterwards'),
summary(nxt['summary']), summary(nxt['summary']),
location(nxt['location']), location(nxt['location']),
gray('na een pauze van'), gray('after a break of'),
formatdd(current['end'], nxt['start']) formatdd(current['end'], nxt['start'])
) )
@ -150,17 +157,12 @@ def main():
scheduler = sched.scheduler(time.time, time.sleep) scheduler = sched.scheduler(time.time, time.sleep)
print('Initializing') print('Initializing')
if 'TZ' in os.environ:
TZ = pytz.timezone(os.environ['TZ'])
else:
print("Warning: TZ environ variable not set")
service = authenticate() service = authenticate()
print('Authenticated') print('Authenticated')
# Call the Calendar API # Call the Calendar API
now = datetime.datetime.now(tz=TZ) now = datetime.datetime.now(tz=TIMEZONE)
morning = now.replace(hour=6, minute=0, microsecond=0) morning = now.replace(hour=6, minute=0, microsecond=0)
evening = now.replace(hour=23, minute=59, microsecond=0) evening = now.replace(hour=23, minute=59, microsecond=0)
@ -187,17 +189,15 @@ def main():
if 'dateTime' in event['start'] if 'dateTime' in event['start']
] ]
events = get_events(userCalendarId) events = get_events(USERCALENDARID)
# events = get_events('primary') + get_events('school-calendar@import.calendar.google.com') # events = get_events('primary') + get_events('school-calendar@import.calendar.google.com')
print('Done') print('Done')
DELAY = 60
def print_message(): def print_message():
now = datetime.datetime.now(tz=TZ) now = datetime.datetime.now(tz=TIMEZONE)
print(text(events, now)) print(text(events, now))
if now < evening: if now < evening:
scheduler.enter(DELAY, 1, print_message) scheduler.enter(SCHEDULER_DELAY, 1, print_message)
for event in events: for event in events:
# absolute entry, priority 1 # absolute entry, priority 1
@ -218,6 +218,7 @@ def wait_for_internet_connection(url, timeout=1):
except: except:
conn.close() conn.close()
if __name__ == '__main__': if __name__ == '__main__':
os.chdir(sys.path[0]) os.chdir(sys.path[0])
print('Waiting for connection') print('Waiting for connection')

45
scripts/courses.py → src/courses.py Executable file → Normal file
View file

@ -1,40 +1,59 @@
#!/usr/bin/python3 #!/usr/bin/python3
from pathlib import Path
import yaml
import warnings import warnings
from lectures import Lectures import yaml
from typing import List
from config_loader import ROOT, CURRENT_COURSE_ROOT, CURRENT_COURSE_SYMLINK, CURRENT_COURSE_WATCH_FILE, COURSE_IGNORE_FILE, \
COURSE_INFO_FILE_NAME, FALLBACK_COURSE_INFO_FILE
from notes import Notes from notes import Notes
from config import ROOT, CURRENT_COURSE_ROOT, CURRENT_COURSE_SYMLINK, CURRENT_COURSE_WATCH_FILE, COURSE_IGNORE_FILE, \ from links import Links
COURSE_INFO_FILE from utils import merge_dictionaries
from exercises import Exercises
class Course: class Course:
def __init__(self, path): def __init__(self, path):
self.path = path self.path = path
self.name = path.stem self.name = path.stem
if (path / COURSE_INFO_FILE).is_file(): if (path / COURSE_INFO_FILE_NAME).is_file():
self.info = yaml.safe_load((path / COURSE_INFO_FILE).open()) self.info = yaml.safe_load((path / COURSE_INFO_FILE_NAME).open())
else: else:
warnings.warn(f"No course info file found in directory '{path.stem}'. Place a {COURSE_INFO_FILE} " warnings.warn(f"No course info file found in directory '{path.stem}'. Place a {COURSE_INFO_FILE_NAME} "
f"file in the directory or add the directory to the course ignore file named" f"file in the directory or add the directory to the course ignore file named"
f" '{COURSE_IGNORE_FILE}' in your root directory ({ROOT})") f" '{COURSE_IGNORE_FILE}' in your root directory ({ROOT})")
self.info = {'title': path.stem, 'short': path.stem} self.info = {'title': str(path.stem) + ' (unnamed course)'}
fallback_file = yaml.safe_load(FALLBACK_COURSE_INFO_FILE.open())
self.info = merge_dictionaries(self.info, fallback_file)
self._notes = None self._notes = None
self._links = None
self._exercises = None
@property @property
def notes(self): def links(self) -> Links:
if not self._links:
self._links = Links(self)
return self._links
@property
def notes(self) -> Notes:
if not self._notes: if not self._notes:
self._notes = Notes(self) self._notes = Notes(self)
return self._notes return self._notes
@property
def exercises(self) -> Exercises:
if not self._exercises:
self._exercises = Exercises(self)
return self._exercises
def __eq__(self, other): def __eq__(self, other):
if other is None: if other is None:
return False return False
return self.path == other.path return self.path == other.path
def ignored_courses(): def ignored_courses() -> List[Course]:
if (ROOT / COURSE_IGNORE_FILE).is_file(): if (ROOT / COURSE_IGNORE_FILE).is_file():
with open(ROOT / COURSE_IGNORE_FILE) as ignore: with open(ROOT / COURSE_IGNORE_FILE) as ignore:
lines = ignore.readlines() lines = ignore.readlines()
@ -45,7 +64,7 @@ def ignored_courses():
return [] return []
def read_files(): def read_files() -> List[Course]:
course_directories = [x for x in ROOT.iterdir() if x.is_dir() and x not in ignored_courses()] course_directories = [x for x in ROOT.iterdir() if x.is_dir() and x not in ignored_courses()]
_courses = [Course(path) for path in course_directories] _courses = [Course(path) for path in course_directories]
return sorted(_courses, key=lambda c: c.name) return sorted(_courses, key=lambda c: c.name)
@ -56,7 +75,7 @@ class Courses(list):
list.__init__(self, read_files()) list.__init__(self, read_files())
@property @property
def current(self): def current(self) -> Course:
return Course(CURRENT_COURSE_ROOT.resolve()) return Course(CURRENT_COURSE_ROOT.resolve())
@current.setter @current.setter

108
src/exercises.py Normal file
View file

@ -0,0 +1,108 @@
from file_list import Files, FileHandle, FileType
from pathlib import Path
from typing import Dict
from utils import normalize
from config_loader import NEW_EXERCISE_SHEET_HEADER
class ExerciseWriteUp(FileHandle):
def __init__(self, root_dir: Path, course):
self.root_dir = root_dir
self.course = course
try:
tex_file = next(self.root_dir.rglob('*.tex'))
except StopIteration:
print("No valid '.tex' file found in directory {}, can't instantiate write up here".format(root_dir))
# TODO: raise proper error
exit(1)
FileHandle.__init__(self, tex_file, FileType.tex)
class Exercise:
def __init__(self, course, number: int):
self.course = course
self.number = number
self._writeup = None
self._problem = None
self._solution = None
@property
def writeup(self):
if not self._writeup:
self._writeup = next((w for w in self.course.writeups if w.number == self.number), None)
return self._writeup
@property
def problem(self):
if not self._problem:
self._problem = next((p for p in self.course.problems if p.number == self.number), None)
return self._problem
@property
def solution(self):
if not self._solution:
self._solution = next((s for s in self.course.solutions if s.number == self.number), None)
return self._solution
class Exercises(list):
def __init__(self, course):
self.course = course
self.info: Dict = course.info['exercises']
self.root: Path = course.path / self.info['path']
self.sheet_root = self.root / self.info['sheets'].strip()
self.solutions_root = self.root / self.info['solutions'].strip()
self.root.mkdir(parents=True, exist_ok=True)
self.sheet_root.mkdir(parents=True, exist_ok=True)
self.solutions_root.mkdir(parents=True, exist_ok=True)
self._solutions = None
self._writeups = None
self._sheets = Files(self.sheet_root)
list.__init__(self, (Exercise(self.course, num) for num in map(lambda s: s.number, self._sheets)))
@property
def sheets(self):
return self._sheets
@property
def solutions(self):
if not self._solutions:
self._solutions = Files(self.solutions_root)
return self._solutions
@property
def writeups(self):
if not self._writeups:
dirs = [d for d in self.root.glob('ub*') if d.is_dir()]
self._writeups = sorted((ExerciseWriteUp(d, self.course) for d in dirs), key=lambda e: e.number)
return self._writeups
@staticmethod
def __generate_name(name: str):
return normalize(name.split(' ')[-1])
def __generate_names(self):
names = self.info['name']
if type(names) == str:
return self.__generate_name(names)
elif type(names) == list:
return '_'.join(map(self.__generate_name, names))
def new_writeup(self):
try:
new_num = max(self.writeups, key=lambda w: w.number).number + 1
except ValueError:
new_num = 1
new_dir = self.root / 'ub{:02d}'.format(new_num)
new_dir.mkdir(parents=True, exist_ok=False)
new_file = new_dir / '{names}_{course}_sheet_{num}.tex'.format(
names=self.__generate_names(),
course=normalize(self.course.info['short']),
num=new_num
)
new_file.write_text(NEW_EXERCISE_SHEET_HEADER.format(
language='ngerman' if self.course.info['language'] == 'german' else 'english',
author=self.info['name'] if type(self.info['name']) == str else ', '.join(self.info['name']),
course=self.course.info['title'],
number=new_num
))
return ExerciseWriteUp(new_dir, self.course)

56
src/file_list.py Normal file
View file

@ -0,0 +1,56 @@
#!/usr/bin/python3
import re
import subprocess
import warnings
from pathlib import Path
from window_subprocess import open_pdf, edit
from enum import Enum
class FileType(Enum):
pdf = 'pdf'
tex = 'tex'
class FileHandle:
def __init__(self, file_path: Path, file_type: FileType = FileType.pdf):
self.path = file_path
self.file_type = file_type
match = re.match(r'[\D]*(\d+)[\D]*\.' + file_type.value, file_path.name)
if match is None:
warnings.warn(f'Invalid format in file {str(file_path)}: Could not parse number.')
self.number = -1
else:
self.number = int(match.group(1))
def open(self):
if self.file_type == FileType.pdf:
open_pdf(self.path)
return 0
elif self.file_type == FileType.tex:
edit(self.path)
return 0
return 1
def edit(self):
if self.file_type == FileType.tex:
edit(self.path)
return 0
else:
return 1
class Files(list):
def __init__(self, root_path: Path, pattern: str = "*"):
self.root: Path = root_path
list.__init__(self, self.read_files(pattern))
def read_files(self, pattern):
files = self.root.glob(pattern)
return sorted((FileHandle(f) for f in files), key=lambda f: f.number)
def unite_files(self, name):
result = subprocess.run(
['pdfunite'] + list(map(lambda f: str(f.path), self)) + [str(self.root / name)]
)
print(result.stdout)

69
src/labels.py Normal file
View file

@ -0,0 +1,69 @@
#!/usr/bin/python3
from pathlib import Path
from rofi import rofi
import sys
def fancy(label, number):
return f"{label} ({number})"
def remove_duplicates(ls):
new_list = []
[new_list.append(elem) for elem in ls if not elem in new_list]
return new_list
def get_labels(path):
file = open(path, mode='r', encoding='utf-8-sig')
lines = file.readlines()
file.close()
lines = [line for line in lines if
'\\newlabel' in line and '{' in line and not '@' in line and not 'gdef' in line and not 'LastPage' in line]
labels = [line.split('{')[1].split('}')[0] for line in lines]
numbers = [line.split('{')[3].split('}')[0] for line in lines]
options = [fancy(label, number) for (label, number) in zip(labels, numbers)]
return labels, options
def get_all_labels(pathlist):
labels = []
options = []
for path in pathlist:
try:
nlabels, noptions = get_labels(path)
except:
continue
labels += nlabels
options += noptions
unique = remove_duplicates(zip(labels, options))
return [a for (a, b) in unique], [b for (a, b) in unique]
def main(args):
arglist = []
if len(args) > 1:
path = Path(args[1])
arglist = list(path.glob('*.aux')) + list(path.glob('build/*.aux'))
else:
arglist = ['/home/maximilian/current_course/full.aux']
labels, options = get_all_labels(arglist)
key, index, selected = rofi('Select label', options, [
'-lines', min(40, max(len(options), 5)), '-width', '1700'
])
if index >= 0:
command = labels[index]
else:
command = selected
return command.strip()
if __name__ == '__main__':
selected_label = main(sys.argv)
print(selected_label)

36
scripts/lectures.py → src/lectures.py Executable file → Normal file
View file

@ -2,10 +2,12 @@
import locale import locale
import os import os
import re import re
import subprocess import warnings
from datetime import datetime from datetime import datetime
from config import DATE_FORMAT, LOCALE, DEFAULT_NEW_LECTURE_HEADER, DEFAULT_LECTURE_SEARCH_REGEX from config import DATE_FORMAT, LOCALE, DEFAULT_NEW_LECTURE_HEADER, DEFAULT_LECTURE_SEARCH_REGEX, \
DEFAULT_NEW_LECTURE_TITLE
from window_subprocess import edit
from utils import get_week from utils import get_week
# TODO # TODO
@ -21,34 +23,39 @@ def filename2number(s):
class Lecture: class Lecture:
def __init__(self, file_path, course): def __init__(self, file_path, notes):
with file_path.open() as f: with file_path.open() as f:
for line in f: for line in f:
lecture_match = re.search(DEFAULT_LECTURE_SEARCH_REGEX, line) lecture_match = re.search(DEFAULT_LECTURE_SEARCH_REGEX, line)
if lecture_match: if lecture_match:
break break
# number = int(lecture_match.group(1)) if lecture_match:
date_str = lecture_match.group(2) date_str = lecture_match.group(2)
try:
date = datetime.strptime(date_str, DATE_FORMAT) date = datetime.strptime(date_str, DATE_FORMAT)
except ValueError:
warnings.warn(f"Invalid date format found in lecture file {file_path}. Specify time in format"
f"'{DATE_FORMAT}' that you set in the config.py file.")
date = datetime.min
week = get_week(date) week = get_week(date)
title = lecture_match.group(3) title = lecture_match.group(3)
else:
date = datetime.min
week = get_week(date)
title = 'Error while parsing lecture file'
self.file_path = file_path self.file_path = file_path
self.date = date self.date = date
self.week = week self.week = week
self.number = filename2number(file_path.stem) self.number = filename2number(file_path.stem)
self.title = title self.title = title
self.course = course self.notes = notes
def edit(self): def edit(self):
subprocess.Popen([ edit(self.file_path, rootpath=self.notes.root, env=self.notes.environment())
"x-terminal-emulator",
"-e", "zsh", "-i", "-c",
f"\\vim --servername kulak --remote-silent {str(self.file_path)}"
])
def __str__(self): def __str__(self):
return f'<Lecture {self.course.info["short"]} {self.number} "{self.title}">' return f'<Lecture {self.course.info["short"]} {self.number} "{self.title}">'
@ -71,7 +78,7 @@ class Lectures(list):
def read_files(self): def read_files(self):
files = self.root.glob('lec_*.tex') files = self.root.glob('lec_*.tex')
return sorted((Lecture(f, self.course) for f in files), key=lambda l: l.number) return sorted((Lecture(f, self.notes) for f in files), key=lambda l: l.number)
def parse_lecture_spec(self, string): def parse_lecture_spec(self, string):
if len(self) == 0: if len(self) == 0:
@ -111,12 +118,13 @@ class Lectures(list):
date = today.strftime(DATE_FORMAT) date = today.strftime(DATE_FORMAT)
vimtex_root_str = f"%! TEX root = {str(os.path.relpath(self.notes.master_file, self.root))}\n" vimtex_root_str = f"%! TEX root = {str(os.path.relpath(self.notes.master_file, self.root))}\n"
header_str = DEFAULT_NEW_LECTURE_HEADER.format(number=new_lecture_number, date=date, title='Untitled') header_str = DEFAULT_NEW_LECTURE_HEADER.format(
number=new_lecture_number, date=date, title=DEFAULT_NEW_LECTURE_TITLE)
new_lecture_path.touch() new_lecture_path.touch()
new_lecture_path.write_text(vimtex_root_str + header_str) new_lecture_path.write_text(vimtex_root_str + header_str)
self.read_files() self.read_files()
lec = Lecture(new_lecture_path, self.course) lec = Lecture(new_lecture_path, self.notes)
return lec return lec

22
src/links.py Normal file
View file

@ -0,0 +1,22 @@
import subprocess
from typing import Dict, List
class Links:
def __init__(self, course):
self.course = course # A course
self.info: Dict = course.info['links']
def open(self, key: str):
self.open_link_in_browser(self.info[key])
def available(self) -> List[str]:
return [key for key in self.info.keys() if self.info[key] != '']
@staticmethod
def open_link_in_browser(url):
result = subprocess.run(
['qutebrowser', str(url)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)

6
src/new-writeup.py Executable file
View file

@ -0,0 +1,6 @@
#! /usr/bin/python3
from courses import Courses
course = Courses().current
writeup = course.exercises.new_writeup()
writeup.edit()

147
src/notes.py Normal file
View file

@ -0,0 +1,147 @@
#!/usr/bin/python3
import os
import subprocess
from pathlib import Path
from typing import Dict
from config_loader import LECTURE_START_MARKER, LECTURE_END_MARKER, DEFAULT_IMPORT_INDENTATION, \
DEFAULT_LATEX_COUNTER_AUX_FILE_EXTENSION
from window_subprocess import edit
from lectures import Lectures, number2filename
from parse_counters import parse_counters, dict2setcounters
class Notes:
def __init__(self, course):
self.course = course # A course
self.info: Dict = course.info['notes']
self.root: Path = course.path / self.info['path']
self.root.mkdir(parents=True, exist_ok=True)
self.master_file: Path = self.root / self.info['master_file']
self.full_file: Path = self.root / self.info['full_file']
self.texinputs: Path = self.root / self.info['texinputs']
self._lectures = None
self.build_dir: Path = self.root / self.info['build_dir']
@staticmethod
def get_header_footer(filepath):
part = 0
header = ''
footer = ''
with filepath.open() as f:
for line in f:
# order of if-statements is important here!
if LECTURE_END_MARKER in line:
part = 2
if part == 0:
header += line
if part == 2:
footer += line
if LECTURE_START_MARKER in line:
part = 1
return header, footer
def new_lecture(self):
lec = self.lectures.new_lecture()
if lec.number == 1:
self.update_lectures_in_master([1])
else:
self.update_lectures_in_master([lec.number - 1, lec.number])
self._lectures = None # This causes the lectures to be re-computed
self.update_lectures_in_full(self.lectures.parse_range_string('all'))
return lec
def input_lecture_command(self, num: int):
if self.lectures.root.relative_to(self.root) == Path('.'):
input_command = r'\input{'
else:
input_command = r'\import{' + str(self.lectures.root.relative_to(self.root)) + '/}{'
return ' ' * DEFAULT_IMPORT_INDENTATION + input_command + number2filename(num) + '}\n'
def set_counters(self, lecture_list, lec, setcounters=False):
if not setcounters:
return ''
if lec - 1 not in lecture_list and self.full_file:
cnt_file = self.full_file.with_suffix(DEFAULT_LATEX_COUNTER_AUX_FILE_EXTENSION)
if not cnt_file.exists():
cnt_file = self.full_file.parent / 'build' / cnt_file.name
return dict2setcounters(parse_counters(
cnt_file,
{'lecture': lec}
))
return ''
def update_lectures_in_file(self, filename, lecture_list, setcounters=False):
header, footer = self.get_header_footer(filename)
body = ''.join([self.set_counters(lecture_list, num, setcounters) + self.input_lecture_command(num)
for num in lecture_list])
filename.write_text(header + body + footer)
def update_lectures_in_master(self, lecture_list):
self.update_lectures_in_file(self.master_file, lecture_list, True)
def update_lectures_in_full(self, lecture_list):
if self.full_file:
self.update_lectures_in_file(self.full_file, lecture_list)
def edit_master(self):
edit(self.master_file, rootpath=self.root, env=self.environment())
def edit_full(self):
edit(self.full_file, rootpath=self.root, env=self.environment())
def open_master(self):
result = subprocess.run(
['zathura', str(self.master_file.with_suffix('.pdf'))],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
return result.returncode
def open_full(self):
result = subprocess.run(
['zathura', str(self.build_dir / self.full_file.with_suffix('.pdf').name)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
return result.returncode
def open_terminal(self):
result = subprocess.Popen(
['i3-sensible-terminal'], env=self.environment(), cwd=self.root
)
def compile_master(self):
result = subprocess.run(
['latexmk', '-f', '-interaction=nonstopmode', '-dvi-', '-pdf', str(self.master_file)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=str(self.root),
env=self.environment()
)
return result.returncode
def compile_full(self):
if not self.full_file:
return 0
result = subprocess.run(
['latexmk', '-f', '-interaction=nonstopmode', '-dvi-', '-pdf', str(self.full_file)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=str(self.root),
env=self.environment()
)
return result.returncode
def environment(self):
env = os.environ
env["TEXINPUTS"] = str(self.texinputs) + '//:'
return env
@property
def lectures(self):
if not self._lectures:
self._lectures = Lectures(self)
return self._lectures

37
src/open.py Executable file
View file

@ -0,0 +1,37 @@
#!/usr/bin/python3
from courses import Courses
import sys
def open_spec(specification: str):
current = Courses().current
switcher = {
'full': current.notes.open_full,
'master': current.notes.open_master,
'terminal': current.notes.open_terminal
}
if specification in switcher.keys():
return switcher[specification]()
link_type = {
'webpage': 'webpage',
'w': 'webpage',
'url': 'webpage',
'u': 'webpage',
'ecampus': 'ecampus',
'e': 'ecampus',
'sciebo': 'sciebo',
's': 'sciebo',
'basis': 'basis',
'b': 'basis',
'github': 'github',
'g': 'github'
}
return current.links.open(link_type[specification])
if __name__ == "__main__":
for arg in sys.argv[1:]:
open_spec(arg)

28
src/parse_counters.py Executable file
View file

@ -0,0 +1,28 @@
#!/usr/bin/python3
import re
from pathlib import Path
from typing import Dict
from config_loader import DEFAULT_IMPORT_INDENTATION
def parse_counters(filepath: Path, break_point: Dict) -> Dict:
if not filepath.is_file():
return {}
counters: Dict = {}
with open(filepath) as f:
for line in f:
match = re.search(r"(.*): (\d*\.)*?(\d+)", line)
if not match:
continue
counter, _, num = match.groups()
num = int(num)
if counter in break_point and num >= break_point[counter]:
return counters
counters[counter] = num
return counters
def dict2setcounters(counters: Dict):
counters_as_list = [(counter, counters[counter]) for counter in counters.keys()]
return ''.join(' ' * DEFAULT_IMPORT_INDENTATION + r'\setcounter{' + counter + '}{' + str(num) + '}\n'
for (counter, num) in counters_as_list)

48
src/rofi-exercises.py Executable file
View file

@ -0,0 +1,48 @@
#!/usr/bin/python3
from courses import Courses
from exercises import Exercises
from rofi import rofi
import sys
def rofi_pick_exercise(spec: str = 'writeup'):
exercises = Courses().current.exercises
switcher = {
'writeup': Exercises.writeups,
'solution': Exercises.solutions,
'sheet': Exercises.sheets
}
sorted_ex = sorted(switcher[spec].fget(exercises), key=lambda e: -e.number)
options = [
"{number: >2}".format(
number=ex.number
)
for ex in sorted_ex
]
switcher = {
'writeup': 'writeup',
'solution': 'solution',
'sheet': 'sheet'
}
key, index, selected = rofi('Select number of exercise {spec}'.format(spec=switcher[spec]), options, [
'-lines', max(5, min(15, len(sorted_ex))),
'-markup-rows',
'-kb-custom-1', 'Alt+n'
])
if key == 0:
sorted_ex[index].open()
elif key == 1 and spec == 'writeup':
exercises.new_writeup()
if __name__ == '__main__':
if not len(sys.argv) == 2:
print('Please specify exactly one of "writeup", "solution" and "sheet"')
exit(1)
rofi_pick_exercise(sys.argv[1])
exit(0)

View file

@ -2,10 +2,10 @@
from courses import Courses from courses import Courses
from rofi import rofi from rofi import rofi
from utils import generate_short_title from utils import generate_short_title
from config import MAX_LEN from config_loader import MAX_LEN
script = Courses().current.notes notes = Courses().current.notes
lectures = script.lectures lectures = notes.lectures
sorted_lectures = sorted(lectures, key=lambda l: -l.number) sorted_lectures = sorted(lectures, key=lambda l: -l.number)
@ -24,11 +24,17 @@ key, index, selected = rofi('Select lecture', options, [
'-lines', 5, '-lines', 5,
'-markup-rows', '-markup-rows',
'-kb-row-down', 'Down', '-kb-row-down', 'Down',
'-kb-custom-1', 'Ctrl+n' '-kb-custom-1', 'Alt+n',
'-kb-custom-2', 'Alt+m',
'-kb-custom-3', 'Alt+s'
]) ])
if key == 0: if key == 0:
sorted_lectures[index].edit() sorted_lectures[index].edit()
elif key == 1: elif key == 1:
new_lecture = script.new_lecture() new_lecture = notes.new_lecture()
new_lecture.edit() new_lecture.edit()
elif key == 2:
notes.edit_master()
elif key == 3:
notes.edit_full()

View file

@ -1,5 +1,6 @@
import subprocess import subprocess
def rofi(prompt, options, rofi_args=[], fuzzy=True): def rofi(prompt, options, rofi_args=[], fuzzy=True):
optionstr = '\n'.join(option.replace('\n', ' ') for option in options) optionstr = '\n'.join(option.replace('\n', ' ') for option in options)
args = ['rofi', '-sort', '-no-levenshtein-sort'] args = ['rofi', '-sort', '-no-levenshtein-sort']
@ -9,7 +10,6 @@ def rofi(prompt, options, rofi_args=[], fuzzy=True):
args += rofi_args args += rofi_args
args = [str(arg) for arg in args] args = [str(arg) for arg in args]
result = subprocess.run(args, input=optionstr, stdout=subprocess.PIPE, universal_newlines=True) result = subprocess.run(args, input=optionstr, stdout=subprocess.PIPE, universal_newlines=True)
returncode = result.returncode returncode = result.returncode
stdout = result.stdout.strip() stdout = result.stdout.strip()
@ -20,11 +20,18 @@ def rofi(prompt, options, rofi_args=[], fuzzy=True):
except ValueError: except ValueError:
index = -1 index = -1
# We handle the return code from rofi here:
# 0 of course means successful, we pass this on
# 1 means that the user exited the prompt without specifying an option
# returns codes >=10 are custom return codes specified with '-kb-custom-<n> <keybind>' options
# that are passed to rofi. We subtract 9 from them to pass '<n>' to the caller
if returncode == 0: if returncode == 0:
key = 0 key = 0
elif returncode == 1: elif returncode == 1:
key = -1 key = -1
elif returncode > 9: elif returncode > 9:
key = returncode - 9 key = returncode - 9
else: # This case should never be reached
key = -2
return key, index, selected return key, index, selected

52
src/utils.py Normal file
View file

@ -0,0 +1,52 @@
import re
from datetime import datetime
from typing import Dict
import warnings
from config_loader import MAX_LEN
def beautify(string):
return string.replace('_', ' ').replace('-', ' ').title()
def unbeautify(string):
return string.replace(' ', '-').lower()
def normalize(string):
return string.lower().replace('ä', 'ae').replace('ö', 'oe').replace('ü', 'ue').replace('ß', 'ss')
def generate_short_title(title):
short_title = title or 'Untitled'
if len(title) >= MAX_LEN:
short_title = title[:MAX_LEN - len(' ... ')] + ' ... '
short_title = short_title.replace('$', '')
return short_title
def get_week(d=datetime.today()):
return (int(d.strftime("%W")) + 52 - 5) % 52
def parse_zoom_link(browser_join_link: str):
match = re.search(r'(?:/j/|&confno=)(?P<confno>\d*)(?:&zc=0)?(?:\?|&)pwd=(?P<pwd>.*?)(?:#success|$)', browser_join_link)
if not match:
return None
else:
return match.groupdict()['confno'], match.groupdict()['pwd']
def merge_dictionaries(main: Dict, fallback: Dict):
merged = main
for key in fallback.keys():
if key not in main.keys():
merged[key] = fallback[key]
elif type(fallback[key]) == dict:
if not type(merged[key]) == dict:
warnings.warn(
f"Main dictionary has invalid format. Replacing entry with key {key} with fallback value.")
merged[key] = fallback[key]
merged[key] = merge_dictionaries(merged[key], fallback[key])
return merged

42
src/window_subprocess.py Normal file
View file

@ -0,0 +1,42 @@
#! /usr/bin/python3
import subprocess
from pathlib import Path
import os
from config_loader import TERMINAL, EDITOR
def edit(filepath: Path, rootpath: Path = None, env=os.environ, servername='tex lecture'):
if not rootpath:
rootpath = filepath.root
subprocess.Popen([
TERMINAL,
"-e",
EDITOR,
f"--servername",
f"{servername}",
f"--remote-silent",
f"{str(filepath)}"
], env=env, cwd=str(rootpath))
def open_pdf(filepath: Path):
result = subprocess.run(
['zathura', str(filepath)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
return result.returncode
def open_zoom(confno: int, pwd_hash: str = None):
subprocess.Popen(
["zoom",
"zoomtg://zoom.us/join?browser=chrom&confno={confno}&zc=0{pwd}".format(
confno=confno,
pwd='&pwd={}'.format(pwd_hash) if pwd_hash is not None else '')
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)