Compare commits
74 commits
custom-set
...
main
Author | SHA1 | Date | |
---|---|---|---|
c516684e11 | |||
152fe36797 | |||
cf90c1f2ed | |||
f2212b4f71 | |||
ce0b8edf47 | |||
98e661f571 | |||
2ebd7f4e8c | |||
ce8dd726e5 | |||
64564562b8 | |||
308a877251 | |||
f3896398d4 | |||
252e845505 | |||
28c841e7f4 | |||
693c9d37e0 | |||
231b57df91 | |||
2aa8cd2142 | |||
6cba512c01 | |||
592f436fd4 | |||
e8c0830f7d | |||
db9b26dcbf | |||
9631f273e9 | |||
79d7e17b5f | |||
6d7e96f43b | |||
fa6bec2835 | |||
0ff7799dee | |||
e575d61c2b | |||
|
8cf70e724d | ||
|
0d7d1cc2f3 | ||
|
a4f973da22 | ||
|
6305fced47 | ||
|
b2fded51e6 | ||
|
8659206ee6 | ||
|
18621cf9b8 | ||
|
e5c40a54a0 | ||
|
0aafc87ed1 | ||
|
65efb9c854 | ||
|
f2e0ffc05d | ||
|
8d5af2f340 | ||
|
96a3650663 | ||
|
f82b732d38 | ||
|
ab9e4e665f | ||
|
ced953bb82 | ||
|
6e9f6c0c06 | ||
|
7728ec97ca | ||
|
32487937a9 | ||
|
b1504fe2ea | ||
|
19cd1ef603 | ||
|
d028139860 | ||
|
49689e4dd0 | ||
|
86bb3babe5 | ||
|
adc8b84f3f | ||
|
fc9978b484 | ||
|
3407f03091 | ||
|
c46cb220ae | ||
|
7f20952fb4 | ||
|
9b528a91a2 | ||
|
2f646b0780 | ||
|
c1e2fe5500 | ||
|
73cae4f2f1 | ||
|
2f967f57cc | ||
|
fc7c77e5d7 | ||
|
d952a22591 | ||
|
78ff6c693a | ||
|
10a8a42b84 | ||
|
602f6e4323 | ||
|
5a2fa4494e | ||
|
56637a3b16 | ||
|
758ace4ffe | ||
|
4e71f12254 | ||
|
12b1632f8e | ||
|
9f498a7c61 | ||
|
1b82dfd627 | ||
|
239899b75a | ||
|
b98b23457b |
34 changed files with 901 additions and 481 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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__
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -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
16
Makefile
Normal 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}
|
15
README.md
15
README.md
|
@ -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
50
config/config.py
Normal 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
24
config/fallback.yaml
Normal 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: '.'
|
265
preamble.tex
265
preamble.tex
|
@ -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
5
requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
google-api-python-client
|
||||||
|
google-auth-oauthlib
|
||||||
|
python-dateutil
|
||||||
|
pytz
|
||||||
|
PyYAML
|
|
@ -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()
|
|
|
@ -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{(.*?)}{(.*?)}{(.*)}'
|
|
|
@ -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
|
|
|
@ -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
|
|
13
src/compile_all_full_versions.py
Executable file
13
src/compile_all_full_versions.py
Executable 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
53
src/config_loader.py
Normal 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
|
|
@ -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
45
scripts/courses.py → src/courses.py
Executable file → Normal 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
108
src/exercises.py
Normal 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
56
src/file_list.py
Normal 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
69
src/labels.py
Normal 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
36
scripts/lectures.py → src/lectures.py
Executable file → Normal 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
22
src/links.py
Normal 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
6
src/new-writeup.py
Executable 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
147
src/notes.py
Normal 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
37
src/open.py
Executable 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
28
src/parse_counters.py
Executable 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
48
src/rofi-exercises.py
Executable 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)
|
|
@ -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()
|
|
@ -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
52
src/utils.py
Normal 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
42
src/window_subprocess.py
Normal 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
|
||||||
|
)
|
Loading…
Reference in a new issue