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
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)
|