From e612b471a52508fce3294ee99ccaab19f61ad51b Mon Sep 17 00:00:00 2001 From: Gilles Castel Date: Sun, 15 Sep 2019 20:42:11 +0200 Subject: [PATCH] Initial commit --- preamble.tex | 264 +++++++++++++++++++++++++++++++++ scripts/compile-all-masters.py | 9 ++ scripts/config.py | 11 ++ scripts/countdown.py | 224 ++++++++++++++++++++++++++++ scripts/courses.py | 44 ++++++ scripts/lectures.py | 190 ++++++++++++++++++++++++ scripts/rofi-courses.py | 22 +++ scripts/rofi-lectures-view.py | 22 +++ scripts/rofi-lectures.py | 32 ++++ scripts/rofi.py | 30 ++++ scripts/utils.py | 14 ++ 11 files changed, 862 insertions(+) create mode 100644 preamble.tex create mode 100755 scripts/compile-all-masters.py create mode 100644 scripts/config.py create mode 100755 scripts/countdown.py create mode 100755 scripts/courses.py create mode 100755 scripts/lectures.py create mode 100755 scripts/rofi-courses.py create mode 100755 scripts/rofi-lectures-view.py create mode 100755 scripts/rofi-lectures.py create mode 100644 scripts/rofi.py create mode 100644 scripts/utils.py diff --git a/preamble.tex b/preamble.tex new file mode 100644 index 0000000..7a24445 --- /dev/null +++ b/preamble.tex @@ -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} diff --git a/scripts/compile-all-masters.py b/scripts/compile-all-masters.py new file mode 100755 index 0000000..7dca07a --- /dev/null +++ b/scripts/compile-all-masters.py @@ -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() diff --git a/scripts/config.py b/scripts/config.py new file mode 100644 index 0000000..aadc344 --- /dev/null +++ b/scripts/config.py @@ -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' diff --git a/scripts/countdown.py b/scripts/countdown.py new file mode 100755 index 0000000..5a604c3 --- /dev/null +++ b/scripts/countdown.py @@ -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() diff --git a/scripts/courses.py b/scripts/courses.py new file mode 100755 index 0000000..34cfe41 --- /dev/null +++ b/scripts/courses.py @@ -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'])) diff --git a/scripts/lectures.py b/scripts/lectures.py new file mode 100755 index 0000000..b53bdf9 --- /dev/null +++ b/scripts/lectures.py @@ -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'' + + +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() diff --git a/scripts/rofi-courses.py b/scripts/rofi-courses.py new file mode 100755 index 0000000..a702d59 --- /dev/null +++ b/scripts/rofi-courses.py @@ -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] diff --git a/scripts/rofi-lectures-view.py b/scripts/rofi-lectures-view.py new file mode 100755 index 0000000..bcc190c --- /dev/null +++ b/scripts/rofi-lectures-view.py @@ -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() diff --git a/scripts/rofi-lectures.py b/scripts/rofi-lectures.py new file mode 100755 index 0000000..b5e8f9d --- /dev/null +++ b/scripts/rofi-lectures.py @@ -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}. {title: <{fill}} {date} ({week})".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() diff --git a/scripts/rofi.py b/scripts/rofi.py new file mode 100644 index 0000000..101c1c6 --- /dev/null +++ b/scripts/rofi.py @@ -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 diff --git a/scripts/utils.py b/scripts/utils.py new file mode 100644 index 0000000..ac06a70 --- /dev/null +++ b/scripts/utils.py @@ -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 +