From 8e366e7a313bb8ac07420934c3b74483674cd51d Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Sat, 22 Dec 2012 16:29:00 -0600 Subject: [PATCH] Adding gcalcli to dotfiles and ~/dotfiles/bin to my PATH.. not sure if I like this yet. --- bin/gcalcli | 1581 +++++++++++++++++++++++++++++++++++++++++++++++++++ zshrc | 1 + 2 files changed, 1582 insertions(+) create mode 100755 bin/gcalcli diff --git a/bin/gcalcli b/bin/gcalcli new file mode 100755 index 0000000..c78975e --- /dev/null +++ b/bin/gcalcli @@ -0,0 +1,1581 @@ +#!/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() + diff --git a/zshrc b/zshrc index 9eb710a..111a62b 100644 --- a/zshrc +++ b/zshrc @@ -52,3 +52,4 @@ export PATH=$HOME/.rbenv/bin:$PATH # rbenv export PATH=$HOME/.gem/ruby/1.9.1/bin:$PATH # Ruby export PATH=/Applications/Postgres.app/Contents/MacOS/bin:$PATH # Postgres export PATH=$HOME/Library/Haskell/bin:$PATH # Haskell +export PATH=$HOME/dotfiles/bin:$PATH # Dotfiles bin