You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1582 lines
54 KiB
Python
1582 lines
54 KiB
Python
#!/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()
|
|
|