Last active
December 14, 2023 19:47
-
-
Save rougier/d559fbd766da14540e8eb47435a5782d to your computer and use it in GitHub Desktop.
org-mode agenda calendar in the terminal
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Copyright 2020 Nicolas P. Rougier - BSD License | |
# | |
# from reading org-mode emacs files, display a formated calendar in the | |
# terminal showing holidays and busy days and upcomiing events. | |
import holidays # https://pypi.org/project/holidays/ | |
import calendar | |
import datetime | |
import orgparse # https://pypi.org/project/orgparse | |
# Colors for displaying things | |
color = { "month" : "\033[1;37;40m", | |
"week" : "\033[30m", | |
"weekend": "\033[0;2m", | |
"vacant": "\033[0;2m", | |
"busy-0" : "\033[0m", | |
"busy-1" : "\033[48;5;223m", | |
"busy-2" : "\033[48;5;216m", | |
"busy-3" : "\033[48;5;209m", | |
"busy-4" : "\033[48;5;202m", | |
"deadline": "\033[1;31m", | |
"today" : "\033[1m", | |
"reset" : "\033[0m" } | |
markers = { | |
"busy-0" : " ", | |
"busy-1" : "⠁", | |
"busy-2" : "⠃", | |
"busy-3" : "⠇", | |
"busy-4" : "⠏", | |
"busy-5" : "⠟", | |
"busy-6" : "⠿", | |
} | |
# French holidays + Inria holidays | |
class InriaHolidays(holidays.France): | |
def _populate(self, year): | |
holidays.France._populate(self, year) | |
self[datetime.date(2020, 5, 22)] = "RTT" | |
self[datetime.date(2020, 7, 13)] = "RTT" | |
inria_holidays = InriaHolidays() | |
def yearday(date): | |
return (datetime.date(date.year,date.month,date.day) - | |
datetime.date(date.year, 1, 1)).days + 1 | |
def daterange(start, end): | |
for n in range(int ((end - start).days)+1): | |
yield start + datetime.timedelta(n) | |
def is_today(year, month, day): | |
return datetime.date(year, month, day) == datetime.date.today() | |
def is_weekend(year, month, day): | |
return datetime.date(year, month, day).weekday() in [5,6] | |
def is_vacant(year, month, day): | |
return datetime.date(year, month, day) in inria_holidays | |
def is_busy(year, month, day): | |
date = datetime.datetime(year, month, day) | |
return busydays[yearday(date)] | |
def is_deadline(year, month, day): | |
date = datetime.datetime(year, month, day) | |
return yearday(date) in deadlines | |
def format_month(year, month): | |
day_names = [day[:2] for day in list(calendar.day_name)] | |
month_names = list(calendar.month_name)[1:] | |
width = 7*3-1 | |
lines = [""]*8 | |
# Month name | |
# ---------- | |
name = month_names[month-1] | |
lines[0] = color["month"] + name.center(width) + color["reset"] + " " | |
# Week day names | |
# -------------- | |
s = color["week"] | |
for name in day_names[:5]: s += name + " " | |
s += color["reset"] + color["weekend"] | |
for name in day_names[5:]: s += name + " " | |
s += color["reset"] | |
lines[1] = s | |
# Week days | |
# --------- | |
first = calendar.weekday(year, month, 1) | |
last = calendar.monthrange(year, month)[1] | |
for line in range(6): | |
s = "" | |
for column in range(7): | |
day = line*7 + column - first + 1 | |
if day < 1 or day > last: | |
s += " " | |
else: | |
c = "" | |
c += color["busy-" + str( min(is_busy(year, month, day),4))] | |
if is_weekend(year, month, day): | |
c += color["weekend"] | |
else: | |
c += color["week"] | |
if is_today(year, month, day): | |
c += color["today"] | |
if is_vacant(year, month, day): | |
c += color["vacant"] | |
s += c + "%2d" % day | |
# s += color["reset"] | |
if is_deadline(year, month, day): | |
s += color["deadline"] + "!" | |
else: | |
s += markers["busy-" + str(min(is_busy(year, month, day),6))] | |
s += color["reset"] | |
lines[2+line] = s | |
return lines | |
# Fill in busy days from agenda.org | |
busydays = { i:0 for i in range(366+1) } | |
deadlines = set() | |
events = [] | |
for filename in ["/Users/rougier/Documents/org/agenda.org", | |
"/Users/rougier/Documents/org/todo.org"]: | |
agenda = orgparse.load(filename) | |
for node in agenda: | |
if hasattr(node, "datelist") and node.datelist: | |
for date in node.datelist: | |
d = date.start | |
d = datetime.date(d.year,d.month,d.day) | |
# busydays.add(yearday(d)) | |
busydays[yearday(d)] += 1 | |
events.append([d, None, node.heading, 0, list(node.tags)]) | |
if hasattr(node, "rangelist") and node.rangelist: | |
for date in node.rangelist: | |
ds,de = date.start, date.end | |
ds = datetime.date(ds.year, ds.month, ds.day) | |
de = datetime.date(de.year, de.month, de.day) | |
if de > ds: events.append([ds, de, node.heading, 0, list(node.tags)]) | |
else: events.append([ds, None, node.heading, 0, list(node.tags)]) | |
for i,d in enumerate(daterange(date.start, date.end)): | |
# busydays.add(yearday(d)) | |
busydays[yearday(d)] += 1 | |
if hasattr(node, "deadline") and node.deadline: | |
d = node.deadline.start | |
d = datetime.date(d.year,d.month,d.day) | |
deadlines.add(yearday(d)) | |
events.append([d, None, node.heading, 1, list(node.tags)]) | |
events.sort(key=lambda data: data[0]) | |
# Print agenda (3 months per line) | |
n = 4 | |
lines = [] | |
print("\033[2J;\033[H") # clear terminal | |
for month in range(1, 13, n): | |
months = [format_month(2020, month+i) for i in range(n)] | |
for i in range(8): | |
line = "" | |
for j in range(n): | |
line += months[j][i] + " " | |
lines.append(line) | |
lines.append("\033[0m") | |
for line in lines: print(" "+line) | |
# Print details for upcoming events | |
print("\033[1;30m Upcoming events (next 2 weeks):") | |
print(" ───────────────────────────────") | |
today = datetime.date.today() | |
n = 0 | |
for start, stop, heading, deadline, tags in events: | |
if today <= start < today+datetime.timedelta(days=28): | |
n += 1 | |
print("\033[0m", end="") | |
if deadline: | |
print("\033[31m", end="") | |
elif start == today: | |
print("\033[1;30m", end="") | |
if deadline: | |
print(" ! ", end="") | |
else: | |
print(" ", end="") | |
if stop is None: | |
print("{0}: {1}".format(start, heading), end="") | |
else: | |
print("{0} to {1}: {2}".format(start, stop, heading), end="") | |
if tags: print("\033[2m ({0})".format(tags[0])) | |
if n > 21: break | |
for i in range(22-n): | |
print() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Color comes from your terminal (but the colored day)