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/test.py
scripts/.idea
scripts/credentials.json
scripts/token.pickle
__pycache__

View file

@ -23,7 +23,7 @@ SOFTWARE.
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
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
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.
##
If you want to install this, run `make install` on your favourite unix operating system.
This installs into `/opt/mkessler/university-setup`.
There is also a [PKGBUILD][pkgbuild] for Arch linux available.
# Managing LaTeX lecture notes
This repository complements my [third blog post about my note taking setup](https://castel.dev/post/lecture-notes-3).
@ -21,7 +14,7 @@ This repository complements my [third blog post about my note taking setup](http
ROOT
├── riemann-surfaces
│   ├── info.yaml
│   ├── master.texvllt sollte ich
│   ├── master.tex
│   ├── lec_01.tex
│   ├── ...
│   ├── lec_13.tex
@ -159,7 +152,3 @@ Some utility functions
#### `compile-all-masters.py`
This script updates the `master.tex` files to include all lectures and compiles them. I use when syncing my notes to the cloud. This way I always have access to my compiles notes on my phone.
[setup]: https://github.com/gillescastel/university-setup
[pkgbuild]: https://git.abstractnonsen.se/arch/university-setup-git

View file

@ -1,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
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
@ -11,40 +10,14 @@ 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()
ROOT = Path('~/Uni/semester-5').expanduser()
DATE_FORMAT = '%a %d %b %Y'
LOCALE = "de_DE.utf8"
COURSE_IGNORE_FILE = '.courseignore'
COURSE_INFO_FILE_NAME = 'info.yaml'
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[]{{{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}}"
])

View file

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

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

@ -1,59 +1,40 @@
#!/usr/bin/python3
from pathlib import Path
import yaml
import warnings
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 lectures import Lectures
from notes import Notes
from links import Links
from utils import merge_dictionaries
from exercises import Exercises
from config import ROOT, CURRENT_COURSE_ROOT, CURRENT_COURSE_SYMLINK, CURRENT_COURSE_WATCH_FILE, COURSE_IGNORE_FILE, \
COURSE_INFO_FILE
class Course:
def __init__(self, path):
self.path = path
self.name = path.stem
if (path / COURSE_INFO_FILE_NAME).is_file():
self.info = yaml.safe_load((path / COURSE_INFO_FILE_NAME).open())
if (path / COURSE_INFO_FILE).is_file():
self.info = yaml.safe_load((path / COURSE_INFO_FILE).open())
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" '{COURSE_IGNORE_FILE}' in your root directory ({ROOT})")
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.info = {'title': path.stem, 'short': path.stem}
self._notes = None
self._links = None
self._exercises = None
@property
def links(self) -> Links:
if not self._links:
self._links = Links(self)
return self._links
@property
def notes(self) -> Notes:
def notes(self):
if not self._notes:
self._notes = Notes(self)
return self._notes
@property
def exercises(self) -> Exercises:
if not self._exercises:
self._exercises = Exercises(self)
return self._exercises
def __eq__(self, other):
if other is None:
return False
return self.path == other.path
def ignored_courses() -> List[Course]:
def ignored_courses():
if (ROOT / COURSE_IGNORE_FILE).is_file():
with open(ROOT / COURSE_IGNORE_FILE) as ignore:
lines = ignore.readlines()
@ -64,7 +45,7 @@ def ignored_courses() -> List[Course]:
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()]
_courses = [Course(path) for path in course_directories]
return sorted(_courses, key=lambda c: c.name)
@ -75,7 +56,7 @@ class Courses(list):
list.__init__(self, read_files())
@property
def current(self) -> Course:
def current(self):
return Course(CURRENT_COURSE_ROOT.resolve())
@current.setter

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

@ -2,12 +2,10 @@
import locale
import os
import re
import warnings
import subprocess
from datetime import datetime
from config import DATE_FORMAT, LOCALE, DEFAULT_NEW_LECTURE_HEADER, DEFAULT_LECTURE_SEARCH_REGEX, \
DEFAULT_NEW_LECTURE_TITLE
from window_subprocess import edit
from config import DATE_FORMAT, LOCALE, DEFAULT_NEW_LECTURE_HEADER, DEFAULT_LECTURE_SEARCH_REGEX
from utils import get_week
# TODO
@ -23,39 +21,34 @@ def filename2number(s):
class Lecture:
def __init__(self, file_path, notes):
def __init__(self, file_path, course):
with file_path.open() as f:
for line in f:
lecture_match = re.search(DEFAULT_LECTURE_SEARCH_REGEX, line)
if lecture_match:
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)
else:
date = datetime.min
week = get_week(date)
title = 'Error while parsing lecture file'
self.file_path = file_path
self.date = date
self.week = week
self.number = filename2number(file_path.stem)
self.title = title
self.notes = notes
self.course = course
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):
return f'<Lecture {self.course.info["short"]} {self.number} "{self.title}">'
@ -78,7 +71,7 @@ class Lectures(list):
def read_files(self):
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):
if len(self) == 0:
@ -118,13 +111,12 @@ class Lectures(list):
date = today.strftime(DATE_FORMAT)
vimtex_root_str = f"%! TEX root = {str(os.path.relpath(self.notes.master_file, self.root))}\n"
header_str = DEFAULT_NEW_LECTURE_HEADER.format(
number=new_lecture_number, date=date, title=DEFAULT_NEW_LECTURE_TITLE)
header_str = DEFAULT_NEW_LECTURE_HEADER.format(number=new_lecture_number, date=date, title='Untitled')
new_lecture_path.touch()
new_lecture_path.write_text(vimtex_root_str + header_str)
self.read_files()
lec = Lecture(new_lecture_path, self.notes)
lec = Lecture(new_lecture_path, self.course)
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 rofi import rofi
from utils import generate_short_title
from config_loader import MAX_LEN
from config import MAX_LEN
notes = Courses().current.notes
lectures = notes.lectures
script = Courses().current.notes
lectures = script.lectures
sorted_lectures = sorted(lectures, key=lambda l: -l.number)
@ -24,17 +24,11 @@ key, index, selected = rofi('Select lecture', options, [
'-lines', 5,
'-markup-rows',
'-kb-row-down', 'Down',
'-kb-custom-1', 'Alt+n',
'-kb-custom-2', 'Alt+m',
'-kb-custom-3', 'Alt+s'
'-kb-custom-1', 'Ctrl+n'
])
if key == 0:
sorted_lectures[index].edit()
elif key == 1:
new_lecture = notes.new_lecture()
new_lecture = script.new_lecture()
new_lecture.edit()
elif key == 2:
notes.edit_master()
elif key == 3:
notes.edit_full()

View file

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

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
)