#!/usr/bin/env python # ** The MIT License ** # # Copyright (c) 2007 Eric Davis (aka Insanum) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # Dude... just buy me a beer. :-) # # # Home: http://code.google.com/p/gcalcli # # Author: Eric Davis <http://www.insanum.com> # # Requirements: # - Python 2 # http://www.python.org # - Google's GData Python module (for Python 2) # http://code.google.com/p/gdata-python-client # - dateutil Python module # http://www.labix.org/python-dateutil # - vobject Python module (optional, needed for importing ics/vcal files) # http://vobject.skyhouseconsulting.com # __program__ = 'gcalcli' __version__ = 'v2.2' __author__ = 'Eric Davis' import inspect import sys, os, re, urllib, getopt, shlex, subprocess import codecs, locale, csv, threading, getpass from Queue import Queue from ConfigParser import RawConfigParser from gdata.calendar.service import * from datetime import * from dateutil.tz import * from dateutil.parser import * from dateutil.rrule import * from unicodedata import east_asian_width def Version(): sys.stdout.write(__program__+' '+__version__+' ('+__author__+')\n') sys.exit(1) def Usage(): sys.stdout.write(''' Usage: gcalcli [options] command [command args] Options: --help this usage text --version version information --config <file> config file to read (default is '~/.gcalclirc') --user <username> google username --pw <password> password --cals [all, 'calendars' to work with (default is all calendars) default, - default (your default main calendar) owner, - owner (your owned calendars) editor, - editor (editable calendar) contributor, - contributor (non-owner but able to edit) read, - read (read only calendars) freebusy] - freebusy (only free/busy info visible) --cal <name>[#color] 'calendar' to work with (default is all calendars) - you can specify a calendar by name or by substring which can match multiple calendars - you can use multiple '--cal' arguments on the command line - in the config file specify multiple calendars in quotes separated by commas as: cal: "foo", "bar", "my cal" - an optional color override can be specified per calendar using the ending hashtag: --cal "Eric Davis"#green --cal foo#red or via the config file: cal: "foo"#red, "bar"#yellow, "my cal"#green --24hr show all dates in 24 hour format --details show all event details (i.e. length, location, reminders, contents) --ignore-started ignore old or already started events - when used with the 'agenda' command, ignore events that have already started and are in-progress with respect to the specified [start] time - when used with the 'search' command, ignore events that have already occurred and only show future events --width the number of characters to use for each column in the 'calw' and 'calm' command outputs (default is 10) --mon week begins with Monday for 'calw' and 'calm' command outputs (default is Sunday) --nc don't use colors --cal-owner-color specify the colors used for the calendars and dates --cal-editor-color each of these argument requires a <color> argument --cal-contributor-color which must be one of [ default, black, brightblack, --cal-read-color red, brightred, green, brightgreen, yellow, --cal-freebusy-color brightyellow, blue, brightblue, magenta, --date-color brightmagenta, cyan, brightcyan, white, --border-color brightwhite ] --tsv tab-separated output for 'agenda'. Format is: 'date' 'start' 'end' 'title' 'location' 'description' Commands: list list all calendars search <text> search for events - only matches whole words agenda [start] [end] get an agenda for a time period - start time default is 12am today - end time default is 5 days from start - example time strings: '9/24/2007' 'Sep 24 2007 3:30pm' '2007-09-24T15:30' '2007-09-24T15:30-8:00' '20070924T15' '8am' calw <weeks> [start] get a week based agenda in a nice calendar format - weeks is the number of weeks to display - start time default is beginning of this week - note that all events for the week(s) are displayed calm [start] get a month agenda in a nice calendar format - start time default is the beginning of this month - note that all events for the month are displayed and only one month will be displayed quick <text> quick add an event to a calendar - if a --cal is not specified then the event is added to the default calendar - example: 'Dinner with Eric 7pm tomorrow' '5pm 10/31 Trick or Treat' import [-v] [file] import an ics/vcal file to a calendar - if a --cal is not specified then the event is added to the default calendar - if a file is not specified then the data is read from standard input - if -v is given then each event in the file is displayed and you're given the option to import or skip it, by default everything is imported quietly without any interaction remind <mins> <command> execute command if event occurs within <mins> minutes time ('%s' in <command> is replaced with event start time and title text) - <mins> default is 10 - default command: 'notify-send -u critical -a gcalcli %s' ''') sys.exit(1) class CLR: useColor = True def __str__(self): if self.useColor: return self.color else: return "" class CLR_NRM(CLR): color = "\033[0m" class CLR_BLK(CLR): color = "\033[0;30m" class CLR_BRBLK(CLR): color = "\033[30;1m" class CLR_RED(CLR): color = "\033[0;31m" class CLR_BRRED(CLR): color = "\033[31;1m" class CLR_GRN(CLR): color = "\033[0;32m" class CLR_BRGRN(CLR): color = "\033[32;1m" class CLR_YLW(CLR): color = "\033[0;33m" class CLR_BRYLW(CLR): color = "\033[33;1m" class CLR_BLU(CLR): color = "\033[0;34m" class CLR_BRBLU(CLR): color = "\033[34;1m" class CLR_MAG(CLR): color = "\033[0;35m" class CLR_BRMAG(CLR): color = "\033[35;1m" class CLR_CYN(CLR): color = "\033[0;36m" class CLR_BRCYN(CLR): color = "\033[36;1m" class CLR_WHT(CLR): color = "\033[0;37m" class CLR_BRWHT(CLR): color = "\033[37;1m" def PrintErrMsg(msg): if CLR.useColor: sys.stdout.write(str(CLR_BRRED())) sys.stdout.write(msg) sys.stdout.write(str(CLR_NRM())) else: sys.stdout.write(msg) def PrintMsg(color, msg): if CLR.useColor: sys.stdout.write(str(color)) sys.stdout.write(msg) sys.stdout.write(str(CLR_NRM())) else: sys.stdout.write(msg) def DebugPrint(msg): return sys.stdout.write(str(CLR_YLW())) sys.stdout.write(msg) sys.stdout.write(str(CLR_NRM())) class gcalcli: gcal = None allCals = None cals = [] now = datetime.now(tzlocal()) feedPrefix = 'https://www.google.com/calendar/feeds/' agendaLength = 5 username = None password = None access = '' military = False details = False ignoreStarted = False calWidth = 10 calMonday = False command = 'notify-send -u critical -a gcalcli %s' tsv = False calOwnerColor = CLR_CYN() calEditorColor = CLR_NRM() calContributorColor = CLR_NRM() calReadColor = CLR_MAG() calFreeBusyColor = CLR_NRM() dateColor = CLR_YLW() borderColor = CLR_WHT() ACCESS_ALL = 'all' # non-google access level ACCESS_DEFAULT = 'default' # non-google access level ACCESS_CONTRIBUTOR = 'contributor' ACCESS_EDITOR = 'editor' ACCESS_FREEBUSY = 'freebusy' ACCESS_NONE = 'none' ACCESS_OVERRIDE = 'override' ACCESS_OWNER = 'owner' ACCESS_READ = 'read' ACCESS_RESPOND = 'respond' ACCESS_ROOT = 'root' def __init__(self, username=None, password=None, access='all', calNames=[], calNameColors=[], military=False, details=False, ignoreStarted=False, calWidth=10, calMonday=False, calOwnerColor=CLR_CYN(), calEditorColor=CLR_GRN(), calContributorColor=CLR_NRM(), calReadColor=CLR_MAG(), calFreeBusyColor=CLR_NRM(), dateColor=CLR_GRN(), borderColor=CLR_WHT(), tsv=False): self.gcal = CalendarService() self.gcal.ssl = True self.username = username self.password = password self.access = access self.military = military self.details = details self.ignoreStarted = ignoreStarted self.calWidth = calWidth self.calMonday = calMonday self.tsv = tsv self.calOwnerColor = calOwnerColor self.calEditorColor = calEditorColor self.calContributorColor = calContributorColor self.calReadColor = calReadColor self.calFreeBusyColor = calFreeBusyColor self.dateColor = dateColor self.borderColor = borderColor # authenticate and login to google calendar try: self.gcal.ClientLogin( username=self.username, password=self.password, service='cl', source=__author__+'-'+__program__+'-'+__version__) except Exception, e: PrintErrMsg("Error: " + str(e) + "!\n") sys.exit(1) # get the list of calendars self.allCals = self.gcal.GetAllCalendarsFeed() # gcalcli defined way to order calendars XXX order = { self.ACCESS_OWNER : 1, self.ACCESS_EDITOR : 2, self.ACCESS_ROOT : 3, self.ACCESS_CONTRIBUTOR : 4, self.ACCESS_OVERRIDE : 5, self.ACCESS_RESPOND : 6, self.ACCESS_FREEBUSY : 7, self.ACCESS_READ : 8, self.ACCESS_NONE : 9 } self.allCals.entry.sort(lambda x, y: cmp(order[x.access_level.value], order[y.access_level.value])) for cal in self.allCals.entry: cal.gcalcli_altLink = cal.GetAlternateLink().href match = re.match('^' + self.feedPrefix + '(.*?)/(.*?)/(.*)$', cal.gcalcli_altLink) cal.gcalcli_username = urllib.unquote(match.group(1)) cal.gcalcli_visibility = urllib.unquote(match.group(2)) cal.gcalcli_projection = urllib.unquote(match.group(3)) if len(calNames): for i in xrange(len(calNames)): if re.search(calNames[i].lower(), cal.title.text.lower()): self.cals.append(cal) cal.colorSpec = calNameColors[i] else: self.cals.append(cal) cal.colorSpec = None def _CalendarWithinAccess(self, cal): if self.access == self.ACCESS_ALL: return True elif self.access == self.ACCESS_DEFAULT and \ cal.gcalcli_username == self.username: return True elif self.access != cal.access_level.value: return False else: return True def _CalendarColor(self, cal): if cal == None: return CLR_NRM() elif hasattr(cal, 'colorSpec') and cal.colorSpec != None: return cal.colorSpec elif cal.access_level.value == self.ACCESS_OWNER: return self.calOwnerColor elif cal.access_level.value == self.ACCESS_EDITOR: return self.calEditorColor elif cal.access_level.value == self.ACCESS_CONTRIBUTOR: return self.calContributorColor elif cal.access_level.value == self.ACCESS_FREEBUSY: return self.calFreeBusyColor elif cal.access_level.value == self.ACCESS_READ: return self.calReadColor else: return CLR_NRM() def _TargetCalendar(self): if len(self.cals) == 1: match = re.match('^https://www.google.com(.*)$', self.cals[0].gcalcli_altLink) return match.group(1) else: return '/calendar/feeds/default/private/full' def _ValidTitle(self, title): if title == None: return "(No title)" else: return title def _GetWeekEventStrings(self, cmd, curMonth, startDateTime, endDateTime, eventList): weekEventStrings = [ '', '', '', '', '', '', '' ] for event in eventList: if cmd == 'calm' and curMonth != event.s.strftime("%b"): continue dayNum = int(event.s.strftime("%w")) if self.calMonday: dayNum -= 1 if dayNum < 0: dayNum = 6 if event.s >= startDateTime and event.s < endDateTime: if event.s.hour == 0 and event.s.minute == 0 and \ event.e.hour == 0 and event.e.minute == 0: tmpTimeStr = '' elif self.military: tmpTimeStr = event.s.strftime("%H:%M") else: tmpTimeStr = \ event.s.strftime("%I:%M").lstrip('0') + \ event.s.strftime('%p').lower() # newline and empty string are the keys to turn off coloring weekEventStrings[dayNum] += \ "\n" + \ str(self._CalendarColor(event.gcalcli_cal)) + \ tmpTimeStr.strip() + \ " " + \ self._ValidTitle(event.title.text).strip() return weekEventStrings UNIWIDTH = {'W': 2, 'F': 2, 'N': 1, 'Na': 1, 'H': 1, 'A': 1} def _PrintLen(self, string): printLen = 0 for tmpChar in string: printLen += self.UNIWIDTH[east_asian_width(tmpChar)] return printLen # return print length before cut, cut index, and force cut flag def _NextCut(self, string, curPrintLen): idx = 0 printLen = 0 for tmpChar in string: if (curPrintLen + printLen) >= self.calWidth: return (printLen, idx, True) if tmpChar in (' ', '\n'): return (printLen, idx, False) idx += 1 printLen += self.UNIWIDTH[east_asian_width(tmpChar)] return (printLen, -1, False) def _GetCutIndex(self, eventString): printLen = self._PrintLen(eventString) if printLen <= self.calWidth: DebugPrint("------ printLen=%d (end of string)\n" % printLen) return (printLen, len(eventString)) cutWidth, cut, forceCut = self._NextCut(eventString, 0) DebugPrint("------ cutWidth=%d cut=%d \"%s\"\n" % (cutWidth, cut, eventString)) if forceCut: DebugPrint("--- forceCut cutWidth=%d cut=%d\n" % (cutWidth, cut)) return (cutWidth, cut) DebugPrint("--- looping\n") while cutWidth < self.calWidth: DebugPrint("--- cutWidth=%d cut=%d \"%s\"\n" % (cutWidth, cut, eventString[cut:])) while cut < self.calWidth and \ cut < printLen and \ eventString[cut] == ' ': DebugPrint("-> skipping space <-\n") cutWidth += 1 cut += 1 DebugPrint("--- cutWidth=%d cut=%d \"%s\"\n" % (cutWidth, cut, eventString[cut:])) nextCutWidth, nextCut, forceCut = \ self._NextCut(eventString[cut:], cutWidth) if forceCut: DebugPrint("--- forceCut cutWidth=%d cut=%d\n" % (cutWidth, cut)) break cutWidth += nextCutWidth cut += nextCut if eventString[cut] == '\n': break DebugPrint("--- loop cutWidth=%d cut=%d\n" % (cutWidth, cut)) return (cutWidth, cut) def _GraphEvents(self, cmd, startDateTime, count, eventList): # ignore started events (i.e. that start previous day and end start day) while (len(eventList) and eventList[0].s < startDateTime): eventList = eventList[1:] dayDivider = '' for i in xrange(self.calWidth): dayDivider += '-' weekDivider = '' for i in xrange(7): weekDivider += '+' weekDivider += dayDivider weekDivider += '+' weekDivider = str(self.borderColor) + weekDivider + str(CLR_NRM()) empty = '' for i in xrange(self.calWidth): empty += ' ' dayFormat = '%-' + str(self.calWidth) + '.' + str(self.calWidth) + 's' # Get the localized day names... January 1, 2001 was a Monday dayNames = [ date(2001, 1, i+1).strftime('%A') for i in range(7) ] dayNames = dayNames[6:] + dayNames[:6] dayHeader = str(self.borderColor) + '|' + str(CLR_NRM()) for i in xrange(7): if self.calMonday: if i == 6: dayName = dayFormat % (dayNames[0]) else: dayName = dayFormat % (dayNames[i+1]) else: dayName = dayFormat % (dayNames[i]) dayHeader += str(self.dateColor) + dayName + str(CLR_NRM()) dayHeader += str(self.borderColor) + '|' + str(CLR_NRM()) PrintMsg(CLR_NRM(), "\n" + weekDivider + "\n") if cmd == 'calm': m = startDateTime.strftime('%B %Y') mw = str((self.calWidth * 7) + 6) mwf = '%-' + mw + '.' + mw + 's' PrintMsg(CLR_NRM(), str(self.borderColor) + '|' + str(CLR_NRM()) + str(self.dateColor) + mwf % (m) + str(CLR_NRM()) + str(self.borderColor) + '|' + str(CLR_NRM()) + '\n') PrintMsg(CLR_NRM(), weekDivider + "\n") PrintMsg(CLR_NRM(), dayHeader + "\n") PrintMsg(CLR_NRM(), weekDivider + "\n") curMonth = startDateTime.strftime("%b") # get date range objects for the first week if cmd == 'calm': dayNum = int(startDateTime.strftime("%w")) if self.calMonday: dayNum -= 1 if dayNum < 0: dayNum = 6 startDateTime = (startDateTime - timedelta(days=dayNum)) startWeekDateTime = startDateTime endWeekDateTime = (startWeekDateTime + timedelta(days=7)) for i in xrange(count): # create/print date line line = str(self.borderColor) + '|' + str(CLR_NRM()) for j in xrange(7): if cmd == 'calw': d = (startWeekDateTime + timedelta(days=j)).strftime("%d %b") else: # (cmd == 'calm'): d = (startWeekDateTime + timedelta(days=j)).strftime("%d") if curMonth != (startWeekDateTime + \ timedelta(days=j)).strftime("%b"): d = '' todayMarker = '' if self.now.strftime("%d%b%Y") == \ (startWeekDateTime + timedelta(days=j)).strftime("%d%b%Y"): todayMarker = " **" line += str(self.dateColor) + \ dayFormat % (d + todayMarker) + \ str(CLR_NRM()) + \ str(self.borderColor) + \ '|' + \ str(CLR_NRM()) PrintMsg(CLR_NRM(), line + "\n") weekColorStrings = [ '', '', '', '', '', '', '' ] weekEventStrings = self._GetWeekEventStrings(cmd, curMonth, startWeekDateTime, endWeekDateTime, eventList) # convert the strings to unicode for various string ops for j in xrange(7): weekEventStrings[j] = unicode(weekEventStrings[j], locale.getpreferredencoding()) # get date range objects for the next week startWeekDateTime = endWeekDateTime endWeekDateTime = (endWeekDateTime + timedelta(days=7)) while 1: done = True line = str(self.borderColor) + '|' + str(CLR_NRM()) for j in xrange(7): if weekEventStrings[j] == '': weekColorStrings[j] = '' line += empty + \ str(self.borderColor) + '|' + str(CLR_NRM()) continue if weekEventStrings[j][0] == '\033': # get/skip over color sequence weekColorStrings[j] = '' while (weekEventStrings[j][0] != 'm'): weekColorStrings[j] += weekEventStrings[j][0] weekEventStrings[j] = weekEventStrings[j][1:] weekColorStrings[j] += weekEventStrings[j][0] weekEventStrings[j] = weekEventStrings[j][1:] if weekEventStrings[j][0] == '\n': weekColorStrings[j] = '' weekEventStrings[j] = weekEventStrings[j][1:] line += empty + \ str(self.borderColor) + '|' + str(CLR_NRM()) done = False continue weekEventStrings[j] = weekEventStrings[j].lstrip() printLen, cut = self._GetCutIndex(weekEventStrings[j]) padding = ' ' * (self.calWidth - printLen) line += weekColorStrings[j] + \ weekEventStrings[j][:cut] + \ padding + \ str(CLR_NRM()) weekEventStrings[j] = weekEventStrings[j][cut:] done = False line += str(self.borderColor) + '|' + str(CLR_NRM()) if done: break PrintMsg(CLR_NRM(), line + "\n") PrintMsg(CLR_NRM(), weekDivider + "\n") def _tsv(self, startDateTime, eventList): # tab-separated output for easier shellscripting. # Format: # "Date" "start" "end" "Event-Title" "Location" "Eventdescription" dayFormat = '%F' for event in eventList: tmpDayStr = event.s.strftime(dayFormat) tmpTimeStr = event.s.strftime("%H:%M") tmpTimeStp = event.e.strftime("%H:%M") str = "%s\t%s\t%s\t%s\t%s\t%s" % (tmpDayStr, tmpTimeStr, tmpTimeStp, self._ValidTitle(event.title.text).strip(), event.where[0].value_string, event.content.text ) str2 = "%s\n" % str.replace('\n', '''\\n''') sys.stdout.write(str2) def _PrintEvents(self, startDateTime, eventList): if len(eventList) == 0: PrintMsg(CLR_YLW(), "\nNo Events Found...\n") return dayFormat = '\n%a %b %d' # 10 chars for day indent = ' ' # 10 spaces detailsIndent = ' ' # 19 spaces day = '' for event in eventList: if self.ignoreStarted and (event.s < startDateTime): continue tmpDayStr = event.s.strftime(dayFormat) if self.military: timeFormat = '%-5s' tmpTimeStr = event.s.strftime("%H:%M") else: timeFormat = '%-7s' tmpTimeStr = \ event.s.strftime("%I:%M").lstrip('0').rjust(5) + \ event.s.strftime('%p').lower() prefix = indent if tmpDayStr != day: day = prefix = tmpDayStr PrintMsg(self.dateColor, prefix) if event.s.hour == 0 and event.s.minute == 0 and \ event.e.hour == 0 and event.e.minute == 0: fmt = ' ' + timeFormat + ' %s\n' PrintMsg(self._CalendarColor(event.gcalcli_cal), fmt % ('', self._ValidTitle(event.title.text).strip())) else: fmt = ' ' + timeFormat + ' %s\n' PrintMsg(self._CalendarColor(event.gcalcli_cal), fmt % (tmpTimeStr, self._ValidTitle(event.title.text).strip())) if self.details: clr = CLR_NRM() if event.where[0].value_string: str = "%s Location: %s\n" % (detailsIndent, event.where[0].value_string) PrintMsg(clr, str) diffDateTime = (event.e - event.s) str = "%s Length: %s\n" % (detailsIndent, diffDateTime) PrintMsg(clr, str) # XXX Why does accessing event.when[0].reminder[0] fail? for rem in event.when[0].reminder: remStr = '' if rem.days: remStr += "%s Days" % (rem.days) if rem.hours: if remStr != '': remStr += ' ' remStr += "%s Hours" % (rem.hours) if rem.minutes: if remStr != '': remStr += ' ' remStr += "%s Minutes" % (rem.minutes) str = "%s Reminder: %s\n" % (detailsIndent, remStr) PrintMsg(clr, str) if event.content.text: str = "%s Content: %s\n" % (detailsIndent, event.content.text) PrintMsg(clr, str) def _GetAllEvents(self, cal, feed, end): eventList = [] while 1: next = feed.GetNextLink() for event in feed.entry: event.gcalcli_cal = cal event.s = parse(event.when[0].start_time) if event.s.tzinfo == None: event.s = event.s.replace(tzinfo=tzlocal()) event.e = parse(event.when[0].end_time) if event.e.tzinfo == None: event.e = event.e.replace(tzinfo=tzlocal()) # For all-day events, Google seems to assume that the event time # is based in the UTC instead of the local timezone. Here we # filter out those events start beyond a specified end time. if end and (event.s >= end): continue # http://en.wikipedia.org/wiki/Year_2038_problem # Catch the year 2038 problem here as the python dateutil module # can choke throwing a ValueError exception. If either the start # or end time for an event has a year '>= 2038' dump it. if event.s.year >= 2038 or event.e.year >= 2039: continue eventList.append(event) if not next: break feed = self.gcal.GetCalendarEventFeed(next.href) return eventList def _SearchForCalEvents(self, start, end, searchText): eventList = [] queue = Queue() threads = [] def worker(cal, query): feed = self.gcal.CalendarQuery(query) queue.put((cal, feed)) for cal in self.cals: if not self._CalendarWithinAccess(cal): continue # see http://code.google.com/apis/calendar/reference.html if not searchText: query = CalendarEventQuery(cal.gcalcli_username, cal.gcalcli_visibility, cal.gcalcli_projection) query.start_min = start.isoformat() query.start_max = end.isoformat() else: query = CalendarEventQuery(cal.gcalcli_username, cal.gcalcli_visibility, cal.gcalcli_projection, searchText) if start: # flagged by --ignore-started # weeds out old but still pulls in started events query.futureevents = 'true' query.singleevents = 'true' # we sort later after getting events from all calendars #query.orderby = 'starttime' #query.sortorder = 'ascending' th = threading.Thread(target=worker, args=(cal, query)) threads.append(th) th.start() for th in threads: th.join() while not queue.empty(): cal, feed = queue.get() eventList.extend(self._GetAllEvents(cal, feed, end)) eventList.sort(lambda x, y: cmp(x.s, y.s)) return eventList def ListAllCalendars(self): accessLen = 0 for cal in self.allCals.entry: length = len(cal.access_level.value) if length > accessLen: accessLen = length if accessLen < len('Access'): accessLen = len('Access') format = ' %0' + str(accessLen) + 's %s\n' PrintMsg(CLR_BRYLW(), "\n" + format % ('Access', 'Title')) PrintMsg(CLR_BRYLW(), format % ('------', '-----')) for cal in self.allCals.entry: PrintMsg(self._CalendarColor(cal), format % (cal.access_level.value, cal.title.text)) def TextQuery(self, searchText=''): # the empty string would get *ALL* events... if searchText == '': return if self.ignoreStarted: start = self.now # flags gdata futureevents to true else: start = None # convert now to midnight this morning and use for default defaultDateTime = self.now.replace(hour=0, minute=0, second=0, microsecond=0) eventList = \ self._SearchForCalEvents(start, None, searchText) self._PrintEvents(self.now, eventList) def AgendaQuery(self, startText='', endText=''): if self.ignoreStarted: defaultDateTime = self.now else: # convert now to midnight this morning and use for default defaultDateTime = self.now.replace(hour=0, minute=0, second=0, microsecond=0) if startText == '': start = defaultDateTime else: try: start = parse(startText, default=defaultDateTime) except: PrintErrMsg('Error: failed to parse start time\n') return if endText == '': end = (start + timedelta(days=self.agendaLength)) else: try: end = parse(endText, default=defaultDateTime) except: PrintErrMsg('Error: failed to parse end time\n') return eventList = self._SearchForCalEvents(start, end, None) if self.tsv: self._tsv(start, eventList) else: self._PrintEvents(start, eventList) def CalQuery(self, cmd, startText='', count=1): # convert now to midnight this morning and use for default defaultDateTime = self.now.replace(hour=0, minute=0, second=0, microsecond=0) if startText == '': start = defaultDateTime else: try: start = parse(startText, default=defaultDateTime) start = start.replace(hour=0, minute=0, second=0, microsecond=0) except: PrintErrMsg('Error: failed to parse start time\n') return # convert start date to the beginning of the week or month if cmd == 'calw': dayNum = int(start.strftime("%w")) if self.calMonday: dayNum -= 1 if dayNum < 0: dayNum = 6 start = (start - timedelta(days=dayNum)) end = (start + timedelta(days=(count * 7))) else: # cmd == 'calm': start = (start - timedelta(days=(start.day - 1))) endMonth = (start.month + 1) endYear = start.year if endMonth == 13: endMonth = 1 endYear += 1 end = start.replace(month=endMonth, year=endYear) daysInMonth = (end - start).days offsetDays = int(start.strftime('%w')) if self.calMonday: offsetDays -= 1 if offsetDays < 0: offsetDays = 6 totalDays = (daysInMonth + offsetDays) count = (totalDays / 7) if totalDays % 7: count += 1 eventList = self._SearchForCalEvents(start, end, None) self._GraphEvents(cmd, start, count, eventList) def QuickAdd(self, eventText): if eventText == '': return quickEvent = gdata.calendar.CalendarEventEntry() quickEvent.content = atom.Content(text=eventText) quickEvent.quick_add = gdata.calendar.QuickAdd(value='true') try: self.gcal.InsertEvent(quickEvent, self._TargetCalendar()) except Exception, e: PrintErrMsg("Error: " + e["reason"] + "\n") sys.exit(1) def Remind(self, minutes=10, command=None): if command == None: command = self.command # perform a date query for now + minutes + slip start = self.now end = (start + timedelta(minutes=(minutes + 5))) eventList = self._SearchForCalEvents(start, end, None) message = '' for event in eventList: # skip this event if it already started # XXX maybe add a 2+ minute grace period here... if event.s < self.now: continue if self.military: tmpTimeStr = event.s.strftime('%H:%M') else: tmpTimeStr = \ event.s.strftime('%I:%M').lstrip('0') + \ event.s.strftime('%p').lower() message += '%s %s\n' % \ (tmpTimeStr, self._ValidTitle(event.title.text).strip()) if message == '': return cmd = shlex.split(command) for i, a in zip(xrange(len(cmd)), cmd): if a == '%s': cmd[i] = message pid = os.fork() if not pid: os.execvp(cmd[0], cmd) def ImportICS(self, verbose=False, icsFile=None): try: import vobject except: PrintErrMsg('Python vobject module not installed!\n') sys.exit(1) f = sys.stdin if icsFile: try: f = file(icsFile) except Exception, e: PrintErrMsg("Error: " + str(e) + "!\n") sys.exit(1) while True: try: v = vobject.readComponents(f).next() except StopIteration: break ve = v.vevent event = gdata.calendar.CalendarEventEntry() if hasattr(ve, 'summary'): DebugPrint("SUMMARY: %s\n" % ve.summary.value) if verbose: print "Event........%s" % ve.summary.value event.title = atom.Title(text=ve.summary.value) if hasattr(ve, 'location'): DebugPrint("LOCATION: %s\n" % ve.location.value) if verbose: print "Location.....%s" % ve.location.value event.where = gdata.calendar.Where(value_string=ve.location.value) if not hasattr(ve, 'dtstart') or not hasattr(ve, 'dtend'): PrintErrMsg("Error: event does not have a dtstart and dtend!\n") continue DebugPrint("DTSTART: %s\n" % ve.dtstart.value.isoformat()) DebugPrint("DTEND: %s\n" % ve.dtend.value.isoformat()) if verbose: print "Start........%s" % ve.dtstart.value.isoformat(' ') print "End..........%s" % ve.dtend.value.isoformat(' ') print "Local Start..%s" % ve.dtstart.value.astimezone(tzlocal()) print "Local End....%s" % ve.dtend.value.astimezone(tzlocal()) if hasattr(ve, 'rrule'): DebugPrint("RRULE: %s\n" % ve.rrule.value) if verbose: print "Recurrence...%s" % ve.rrule.value # XXX Need to print a NICE recurrence string #rr = rrulestr(ve.rrule.value) #print dir(rr) # # In order to add an RRULE using a DTSTART and DTEND in the # local timezone, there needs to be a TIMEZONE section in the # recurrence field. Since that is a pain and I'm lazy... as # a workaround I convert the DTSTART and DTEND to UTC. Google # handles this properly and keys off the timezone setting of # the calendar being added to. The event will be shown at the # correct local time. :-) # if False: # A TIMEZONE section is needed for this to work XXX recurrence = \ "DTSTART;TZID=" + \ ve.dtstart.value.tzinfo._tzid + ":" + \ ve.dtstart.value.strftime('%Y%m%dT%H%M%S') + \ '\r\n' + \ "DTEND;TZID=" + \ ve.dtend.value.tzinfo._tzid + ":" + \ ve.dtend.value.strftime('%Y%m%dT%H%M%S') + \ '\r\n' + \ "RRULE:" + ve.rrule.value + '\r\n' else: ve.dtstart.value -= ve.dtstart.value.utcoffset() ve.dtstart.value = ve.dtstart.value.replace(tzinfo=None) ve.dtend.value -= ve.dtend.value.utcoffset() ve.dtend.value = ve.dtend.value.replace(tzinfo=None) recurrence = \ "DTSTART:" + \ ve.dtstart.value.strftime('%Y%m%dT%H%M%S') + \ '\r\n' + \ "DTEND:" + \ ve.dtend.value.strftime('%Y%m%dT%H%M%S') + \ '\r\n' + \ "RRULE:" + ve.rrule.value + '\r\n' DebugPrint("RECURRENCE:\n%s\n" % recurrence) event.recurrence = \ gdata.calendar.Recurrence(text=recurrence) elif hasattr(ve, 'dtstart') and hasattr(ve, 'dtend'): start = ve.dtstart.value.isoformat() end = ve.dtend.value.isoformat() event.when = gdata.calendar.When(start_time=start, end_time=end) if hasattr(ve, 'description'): DebugPrint("DESCRIPTION: %s\n" % ve.description.value) if verbose: print "Description:\n%s" % ve.description.value event.content = atom.Content(text=ve.description.value) if not verbose: try: self.gcal.InsertEvent(event, self._TargetCalendar()) except Exception, e: PrintErrMsg("Error: " + e["reason"] + "\n") sys.exit(1) continue PrintMsg(CLR_YLW(), "[i]mport [s]kip [q]uit: ") val = raw_input() if val == 'i': try: self.gcal.InsertEvent(event, self._TargetCalendar()) except Exception, e: PrintErrMsg("Error: " + e["reason"] + "\n") sys.exit(1) elif val == 's': continue elif val == 'q': sys.exit(0) else: PrintErrMsg('Error: invalid input\n') sys.exit(1) def LoadConfig(configFile): config = RawConfigParser() config.read(os.path.expanduser(configFile)) return config def GetConfig(config, key, default): try: value = config.get('gcalcli', key) except: value = default if value and value.startswith('`'): # Value is a shell command cmd = value.strip()[1:-1] parts = [] for part in cmd.split(): parts.append(os.path.expanduser(part)) value = subprocess.check_output(parts).strip() return value def GetConfigMultiple(config, key, default): try: values = config.get('gcalcli', key) except: values = default if values == None: return [ None ] valueList = csv.reader([ values ], delimiter=',', quotechar='"', skipinitialspace=True).next() return valueList def GetTrueFalse(value): if value.lower() == 'false': return False else: return True def GetColor(value, exitFlag): colors = { 'default' : CLR_NRM(), 'black' : CLR_BLK(), 'brightblack' : CLR_BRBLK(), 'red' : CLR_RED(), 'brightred' : CLR_BRRED(), 'green' : CLR_GRN(), 'brightgreen' : CLR_BRGRN(), 'yellow' : CLR_YLW(), 'brightyellow' : CLR_BRYLW(), 'blue' : CLR_BLU(), 'brightblue' : CLR_BRBLU(), 'magenta' : CLR_MAG(), 'brightmagenta' : CLR_BRMAG(), 'cyan' : CLR_CYN(), 'brightcyan' : CLR_BRCYN(), 'white' : CLR_WHT(), 'brightwhite' : CLR_BRWHT() } try: return colors[value] except: if exitFlag: PrintErrMsg('Error: invalid color name\n') sys.exit(1) else: return None def GetCalColors(calNames): calNameColors = [] for idx in xrange(len(calNames)): i = calNames[idx].rfind('#') if i != -1: c = GetColor(calNames[idx][(i+1):], False) if c: calNameColors.append(c) calNames[idx] = calNames[idx][:i] else: calNameColors.append(None) else: calNameColors.append(None) return calNames, calNameColors def BowChickaWowWow(): try: opts, args = getopt.getopt(sys.argv[1:], "", ["help", "version", "config=", "user=", "pw=", "cals=", "cal=", "24hr", "details", "tsv", "ignore-started", "width=", "mon", "nc", "cal-owner-color=", "cal-editor-color=", "cal-contributor-color=", "cal-read-color=", "cal-freebusy-color=", "date-color=", "border-color="]) except getopt.error: sys.exit(1) configFile = '~/.gcalclirc' # look for config file override then load the config file # we do this first because command line args take precedence for opt, arg in opts: if opt == "--config": configFile = arg cfg = LoadConfig(configFile) usr = GetConfig(cfg, 'user', None) pwd = GetConfig(cfg, 'pw', None) access = GetConfig(cfg, 'cals', 'all') calNames = GetConfigMultiple(cfg, 'cal', None) military = GetTrueFalse(GetConfig(cfg, '24hr', 'false')) details = GetTrueFalse(GetConfig(cfg, 'details', 'false')) ignoreStarted = GetTrueFalse(GetConfig(cfg, 'ignore-started', 'false')) calWidth = int(GetConfig(cfg, 'width', '10')) calMonday = GetTrueFalse(GetConfig(cfg, 'mon', 'false')) tsv = GetTrueFalse(GetConfig(cfg, 'tsv', 'false')) calOwnerColor = \ GetColor(GetConfig(cfg, 'cal-owner-color', 'cyan'), True) calEditorColor = \ GetColor(GetConfig(cfg, 'cal-editor-color', 'green'), True) calContributorColor = \ GetColor(GetConfig(cfg, 'cal-contributor-color', 'default'), True) calReadColor = \ GetColor(GetConfig(cfg, 'cal-read-color', 'magenta'), True) calFreeBusyColor = \ GetColor(GetConfig(cfg, 'cal-freebusy-color', 'default'), True) dateColor = \ GetColor(GetConfig(cfg, 'date-color', 'yellow'), True) borderColor = \ GetColor(GetConfig(cfg, 'border-color', 'white'), True) # fix wokCalNames when not specified in config file if len(calNames) == 1 and calNames[0] == None: calNames = [] calNameColors = [] # Process options for opt, arg in opts: if opt == "--help": Usage() if opt == "--version": Version() elif opt == "--user": usr = arg elif opt == "--pw": pwd = arg elif opt == "--cals": access = arg elif opt == "--cal": calNames.append(arg) elif opt == "--24hr": military = True elif opt == "--details": details = True elif opt == "--ignore-started": ignoreStarted = True elif opt == "--width": calWidth = int(arg) elif opt == "--mon": calMonday = True elif opt == "--nc": CLR.useColor = False elif opt == "--cal-owner-color": calOwnerColor = GetColor(arg, True) elif opt == "--cal-editor-color": calEditorColor = GetColor(arg, True) elif opt == "--cal-contributor-color": calContributorColor = GetColor(arg, True) elif opt == "--cal-read-color": calReadColor = GetColor(arg, True) elif opt == "--cal-freebusy-color": calFreeBusyColor = GetColor(arg, True) elif opt == "--date-color": dateColor = GetColor(arg, True) elif opt == "--border-color": borderColor = GetColor(arg, True) elif opt == "--tsv": tsv = True if usr == None: PrintErrMsg('Error: must specify a username\n') sys.exit(1) try: if pwd == None: pwd = getpass.getpass("Password: ") except Exception, e: PrintErrMsg("Error: " + str(e) + "!\n") sys.exit(1) if pwd == None or pwd == '': PrintErrMsg('Error: must specify a password\n') sys.exit(1) if len(args) == 0: PrintErrMsg('Error: no command (--help)\n') sys.exit(1) calNames, calNameColors = GetCalColors(calNames) gcal = gcalcli(username=usr, password=pwd, access=access, calNames=calNames, calNameColors=calNameColors, military=military, details=details, ignoreStarted=ignoreStarted, calWidth=calWidth, calMonday=calMonday, calOwnerColor=calOwnerColor, calEditorColor=calEditorColor, calContributorColor=calContributorColor, calReadColor=calReadColor, calFreeBusyColor=calFreeBusyColor, dateColor=dateColor, borderColor=borderColor, tsv=tsv) if args[0] == 'list': gcal.ListAllCalendars() elif args[0] == 'search': if len(args) != 2: PrintErrMsg('Error: invalid search string\n') sys.exit(1) # allow unicode strings for input gcal.TextQuery(unicode(args[1], locale.getpreferredencoding())) sys.stdout.write('\n') elif args[0] == 'agenda': if len(args) == 3: # start and end gcal.AgendaQuery(startText=args[1], endText=args[2]) elif len(args) == 2: # start gcal.AgendaQuery(startText=args[1]) elif len(args) == 1: # defaults gcal.AgendaQuery() else: PrintErrMsg('Error: invalid agenda arguments\n') sys.exit(1) if not tsv: sys.stdout.write('\n') elif args[0] == 'calw': if not calWidth: PrintErrMsg('Error: invalid width, don\'t be an idiot!\n') sys.exit(1) if len(args) >= 2: try: count = int(args[1]) except: PrintErrMsg('Error: invalid calw arguments\n') sys.exit(1) if len(args) == 3: # weeks and start gcal.CalQuery(args[0], count=int(args[1]), startText=args[2]) elif len(args) == 2: # weeks gcal.CalQuery(args[0], count=int(args[1])) elif len(args) == 1: # defaults gcal.CalQuery(args[0]) else: PrintErrMsg('Error: invalid calw arguments\n') sys.exit(1) sys.stdout.write('\n') elif args[0] == 'calm': if not calWidth: PrintErrMsg('Error: invalid width, don\'t be an idiot!\n') sys.exit(1) if len(args) == 2: # start gcal.CalQuery(args[0], startText=args[1]) elif len(args) == 1: # defaults gcal.CalQuery(args[0]) else: PrintErrMsg('Error: invalid calm arguments\n') sys.exit(1) sys.stdout.write('\n') elif args[0] == 'quick': if len(args) != 2: PrintErrMsg('Error: invalid event text\n') sys.exit(1) # allow unicode strings for input gcal.QuickAdd(unicode(args[1], locale.getpreferredencoding())) elif args[0] == 'remind': if len(args) == 3: # minutes and command gcal.Remind(int(args[1]), args[2]) elif len(args) == 2: # minutes gcal.Remind(int(args[1])) elif len(args) == 1: # defaults gcal.Remind() else: PrintErrMsg('Error: invalid remind arguments\n') sys.exit(1) elif args[0] == 'import': args = args[1:] verbose = False if len(args) >= 1 and args[0] == "-v": verbose = True args = args[1:] if len(args) == 0: # stdin gcal.ImportICS(verbose) elif len(args) == 1: # ics file gcal.ImportICS(verbose, args[0]) else: PrintErrMsg('Error: invalid import arguments\n') sys.exit(1) else: PrintErrMsg('Error: unknown command (--help)\n') sys.exit(1) if __name__ == '__main__': BowChickaWowWow()