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
33 changed files with 881 additions and 461 deletions

3
.gitignore vendored
View file

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

View file

@ -23,7 +23,7 @@ SOFTWARE.
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
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
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.
##
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
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
├── riemann-surfaces
│   ├── info.yaml
│   ├── master.tex
│   ├── master.texvllt sollte ich
│   ├── lec_01.tex
│   ├── ...
│   ├── lec_13.tex
@ -152,3 +159,7 @@ Some utility functions
#### `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.
[setup]: https://github.com/gillescastel/university-setup
[pkgbuild]: https://git.abstractnonsen.se/arch/university-setup-git

View file

@ -1,4 +1,5 @@
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
@ -10,14 +11,40 @@ 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()
ROOT = Path('~/uni/semester-6').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'
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,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 datetime
import time
import pytz
from dateutil.parser import parse
import http.client as httplib
@ -21,14 +20,15 @@ from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from courses import Courses
from config import USERCALENDARID
from config_loader import USERCALENDARID, TIMEZONE, SCHEDULER_DELAY
courses = Courses()
def authenticate():
print('Authenticating')
# 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
# The file token.pickle stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
@ -44,7 +44,7 @@ def authenticate():
creds.refresh(Request())
else:
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)
# Save the credentials for the next run
with open('token.pickle', 'wb') as token:
@ -53,37 +53,43 @@ def authenticate():
service = build('calendar', 'v3', credentials=creds)
return service
def join(*args):
return ' '.join(str(e) for e in args if e)
def truncate(string, length):
ellipsis = ' ...'
ldots = ' ...'
if len(string) < length:
return string
return string[:length - len(ellipsis)] + ellipsis
return string[:length - len(ldots)] + ldots
def summary(text):
return truncate(re.sub(r'X[0-9A-Za-z]+', '', text).strip(), 50)
def gray(text):
return '%{F#999999}' + text + '%{F-}'
def formatdd(begin, end):
minutes = math.ceil((end - begin).seconds / 60)
if minutes == 1:
return '1 minuut'
return '1 minute'
if minutes < 60:
return f'{minutes} min'
hours = math.floor(minutes/60)
hours = math.floor(minutes / 60)
rest_minutes = minutes % 60
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):
if not text:
@ -95,15 +101,16 @@ def location(text):
return f'{gray("in")} {match.group(1)}'
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:
nxt = next((e for e in events if now <= e['start']), None)
if nxt:
return join(
summary(nxt['summary']),
gray('over'),
gray('in'),
formatdd(now, nxt['start']),
location(nxt['location'])
)
@ -111,24 +118,24 @@ def text(events, now):
nxt = next((e for e in events if e['start'] >= current['end']), None)
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']:
return join(
gray('Einde over'),
gray('Ends in'),
formatdd(now, current['end']) + gray('.'),
gray('Hierna'),
gray('Afterwards'),
summary(nxt['summary']),
location(nxt['location'])
)
return join(
gray('Einde over'),
gray('Ends in'),
formatdd(now, current['end']) + gray('.'),
gray('Hierna'),
gray('Afterwards'),
summary(nxt['summary']),
location(nxt['location']),
gray('na een pauze van'),
gray('after a break of'),
formatdd(current['end'], nxt['start'])
)
@ -150,20 +157,15 @@ def main():
scheduler = sched.scheduler(time.time, time.sleep)
print('Initializing')
if 'TZ' in os.environ:
TZ = pytz.timezone(os.environ['TZ'])
else:
print("Warning: TZ environ variable not set")
service = authenticate()
print('Authenticated')
# 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)
evening= now.replace(hour=23, minute=59, microsecond=0)
evening = now.replace(hour=23, minute=59, microsecond=0)
print('Searching for events')
@ -187,21 +189,19 @@ def main():
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')
print('Done')
DELAY = 60
def print_message():
now = datetime.datetime.now(tz=TZ)
now = datetime.datetime.now(tz=TIMEZONE)
print(text(events, now))
if now < evening:
scheduler.enter(DELAY, 1, print_message)
scheduler.enter(SCHEDULER_DELAY, 1, print_message)
for event in events:
# absolute entry, priority 1
scheduler.enterabs(event['start'].timestamp(), 1, activate_course, argument=(event, ))
scheduler.enterabs(event['start'].timestamp(), 1, activate_course, argument=(event,))
# Immediate, priority 1
scheduler.enter(0, 1, print_message)
@ -218,6 +218,7 @@ def wait_for_internet_connection(url, timeout=1):
except:
conn.close()
if __name__ == '__main__':
os.chdir(sys.path[0])
print('Waiting for connection')

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

@ -1,40 +1,59 @@
#!/usr/bin/python3
from pathlib import Path
import yaml
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 config import ROOT, CURRENT_COURSE_ROOT, CURRENT_COURSE_SYMLINK, CURRENT_COURSE_WATCH_FILE, COURSE_IGNORE_FILE, \
COURSE_INFO_FILE
from links import Links
from utils import merge_dictionaries
from exercises import Exercises
class Course:
def __init__(self, path):
self.path = path
self.name = path.stem
if (path / COURSE_INFO_FILE).is_file():
self.info = yaml.safe_load((path / COURSE_INFO_FILE).open())
if (path / COURSE_INFO_FILE_NAME).is_file():
self.info = yaml.safe_load((path / COURSE_INFO_FILE_NAME).open())
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" '{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._links = None
self._exercises = None
@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:
self._notes = Notes(self)
return self._notes
@property
def exercises(self) -> Exercises:
if not self._exercises:
self._exercises = Exercises(self)
return self._exercises
def __eq__(self, other):
if other is None:
return False
return self.path == other.path
def ignored_courses():
def ignored_courses() -> List[Course]:
if (ROOT / COURSE_IGNORE_FILE).is_file():
with open(ROOT / COURSE_IGNORE_FILE) as ignore:
lines = ignore.readlines()
@ -45,7 +64,7 @@ def ignored_courses():
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()]
_courses = [Course(path) for path in course_directories]
return sorted(_courses, key=lambda c: c.name)
@ -56,7 +75,7 @@ class Courses(list):
list.__init__(self, read_files())
@property
def current(self):
def current(self) -> Course:
return Course(CURRENT_COURSE_ROOT.resolve())
@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 os
import re
import subprocess
import warnings
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
# TODO
@ -21,34 +23,39 @@ def filename2number(s):
class Lecture:
def __init__(self, file_path, course):
def __init__(self, file_path, notes):
with file_path.open() as f:
for line in f:
lecture_match = re.search(DEFAULT_LECTURE_SEARCH_REGEX, line)
if lecture_match:
break
# number = int(lecture_match.group(1))
if lecture_match:
date_str = lecture_match.group(2)
try:
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)
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.date = date
self.week = week
self.number = filename2number(file_path.stem)
self.title = title
self.course = course
self.notes = notes
def edit(self):
subprocess.Popen([
"termite",
"-e",
f"vim --servername tex-vorlesung --remote-silent {str(self.file_path)}"
])
edit(self.file_path, rootpath=self.notes.root, env=self.notes.environment())
def __str__(self):
return f'<Lecture {self.course.info["short"]} {self.number} "{self.title}">'
@ -71,7 +78,7 @@ class Lectures(list):
def read_files(self):
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):
if len(self) == 0:
@ -111,12 +118,13 @@ class Lectures(list):
date = today.strftime(DATE_FORMAT)
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.write_text(vimtex_root_str + header_str)
self.read_files()
lec = Lecture(new_lecture_path, self.course)
lec = Lecture(new_lecture_path, self.notes)
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 rofi import rofi
from utils import generate_short_title
from config import MAX_LEN
from config_loader import MAX_LEN
script = Courses().current.notes
lectures = script.lectures
notes = Courses().current.notes
lectures = notes.lectures
sorted_lectures = sorted(lectures, key=lambda l: -l.number)
@ -24,11 +24,17 @@ key, index, selected = rofi('Select lecture', options, [
'-lines', 5,
'-markup-rows',
'-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:
sorted_lectures[index].edit()
elif key == 1:
new_lecture = script.new_lecture()
new_lecture = notes.new_lecture()
new_lecture.edit()
elif key == 2:
notes.edit_master()
elif key == 3:
notes.edit_full()

View file

@ -1,5 +1,6 @@
import subprocess
def rofi(prompt, options, rofi_args=[], fuzzy=True):
optionstr = '\n'.join(option.replace('\n', ' ') for option in options)
args = ['rofi', '-sort', '-no-levenshtein-sort']
@ -9,7 +10,6 @@ def rofi(prompt, options, rofi_args=[], fuzzy=True):
args += rofi_args
args = [str(arg) for arg in args]
result = subprocess.run(args, input=optionstr, stdout=subprocess.PIPE, universal_newlines=True)
returncode = result.returncode
stdout = result.stdout.strip()
@ -20,11 +20,18 @@ def rofi(prompt, options, rofi_args=[], fuzzy=True):
except ValueError:
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:
key = 0
elif returncode == 1:
key = -1
elif returncode > 9:
key = returncode - 9
else: # This case should never be reached
key = -2
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
)