the python CalDAV synced calendar
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

333 lines
11 KiB

#!/usr/bin/python
# if you're reading the source code for this (oof), feel free to suggest improvements for this, or, well, anything above or below this comment (as long as it's not just "rewrite this entire thing in C++ for me because i think python bad", "idk how but optimize stuff kthx". just don't be a dick, ok? thanks).
import configparser
import datetime
from datetime import date, timedelta
import calendar
import caldav
import argparse
import os
import json
config = configparser.ConfigParser()
Version = "%(prog)s 0.1"
configpath = os.getenv("HOME")+"/.config/decal.conf"
config.read(configpath)
#define arguments
today = date.today()
parser = argparse.ArgumentParser(description="Cal with events.")
parser.add_argument("year",
action="store",
default=today.year,
nargs="?",
type=int)
parser.add_argument("month",
action="store",
default=today.month,
nargs="?",
type=int)
parser.add_argument("day",
action="store",
default=today.day,
nargs="?",
type=int)
parser.add_argument("-V", "--version",
action="version",
version=Version,
help="Display the version of the program")
parser.add_argument("--json",
action="store_true",
help="Dump events output to json")
parser.add_argument("--create",
action="store_true",
help="Create a new event")
parser.add_argument("--calendar",
action="append",
help="Specify a calendar (or multiple calendars) to sync from")
parser.add_argument("-d",
action="store_true",
help="Show event details for selected day")
parser.add_argument("-m",
action="store_true",
help="Show event details for entire month")
parser.add_argument("-1",
action="store_true",
help="show only a single month (default)")
parser.add_argument("-3",
action="store_true",
help="show three months spanning the date")
parser.add_argument("-y",
action="store_true",
help="show the whole year")
parser.add_argument("-n",
action="store",
type=int,
help="show n months")
parser.add_argument("-s","--sync",
action="store_true",
help="sync the calendar cache")
args = vars(parser.parse_args())
# some stubs for now
if args["create"]:
print("Not implemented")
exit(0)
#check some stuff, do some warnings, initiate the config, etc.
if not os.path.exists(configpath):
config['DEFAULT'] = {'uri': 'your caldav server here',
'user': 'your username here',
'password': 'your pass here',
'cache':os.getenv('HOME')+'/.cache/decal.json',
'cacheEnabled':1,
'syncAfter':1}
print("Creating an empty config in ~/.config/decal.conf")
with open(configpath,'w') as configfile:
config.write(configfile)
configfile.close()
print("To properly utilize decal, please fill out the fields in the config")
exit(1)
for arg in ("user","password","uri"):
if not arg in config['DEFAULT']:
print("The config is incomplete, please check the \""+arg+"\" field")
exit(1)
if config['DEFAULT']['uri'] == "your caldav server here":
print("To properly utilize decal, please fill out the fields in the config")
exit(1)
#actual works begins here
#generate the actual calendar, line by line, output an array of lines.
#it works trust me, idk what is happening in this one but it works.
def gencal(year,month,firstweekday=6,cell_modifier=lambda d: d,append_year=True):
cal = calendar.Calendar(firstweekday=firstweekday)
lines = [""]*6
monthstart = False
counter = 0
for curdate in cal.itermonthdates(year,month):
lines[counter//7]
day = str(curdate)[-2:]
if day == "01":
monthstart = not monthstart
if monthstart:
lines[counter//7] += cell_modifier(day)
else:
lines[counter//7] += " "
lines[counter//7] +=" "
counter+=1
weeklines = ["Mo","Tu","We","Th","Fr","Sa","Su"]
for times in range(firstweekday):
weeklines.append(weeklines.pop(0))
lines.insert(0," ".join(weeklines)+" ")
lines.insert(0,date(year,month,1).strftime("%B %Y").center(21))
lines[-1] += " "*(21-len(lines[-1]))
return lines
# introduce some generic color names (idk how this will work, ig through parsing summaries? maybe? we'll figure that out ig)
color_names = {
"red":"0;31",
"green":"0;32",
"brown":"0;33",
"orange":"0;33",
"blue":"0;34",
"purple":"0;35",
"cyan":"0;36",
"yellow":"1;33",
"white":"1;37",
"blink":"5",
"bold":"1",
"italic":"3",
"underline":"4",
"inverse":"7",
"strikethrough":"9",
"light red":"1;31",
"light green":"1;32",
"light blue":"1;34",
"light purple":"1;36",
"light cyan":"0;37"
}
# add a function to colorize terminal text
def colorize(text,c):
if c in color_names:
return "\033["+color_names[c]+"m"+text+"\033[0m"
return "\033[38;2;"+str(c[0])+";"+str(c[1])+";"+str(c[2])+"m"+text+"\033[0m"
#calculate offset in months
def span(year,month,offset):
return year+((month+offset-1)//12),((month+offset-1)%12)+1
#get 2 date values - start value to scan from and end value to scan up to.
def getbounds(y,m,offset):
start = date(y,m,1)
postnextmonth = span(y,m,offset)
nextmonth = span(postnextmonth[0],postnextmonth[1],-1)
end = date(nextmonth[0],
nextmonth[1],
(date(postnextmonth[0],postnextmonth[1],1)-timedelta(days=1)).day)
return start,end
# generator for year/month pairs
def ympairs(startdate,offset,dstart = False):
if dstart == True:
startdate = span(startdate[0],startdate[1],-1)
for offset in range(offset):
yield span(startdate[0],startdate[1],offset)
return
# we calculate start/end dates and an offset for later use according to arguments
start = None
end = None
offset = None
if args["1"]:
start,end = getbounds(args["year"],args["month"],1)
offset = 1
elif args["3"]:
y,m = span(args["year"],args["month"],-1)
start,end = getbounds(y,m,3)
offset = 3
elif args["n"]:
start,end = getbounds(args["year"],args["month"],args["n"])
offset = args["n"]
else:
start,end = getbounds(args["year"],args["month"],1)
offset = 1
# aggregate selected calendars
def aggregateCalendars(calendars):
calendars2 = []
cals = config['DEFAULT']["calendars"].split(",")
for cal in calendars:
if cal.name in cal:
calendars2.append(cal)
return calendars2
def daysOfEvent(event):
event = event.vobject_instance.vevent.contents
curdate = event["dtstart"][0].value
enddate = event["dtend"][0].value
while curdate <= enddate:
if type(curdate) == datetime.datetime:
yield str(curdate.date())
else:
yield str(curdate)
curdate += timedelta(days=1)
return
def jsonifyEvent(event):
event = event.vobject_instance.vevent.contents
evdata = {
"uid": event["uid"][0].value,
"dtstart": str(event["dtstart"][0].value),
"dtend": str(event["dtend"][0].value)
}
# may or may not be there, idk why. vobject format is really weird
for key in ["summary","description","status"]:
if key in event:
evdata[key] = event[key][0].value
return evdata
def generateDateTree(calendars):
events = {}
for cal in calendars:
events_fetched = cal.date_search(start,end)
for event in events_fetched:
for date in daysOfEvent(event):
if not date in events:
events[date] = []
events[date].append(jsonifyEvent(event))
return events
cache = None
if "cache" in config['DEFAULT']:
if os.path.exists(config['DEFAULT']['cache']):
with open(config['DEFAULT']['cache'],"r") as file:
cache = json.loads(file.read())
def updateCriteria():
if not ("cacheEnabled" in config["DEFAULT"]):
return False
if (not (config['DEFAULT']["cacheEnabled"] == "1")):
return False
if not cache:
return True
date = datetime.datetime.strptime(cache["lastsync"],"%Y-%m-%d")
if args["sync"]:
return True
return (date.date() <= today - timedelta(days=int(config['DEFAULT']["syncAfter"])))
#Update cache if update criteria are met.
if updateCriteria():
# connect to the DAV and receive calendars
client = caldav.DAVClient(url = config['DEFAULT']['uri'],
username = config['DEFAULT']['user'],
password = config['DEFAULT']['password'])
principal = client.principal()
calendars = principal.calendars()
if "calendars" in config['DEFAULT']:
calendars = aggregateCalendars(calendars)
cache = generateDateTree(calendars)
cache["lastsync"] = str(today)
with open(config['DEFAULT']['cache'],"w") as file:
file.write(json.dumps(cache))
events = cache
if args["json"]:
print(json.dumps(events))
exit(0)
# and now we're just generating calendar lines
cal_prints = []
selected_date = date(args['year'],args['month'],args['day'])
for year,month in ympairs((args['year'],args['month']),offset,dstart=args['3']):
# a function to colorize cells in a more or less generic way
def lambdafunc(cell):
day = date(year,month,int(cell))
if str(day) in events:
event = events[str(day)]
uid = event[0]["uid"].encode()
cell = colorize(cell,uid)
if day == selected_date:
cell = colorize(cell,"inverse")
return cell
cal_prints.append(gencal(year,
month,
cell_modifier=lambdafunc))
# function to print 3 calendar lines at a time
def printlines(c1,c2,c3):
for count in range(len(c1)):
line = ""
line += c1.pop(0)+" "
if len(c2) > 0: line += c2.pop(0)+" "
if len(c3) > 0: line += c3.pop(0)+" "
print(line)
# array padding
for count in range(3-(len(cal_prints)%3)):
cal_prints.append([])
# finally, print the fucking calendar
for cal in range(0,len(cal_prints),3):
printlines(cal_prints[cal],cal_prints[cal+1],cal_prints[cal+2])
def printDailyEvents(events,evtrack=[]):
for event in events:
if not event in evtrack:
uid = event["uid"].encode()
print(colorize(event["dtstart"]+" - "+event["dtend"],uid)+":\n- "+event["summary"])
evtrack.append(event)
evtrack = []
if args["d"]:
printDailyEvents(events[str(selected_date)],evtrack=evtrack)
exit(0)
if args["m"]:
for day in range(*calendar.monthrange(args['year'],args['month'])):
key = str(date(args['year'],args['month'],day))
if key in events:
printDailyEvents(events[key],evtrack=evtrack)