Compare commits

..

No commits in common. "master" and "1b0080e843bd977275ab1004161f78bfae0d5dcd" have entirely different histories.

3 changed files with 63 additions and 146 deletions

15
LICENSE
View File

@ -1,15 +0,0 @@
decal - a GNU cal-like program with CalDAV synchronization
Copyright (C) 2022 Yessiest
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@ -7,14 +7,14 @@ This is a calendar app which tries to bring something new to the functionality o
This project is currently very WIP, the todo list is: This project is currently very WIP, the todo list is:
- [x] Basic decal functionality (receiving events and showing a tagged calendar) - [x] Basic decal functionality (receiving events and showing a tagged calendar)
- [x] Event details - [ ] Event details
- [ ] Creating events - [ ] Creating events
- [ ] Modifying events - [ ] Modifying events
- [ ] Task lists - [ ] Task lists
- [ ] Task lists as calendar tags (maybe?) - [ ] Task lists as calendar tags (maybe?)
- [ ] Creating, modifying, removing, completing tasks - [ ] Creating, modifying, removing, completing tasks
- [x] Offline caching (partial) - [ ] Offline caching
- [x] JSON export - [ ] JSON export
# Dependencies # Dependencies
``` ```

188
decal
View File

@ -1,8 +1,6 @@
#!/usr/bin/python #!/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 configparser
import datetime import datetime
from datetime import date, timedelta
import calendar import calendar
import caldav import caldav
import argparse import argparse
@ -13,7 +11,7 @@ Version = "%(prog)s 0.1"
configpath = os.getenv("HOME")+"/.config/decal.conf" configpath = os.getenv("HOME")+"/.config/decal.conf"
config.read(configpath) config.read(configpath)
#define arguments #define arguments
today = date.today() today = datetime.date.today()
parser = argparse.ArgumentParser(description="Cal with events.") parser = argparse.ArgumentParser(description="Cal with events.")
parser.add_argument("year", parser.add_argument("year",
action="store", action="store",
@ -43,12 +41,6 @@ parser.add_argument("--create",
parser.add_argument("--calendar", parser.add_argument("--calendar",
action="append", action="append",
help="Specify a calendar (or multiple calendars) to sync from") 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", parser.add_argument("-1",
action="store_true", action="store_true",
help="show only a single month (default)") help="show only a single month (default)")
@ -62,9 +54,6 @@ parser.add_argument("-n",
action="store", action="store",
type=int, type=int,
help="show n months") help="show n months")
parser.add_argument("-s","--sync",
action="store_true",
help="sync the calendar cache")
args = vars(parser.parse_args()) args = vars(parser.parse_args())
@ -72,15 +61,14 @@ args = vars(parser.parse_args())
if args["create"]: if args["create"]:
print("Not implemented") print("Not implemented")
exit(0) exit(0)
if args["json"]:
print("Not implemented")
exit(0)
#check some stuff, do some warnings, initiate the config, etc. #check some stuff, do some warnings, initiate the config, etc.
if not os.path.exists(configpath): if not os.path.exists(configpath):
config['DEFAULT'] = {'uri': 'your caldav server here', config['DEFAULT'] = {'uri': 'your caldav server here',
'user': 'your username here', 'user': 'your username here',
'password': 'your pass 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") print("Creating an empty config in ~/.config/decal.conf")
with open(configpath,'w') as configfile: with open(configpath,'w') as configfile:
config.write(configfile) config.write(configfile)
@ -101,14 +89,17 @@ if config['DEFAULT']['uri'] == "your caldav server here":
#generate the actual calendar, line by line, output an array of lines. #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. #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): def gencal(year,month,start_on_sunday=True,cell_modifier=lambda d: d,append_year=True):
firstweekday = 0
if start_on_sunday:
firstweekday = 6
cal = calendar.Calendar(firstweekday=firstweekday) cal = calendar.Calendar(firstweekday=firstweekday)
lines = [""]*6 lines = [""]*6
monthstart = False monthstart = False
counter = 0 counter = 0
for curdate in cal.itermonthdates(year,month): for date in cal.itermonthdates(year,month):
lines[counter//7] lines[counter//7]
day = str(curdate)[-2:] day = str(date)[-2:]
if day == "01": if day == "01":
monthstart = not monthstart monthstart = not monthstart
if monthstart: if monthstart:
@ -117,11 +108,14 @@ def gencal(year,month,firstweekday=6,cell_modifier=lambda d: d,append_year=True)
lines[counter//7] += " " lines[counter//7] += " "
lines[counter//7] +=" " lines[counter//7] +=" "
counter+=1 counter+=1
weeklines = ["Mo","Tu","We","Th","Fr","Sa","Su"] month = datetime.date(year,month,1).strftime("%B %Y")
for times in range(firstweekday): padding = (21-len(month))//2
weeklines.append(weeklines.pop(0)) rpadding = 21%(padding+len(month)+padding)
lines.insert(0," ".join(weeklines)+" ") if start_on_sunday:
lines.insert(0,date(year,month,1).strftime("%B %Y").center(21)) lines.insert(0,"Su Mo Tu We Th Fr Sa ")
else:
lines.insert(0,"Mo Tu We Th Fr Sa Su ")
lines.insert(0,(" "*padding)+month+(" "*(padding+rpadding)))
lines[-1] += " "*(21-len(lines[-1])) lines[-1] += " "*(21-len(lines[-1]))
return lines return lines
@ -161,12 +155,12 @@ def span(year,month,offset):
#get 2 date values - start value to scan from and end value to scan up to. #get 2 date values - start value to scan from and end value to scan up to.
def getbounds(y,m,offset): def getbounds(y,m,offset):
start = date(y,m,1) start = datetime.date(y,m,1)
postnextmonth = span(y,m,offset) postnextmonth = span(y,m,offset)
nextmonth = span(postnextmonth[0],postnextmonth[1],-1) nextmonth = span(postnextmonth[0],postnextmonth[1],-1)
end = date(nextmonth[0], end = datetime.date(nextmonth[0],
nextmonth[1], nextmonth[1],
(date(postnextmonth[0],postnextmonth[1],1)-timedelta(days=1)).day) (datetime.date(postnextmonth[0],postnextmonth[1],1)-datetime.timedelta(days=1)).day)
return start,end return start,end
# generator for year/month pairs # generator for year/month pairs
@ -195,101 +189,57 @@ else:
start,end = getbounds(args["year"],args["month"],1) start,end = getbounds(args["year"],args["month"],1)
offset = 1 offset = 1
# 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()
# aggregate selected calendars # aggregate selected calendars
def aggregateCalendars(calendars): if "calendars" in config['DEFAULT']:
calendars2 = [] calendars2 = []
cals = config['DEFAULT']["calendars"].split(",") cals = config['DEFAULT']["calendars"].split(",")
for cal in calendars: for cal in calendars:
if cal.name in cal: if cal.name in cal:
calendars2.append(cal) calendars2.append(cal)
return calendars2 calendars = calendars2
def daysOfEvent(event): # hooo boy, fun things start here.
event = event.vobject_instance.vevent.contents # we generate a dict of (year,month) pairs for easier indexing
curdate = event["dtstart"][0].value events = {}
enddate = event["dtend"][0].value for ympair in ympairs((args['year'],args['month']),offset,dstart=args['3']):
while curdate <= enddate: events[ympair] = {}
if type(curdate) == datetime.datetime: # next, we iterate over events in each calendar, sorting them the folliwng way:
yield str(curdate.date()) # events[(int year,int month)][int day] = [event event1, event event2, ...]
else: # looks awful, is awful, overall i love this.
yield str(curdate) for cal in calendars:
curdate += timedelta(days=1) events_fetched = cal.date_search(start,end)
return for event in events_fetched:
event = event.vobject_instance.vevent.contents
def jsonifyEvent(event): curdate = event["dtstart"][0].value + datetime.timedelta(days=-1)
event = event.vobject_instance.vevent.contents while curdate < event["dtend"][0].value:
evdata = { curdate += datetime.timedelta(days=1)
"uid": event["uid"][0].value, curdindex = (curdate.year,curdate.month)
"dtstart": str(event["dtstart"][0].value), if curdindex in events:
"dtend": str(event["dtend"][0].value) if not curdate.day in events[curdindex]:
} events[curdindex][curdate.day] = []
# may or may not be there, idk why. vobject format is really weird events[curdindex][curdate.day].append(event)
for key in ["summary","description","status"]: else:
if key in event: break
evdata[key] = event[key][0].value # 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).
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 # and now we're just generating calendar lines
cal_prints = [] cal_prints = []
selected_date = date(args['year'],args['month'],args['day'])
for year,month in ympairs((args['year'],args['month']),offset,dstart=args['3']): 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 # a function to colorize cells in a more or less generic way
def lambdafunc(cell): def lambdafunc(cell):
day = date(year,month,int(cell)) day = int(cell)
if str(day) in events: if day in events[(year,month)]:
event = events[str(day)] event = events[(year,month)][day]
uid = event[0]["uid"].encode() uid = event[0]["uid"][0].value.encode()
cell = colorize(cell,uid) cell = colorize(cell,uid)
if day == selected_date: if datetime.date(year,month,day) == datetime.date.today():
cell = colorize(cell,"inverse") cell = colorize(cell,"inverse")
return cell return cell
cal_prints.append(gencal(year, cal_prints.append(gencal(year,
@ -313,21 +263,3 @@ for count in range(3-(len(cal_prints)%3)):
for cal in range(0,len(cal_prints),3): for cal in range(0,len(cal_prints),3):
printlines(cal_prints[cal],cal_prints[cal+1],cal_prints[cal+2]) 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)