#!/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)