#!/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 # # 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 config file to read (default is '~/.gcalclirc') --user google username --pw 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 [#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 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 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 [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 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 execute command if event occurs within minutes time ('%s' in is replaced with event start time and title text) - 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()