Compare commits

..

2 commits

Author SHA1 Message Date
Maximilian Keßler
1a595c7ad1 use termite as shell 2021-09-16 22:55:49 +02:00
Maximilian Keßler
20a146d96c change \lecture command and corresponding regex 2021-09-16 22:55:37 +02:00
33 changed files with 461 additions and 881 deletions

3
.gitignore vendored
View file

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

View file

@ -23,7 +23,7 @@ SOFTWARE.
MIT License MIT License
Copyright (c) 2021,2022 Maximilian Keßler Copyright (c) 2021 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

View file

@ -1,16 +0,0 @@
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,16 +1,9 @@
# Fork # Fork
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. 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.
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).
@ -21,7 +14,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.texvllt sollte ich │   ├── master.tex
│   ├── lec_01.tex │   ├── lec_01.tex
│   ├── ... │   ├── ...
│   ├── lec_13.tex │   ├── lec_13.tex
@ -159,7 +152,3 @@ 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

View file

@ -1,24 +0,0 @@
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 Normal file
View file

@ -0,0 +1,265 @@
% 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}

View file

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

10
scripts/compile-all-masters.py Executable file
View file

@ -0,0 +1,10 @@
#!/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,5 +1,4 @@
from pathlib import Path from pathlib import Path
import pytz
# default is 'primary', if you are using a separate calendar for your course schedule, # 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 # your calendarId (which you can find by going to your Google Calendar settings, selecting
@ -11,40 +10,14 @@ USERCALENDARID = 'primary'
CURRENT_COURSE_SYMLINK = Path('~/current_course').expanduser() CURRENT_COURSE_SYMLINK = Path('~/current_course').expanduser()
CURRENT_COURSE_ROOT = CURRENT_COURSE_SYMLINK.resolve() CURRENT_COURSE_ROOT = CURRENT_COURSE_SYMLINK.resolve()
CURRENT_COURSE_WATCH_FILE = Path('/tmp/current_course').resolve() CURRENT_COURSE_WATCH_FILE = Path('/tmp/current_course').resolve()
ROOT = Path('~/uni/semester-6').expanduser() ROOT = Path('~/Uni/semester-5').expanduser()
DATE_FORMAT = '%a %d %b %Y' DATE_FORMAT = '%a %d %b %Y'
LOCALE = "de_DE.utf8" LOCALE = "de_DE.utf8"
COURSE_IGNORE_FILE = '.courseignore' COURSE_IGNORE_FILE = '.courseignore'
COURSE_INFO_FILE_NAME = 'info.yaml' COURSE_INFO_FILE = 'info.yaml'
DEFAULT_MASTER_FILE_NAME = 'master.tex'
MAX_LEN = 40 MAX_LEN = 40
LECTURE_START_MARKER = 'start lectures' LECTURE_START_MARKER = 'start lectures'
LECTURE_END_MARKER = 'end lectures' LECTURE_END_MARKER = 'end lectures'
DEFAULT_NEW_LECTURE_HEADER = r'\lecture[]{{{date}}}{{{title}}}' DEFAULT_NEW_LECTURE_HEADER = r'\lecture[]{{{date}}}{{{title}}}'
DEFAULT_NEW_LECTURE_TITLE = 'Untitled'
DEFAULT_LECTURE_SEARCH_REGEX = r'lecture.*({\d*})?{(.*?)}{(.*)}' 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}}"
])

View file

@ -11,6 +11,7 @@ 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
@ -20,15 +21,14 @@ 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_loader import USERCALENDARID, TIMEZONE, SCHEDULER_DELAY from config import USERCALENDARID
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,43 +53,37 @@ 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):
ldots = ' ...' ellipsis = ' ...'
if len(string) < length: if len(string) < length:
return string return string
return string[:length - len(ldots)] + ldots return string[:length - len(ellipsis)] + ellipsis
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 minute' return '1 minuut'
if minutes < 60: if minutes < 60:
return f'{minutes} min' return f'{minutes} min'
hours = math.floor(minutes / 60) hours = math.floor(minutes/60)
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} hours' return f'{hours} uur'
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:
@ -101,16 +95,15 @@ 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 < e['end']), None) current = next((e for e in events if e['start'] < now and 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('in'), gray('over'),
formatdd(now, nxt['start']), formatdd(now, nxt['start']),
location(nxt['location']) location(nxt['location'])
) )
@ -118,24 +111,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('Ends in'), formatdd(now, current['end']) + '!') return join(gray('Einde over'), formatdd(now, current['end']) + '!')
if current['end'] == nxt['start']: if current['end'] == nxt['start']:
return join( return join(
gray('Ends in'), gray('Einde over'),
formatdd(now, current['end']) + gray('.'), formatdd(now, current['end']) + gray('.'),
gray('Afterwards'), gray('Hierna'),
summary(nxt['summary']), summary(nxt['summary']),
location(nxt['location']) location(nxt['location'])
) )
return join( return join(
gray('Ends in'), gray('Einde over'),
formatdd(now, current['end']) + gray('.'), formatdd(now, current['end']) + gray('.'),
gray('Afterwards'), gray('Hierna'),
summary(nxt['summary']), summary(nxt['summary']),
location(nxt['location']), location(nxt['location']),
gray('after a break of'), gray('na een pauze van'),
formatdd(current['end'], nxt['start']) formatdd(current['end'], nxt['start'])
) )
@ -157,15 +150,20 @@ 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=TIMEZONE) now = datetime.datetime.now(tz=TZ)
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)
print('Searching for events') print('Searching for events')
@ -189,19 +187,21 @@ 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=TIMEZONE) now = datetime.datetime.now(tz=TZ)
print(text(events, now)) print(text(events, now))
if now < evening: if now < evening:
scheduler.enter(SCHEDULER_DELAY, 1, print_message) scheduler.enter(DELAY, 1, print_message)
for event in events: for event in events:
# absolute entry, priority 1 # 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 # Immediate, priority 1
scheduler.enter(0, 1, print_message) scheduler.enter(0, 1, print_message)
@ -218,7 +218,6 @@ 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
src/courses.py → scripts/courses.py Normal file → Executable file
View file

@ -1,59 +1,40 @@
#!/usr/bin/python3 #!/usr/bin/python3
from pathlib import Path
import yaml
import warnings import warnings
import yaml from lectures import Lectures
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 links import Links from config import ROOT, CURRENT_COURSE_ROOT, CURRENT_COURSE_SYMLINK, CURRENT_COURSE_WATCH_FILE, COURSE_IGNORE_FILE, \
from utils import merge_dictionaries COURSE_INFO_FILE
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_NAME).is_file(): if (path / COURSE_INFO_FILE).is_file():
self.info = yaml.safe_load((path / COURSE_INFO_FILE_NAME).open()) self.info = yaml.safe_load((path / COURSE_INFO_FILE).open())
else: else:
warnings.warn(f"No course info file found in directory '{path.stem}'. Place a {COURSE_INFO_FILE_NAME} " warnings.warn(f"No course info file found in directory '{path.stem}'. Place a {COURSE_INFO_FILE} "
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': str(path.stem) + ' (unnamed course)'} self.info = {'title': path.stem, 'short': path.stem}
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 links(self) -> Links: def notes(self):
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() -> List[Course]: def ignored_courses():
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()
@ -64,7 +45,7 @@ def ignored_courses() -> List[Course]:
return [] return []
def read_files() -> List[Course]: def read_files():
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)
@ -75,7 +56,7 @@ class Courses(list):
list.__init__(self, read_files()) list.__init__(self, read_files())
@property @property
def current(self) -> Course: def current(self):
return Course(CURRENT_COURSE_ROOT.resolve()) return Course(CURRENT_COURSE_ROOT.resolve())
@current.setter @current.setter

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

@ -2,12 +2,10 @@
import locale import locale
import os import os
import re import re
import warnings import subprocess
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
@ -23,39 +21,34 @@ def filename2number(s):
class Lecture: class Lecture:
def __init__(self, file_path, notes): def __init__(self, file_path, course):
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
if lecture_match: # number = int(lecture_match.group(1))
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) date_str = lecture_match.group(2)
else: date = datetime.strptime(date_str, DATE_FORMAT)
date = datetime.min week = get_week(date)
week = get_week(date)
title = 'Error while parsing lecture file' title = lecture_match.group(3)
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.notes = notes self.course = course
def edit(self): def edit(self):
edit(self.file_path, rootpath=self.notes.root, env=self.notes.environment()) subprocess.Popen([
"termite",
"-e",
f"vim --servername tex-vorlesung --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}">'
@ -78,7 +71,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.notes) for f in files), key=lambda l: l.number) return sorted((Lecture(f, self.course) 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:
@ -118,13 +111,12 @@ 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( header_str = DEFAULT_NEW_LECTURE_HEADER.format(number=new_lecture_number, date=date, title='Untitled')
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.notes) lec = Lecture(new_lecture_path, self.course)
return lec return lec

90
scripts/notes.py Normal file
View file

@ -0,0 +1,90 @@
#!/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

@ -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_loader import MAX_LEN from config import MAX_LEN
notes = Courses().current.notes script = Courses().current.notes
lectures = notes.lectures lectures = script.lectures
sorted_lectures = sorted(lectures, key=lambda l: -l.number) sorted_lectures = sorted(lectures, key=lambda l: -l.number)
@ -24,17 +24,11 @@ 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', 'Alt+n', '-kb-custom-1', 'Ctrl+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 = notes.new_lecture() new_lecture = script.new_lecture()
new_lecture.edit() new_lecture.edit()
elif key == 2:
notes.edit_master()
elif key == 3:
notes.edit_full()

View file

@ -1,6 +1,5 @@
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']
@ -10,6 +9,7 @@ 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,18 +20,11 @@ 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

23
scripts/utils.py Normal file
View file

@ -0,0 +1,23 @@
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

@ -1,13 +0,0 @@
#!/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()

View file

@ -1,53 +0,0 @@
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

@ -1,108 +0,0 @@
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)

View file

@ -1,56 +0,0 @@
#!/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)

View file

@ -1,69 +0,0 @@
#!/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)

View file

@ -1,22 +0,0 @@
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
)

View file

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

View file

@ -1,147 +0,0 @@
#!/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

View file

@ -1,37 +0,0 @@
#!/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)

View file

@ -1,28 +0,0 @@
#!/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)

View file

@ -1,48 +0,0 @@
#!/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

@ -1,52 +0,0 @@
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

View file

@ -1,42 +0,0 @@
#! /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
)