Initial commit

This commit is contained in:
Gilles Castel 2019-09-15 20:42:11 +02:00
commit e612b471a5
11 changed files with 862 additions and 0 deletions

264
preamble.tex Normal file
View file

@ -0,0 +1,264 @@
% Some basic packages
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{textcomp}
\usepackage[dutch]{babel}
\usepackage{url}
\usepackage{graphicx}
\usepackage{float}
\usepackage{booktabs}
\usepackage{enumitem}
% 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}
\pdfminorversion=7
\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}

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

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

11
scripts/config.py Normal file
View file

@ -0,0 +1,11 @@
from datetime import datetime
from pathlib import Path
def get_week(d=datetime.today()):
return (int(d.strftime("%W")) + 52 - 5) % 52
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('~/Documents/Kulak/bachelor_3/semester_2').expanduser()
DATE_FORMAT = '%a %d %b %Y %H:%M'

224
scripts/countdown.py Executable file
View file

@ -0,0 +1,224 @@
#!/usr/bin/python3
import pickle
import os
import os.path
import sys
import re
import math
import sched
import datetime
import time
import pytz
from dateutil.parser import parse
import http.client as httplib
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from courses import Courses
courses = Courses()
def authenticate():
print('Authenticating')
# If modifying these scopes, delete the file token.pickle.
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
# time.
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
print('Refreshing credentials')
creds.refresh(Request())
else:
print('Need to allow access')
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:
pickle.dump(creds, token)
service = build('calendar', 'v3', credentials=creds)
return service
def join(*args):
return ' '.join(str(e) for e in args if e)
def truncate(string, length):
ellipsis = ' ...'
if len(string) < length:
return string
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 minuut'
if minutes < 60:
return f'{minutes} min'
hours = math.floor(minutes/60)
rest_minutes = minutes % 60
if hours > 5 or rest_minutes == 0:
return f'{hours} uur'
return '{}:{:02d} uur'.format(hours, rest_minutes)
def location(text):
if not text:
return ''
match = re.search(r'\((.*)\)', text)
if not match:
return ''
return f'{gray("in")} {match.group(1)}'
def text(events, now):
current = next((e for e in events if e['start'] < now and now < e['end']), None)
if not current:
nxt = next((e for e in events if now <= e['start']), None)
if nxt:
return join(
summary(nxt['summary']),
gray('over'),
formatdd(now, nxt['start']),
location(nxt['location'])
)
return ''
nxt = next((e for e in events if e['start'] >= current['end']), None)
if not nxt:
return join(gray('Einde over'), formatdd(now, current['end']) + '!')
if current['end'] == nxt['start']:
return join(
gray('Einde over'),
formatdd(now, current['end']) + gray('.'),
gray('Hierna'),
summary(nxt['summary']),
location(nxt['location'])
)
return join(
gray('Einde over'),
formatdd(now, current['end']) + gray('.'),
gray('Hierna'),
summary(nxt['summary']),
location(nxt['location']),
gray('na een pauze van'),
formatdd(current['end'], nxt['start'])
)
def activate_course(event):
course = next(
(course for course in courses
if course.info['title'].lower() in event['summary'].lower()),
None
)
if not course:
return
courses.current = course
def main():
scheduler = sched.scheduler(time.time, time.sleep)
print('Initializing')
if 'TZ' in os.environ:
TZ = pytz.timezone(os.environ['TZ'])
else:
print("Warning: TZ environ variable not set")
service = authenticate()
print('Authenticated')
# Call the Calendar API
now = datetime.datetime.now(tz=TZ)
morning = now.replace(hour=6, minute=0, microsecond=0)
evening= now.replace(hour=23, minute=59, microsecond=0)
print('Searching for events')
def get_events(calendar):
events_result = service.events().list(
calendarId=calendar,
timeMin=morning.isoformat(),
timeMax=evening.isoformat(),
singleEvents=True,
orderBy='startTime'
).execute()
events = events_result.get('items', [])
return [
{
'summary': event['summary'],
'location': event.get('location', None),
'start': parse(event['start']['dateTime']),
'end': parse(event['end']['dateTime'])
}
for event in events
if 'dateTime' in event['start']
]
events = get_events('primary')
# events = get_events('primary') + get_events('school-calendar@import.calendar.google.com')
print('Done')
DELAY = 60
def print_message():
now = datetime.datetime.now(tz=TZ)
print(text(events, now))
if now < evening:
scheduler.enter(DELAY, 1, print_message)
for event in events:
# absolute entry, priority 1
scheduler.enterabs(event['start'].timestamp(), 1, activate_course, argument=(event, ))
# Immediate, priority 1
scheduler.enter(0, 1, print_message)
scheduler.run()
def wait_for_internet_connection(url, timeout=1):
while True:
conn = httplib.HTTPConnection(url, timeout=5)
try:
conn.request("HEAD", "/")
conn.close()
return True
except:
conn.close()
if __name__ == '__main__':
os.chdir(sys.path[0])
print('Waiting for connection')
wait_for_internet_connection('www.google.com')
main()

44
scripts/courses.py Executable file
View file

@ -0,0 +1,44 @@
#!/usr/bin/python3
from pathlib import Path
import yaml
from lectures import Lectures
from config import ROOT, CURRENT_COURSE_ROOT, CURRENT_COURSE_SYMLINK, CURRENT_COURSE_WATCH_FILE
class Course():
def __init__(self, path):
self.path = path
self.name = path.stem
self.info = yaml.load((path / 'info.yaml').open())
self._lectures = None
@property
def lectures(self):
if not self._lectures:
self._lectures = Lectures(self)
return self._lectures
def __eq__(self, other):
if other == None:
return False
return self.path == other.path
class Courses(list):
def __init__(self):
list.__init__(self, self.read_files())
def read_files(self):
course_directories = [x for x in ROOT.iterdir() if x.is_dir()]
_courses = [Course(path) for path in course_directories]
return sorted(_courses, key=lambda c: c.name)
@property
def current(self):
return Course(CURRENT_COURSE_ROOT.resolve())
@current.setter
def current(self, course):
CURRENT_COURSE_SYMLINK.unlink()
CURRENT_COURSE_SYMLINK.symlink_to(course.path)
CURRENT_COURSE_WATCH_FILE.write_text('{}\n'.format(course.info['short']))

190
scripts/lectures.py Executable file
View file

@ -0,0 +1,190 @@
#!/usr/bin/python3
import os
from datetime import datetime
from pathlib import Path
import locale
import re
import subprocess
from config import get_week, DATE_FORMAT, CURRENT_COURSE_ROOT
# TODO
locale.setlocale(locale.LC_TIME, "nl_BE.utf8")
def number2filename(n):
return 'lec_{0:02d}.tex'.format(n)
def filename2number(s):
return int(str(s).replace('.tex', '').replace('lec_', ''))
class Lecture():
def __init__(self, file_path, course):
with file_path.open() as f:
for line in f:
lecture_match = re.search(r'lecture\{(.*?)\}\{(.*?)\}\{(.*)\}', line)
if lecture_match:
break;
# number = int(lecture_match.group(1))
date_str = lecture_match.group(2)
date = datetime.strptime(date_str, DATE_FORMAT)
week = get_week(date)
title = lecture_match.group(3)
self.file_path = file_path
self.date = date
self.week = week
self.number = filename2number(file_path.stem)
self.title = title
self.course = course
def edit(self):
subprocess.Popen([
"x-terminal-emulator",
"-e", "zsh", "-i", "-c",
f"\\vim --servername kulak --remote-silent {str(self.file_path)}"
])
def __str__(self):
return f'<Lecture {self.course.info["short"]} {self.number} "{self.title}">'
class Lectures(list):
def __init__(self, course):
self.course = course
self.root = course.path
self.master_file = self.root / 'master.tex'
list.__init__(self, self.read_files())
def read_files(self):
files = self.root.glob('lec_*.tex')
return sorted((Lecture(f, self.course) for f in files), key=lambda l: l.number)
def parse_lecture_spec(self, string):
if len(self) == 0:
return 0
if string.isdigit():
return int(string)
elif string == 'last':
return self[-1].number
elif string == 'prev':
return self[-1].number - 1
def parse_range_string(self, arg):
all_numbers = [lecture.number for lecture in self]
if 'all' in arg:
return all_numbers
if '-' in arg:
start, end = [self.parse_lecture_spec(bit) for bit in arg.split('-')]
return list(set(all_numbers) & set(range(start, end + 1)))
return [self.parse_lecture_spec(arg)]
@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 'end lectures' in line:
part = 2
if part == 0:
header += line
if part == 2:
footer += line
if 'start lectures' in line:
part = 1
return (header, footer)
def update_lectures_in_master(self, r):
header, footer = self.get_header_footer(self.master_file)
body = ''.join(
' ' * 4 + r'\input{' + number2filename(number) + '}\n' for number in r)
self.master_file.write_text(header + body + footer)
def new_lecture(self):
if len(self) != 0:
new_lecture_number = self[-1].number + 1
else:
new_lecture_number = 1
new_lecture_path = self.root / number2filename(new_lecture_number)
today = datetime.today()
date = today.strftime(DATE_FORMAT)
new_lecture_path.touch()
new_lecture_path.write_text(f'\\lecture{{{new_lecture_number}}}{{{date}}}{{}}\n')
if new_lecture_number == 1:
self.update_lectures_in_master([1])
else:
self.update_lectures_in_master([new_lecture_number - 1, new_lecture_number])
self.read_files()
l = Lecture(new_lecture_path, self.course)
return l
def compile_master(self):
subprocess.Popen(
['latexmk', '-g', '-f', str(self.master_file)],
cwd=str(self.root),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
if __name__ == '__main__':
import sys
args = sys.argv
command = args[1]
lectures = Lectures(Path.cwd())
if command == 'view':
lecture_range = args[2]
lecture_range = lectures.parse_range_string(lecture_range)
print(lecture_range)
lectures.update_lectures_in_master(lecture_range)
lectures.compile_master()
if command == 'new':
lectures.new_lecture()
if command == 'init':
from utils import beautify
course_title = beautify(lectures.root.stem)
lines = [r'\documentclass[a4paper]{article}',
r'\input{../preamble.tex}',
fr'\title{{{course_title}}}',
r'\begin{document}',
r' \maketitle',
r' \tableofcontents',
r' % start lectures',
r' % end lectures',
r'\end{document}'
]
lectures.master_file.touch()
lectures.master_file.write_text('\n'.join(lines))
(lectures.root / 'master.tex.latexmain').touch()
info_file = lectures.root / 'info.yaml'
info_file.touch()
info_file.write_text(f"title: '{course_title}'")
(lectures.root / 'figures').mkdir()

22
scripts/rofi-courses.py Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/python3
from rofi import rofi
from courses import Courses
courses = Courses()
current = courses.current
try:
current_index = courses.index(current)
args = ['-a', current_index]
except ValueError:
args = []
code, index, selected = rofi('Select course', [c.info['title'] for c in courses], [
'-auto-select',
'-no-custom',
'-lines', len(courses)
] + args)
if index >= 0:
courses.current = courses[index]

22
scripts/rofi-lectures-view.py Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/python3
from courses import Courses
from rofi import rofi
lectures = Courses().current.lectures
commands = ['last', 'prev-last', 'all', 'prev']
options = ['Current lecture', 'Last two lectures', 'All lectures', 'Previous lectures']
key, index, selected = rofi('Select view', options, [
'-lines', 4,
'-auto-select'
])
if index >= 0:
command = commands[index]
else:
command = selected
lecture_range = lectures.parse_range_string(command)
lectures.update_lectures_in_master(lecture_range)
lectures.compile_master()

32
scripts/rofi-lectures.py Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/python3
from courses import Courses
from rofi import rofi
from utils import generate_short_title, MAX_LEN
lectures = Courses().current.lectures
sorted_lectures = sorted(lectures, key=lambda l: -l.number)
options = [
"{number: >2}. <b>{title: <{fill}}</b> <span size='smaller'>{date} ({week})</span>".format(
fill=MAX_LEN,
number=lecture.number,
title=generate_short_title(lecture.title),
date=lecture.date.strftime('%a %d %b'),
week=lecture.week
)
for lecture in sorted_lectures
]
key, index, selected = rofi('Select lecture', options, [
'-lines', 5,
'-markup-rows',
'-kb-row-down', 'Down',
'-kb-custom-1', 'Ctrl+n'
])
if key == 0:
sorted_lectures[index].edit()
elif key == 1:
new_lecture = lectures.new_lecture()
new_lecture.edit()

30
scripts/rofi.py Normal file
View file

@ -0,0 +1,30 @@
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']
if fuzzy:
args += ['-matching', 'fuzzy']
args += ['-dmenu', '-p', prompt, '-format', 's', '-i']
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()
selected = stdout.strip()
try:
index = [opt.strip() for opt in options].index(selected)
except ValueError:
index = -1
if returncode == 0:
key = 0
elif returncode == 1:
key = -1
elif returncode > 9:
key = returncode - 9
return key, index, selected

14
scripts/utils.py Normal file
View file

@ -0,0 +1,14 @@
def beautify(string):
return string.replace('_', ' ').replace('-', ' ').title()
def unbeautify(string):
return string.replace(' ', '-').lower()
MAX_LEN = 40
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