#!/usr/bin/env python # # $Id$ # # rastatodo - text based todo list # ''' rastatodo - text based todo list You'll need a .todo file (default location is $HOME/.todo; the -f option selects a file). Each line of this file should be: - blank - A comment (line starts with a #; ignored by the program) - A category (just a name in square brackets) - A todo item (format described later) A sample .todo file is within the cut lines below: --8<-- c0 always today (this doesn't have a category) c1 always tomorrow (this doesn't have a category either) [CS101 ] t 2008-03-03 week 1 lab report t 2008-03-10 week 2 cal report [CS134 ] c0 see lecturer about something asap t 2008-03-15 assignment 1 [CS123 ] a2 2008-03-05 12:30 appointment with lecturer about project t 2008-03-15 project report 1 due [new types] a3 2008-03-04 doctor's appointment s0 2008-03-02 do backups (this shows up only on the day) c2 fix something soon w fix something whenever [birthdays] a5 2008-14-11 Alice's birthday a5 2008-14-11 Bob's birthday --8<-- Todo item Format: <[priority]> <[date YYYY-MM-DD]> \n Some types do not have a priority, some don't have a date, some have both or neither. Item types are represented by a single char: t - Todo - No priority, simply has a date by which it must be done. Equivalent to the old todo.c usage. s - Sleeping - Priority is the number of days proximity before it is shown in the output. a - Appointment - These are handled the same as sleeping items, but as you can filter on type this lets you easily show only appointments in the output. c - Constant - No date, the priority is number of days away this item is "due". w - Wishlist - No priority, No date. Wishlist items are effectively infinity days away but are always shown (unless turned off with the exclude types = wishlist option). Commandline options: Use the -h option to the script for a full list. You can use a partial long argument as long as it is unique (i.e. "--app" or even "--ap" instead of "--appointments" -f FILE, --file=FILE File to parse (defaults to $HOME/.todo) -e, --edit Invoke your $EDITOR on todo file -r, --reverse Reversed order of sorting --mono Monochrome output --sort-cat Group by category --all Shows all items, regardless of date and filtering -d DAYS, --days=DAYS Days after which item will not be included --only-types=ONLY_TYPES Only include these types (string of letters) --appointments Shows appointments only (equivalent to --only-types=a --ex-types=EX_TYPES Exclude these types (string of letters) --only-cat=ONLY_CAT Only include these categories (comma delimited) --ex-cat=EX_CAT Exclude these categories (comma delimited) ''' # # ----- # # Copyright (c) 2004-2009 Dylan Leigh. # # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # 3. Modified versions must be plainly marked as such, and must not be # misrepresented as being the original software. # # 4. The names of the authors or copyright holders must not be used to # endorse or promote products derived from this software without # prior written permission. # # 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, EVEN IF ADVISED # OF THE POSSIBILITY OF SUCH DAMAGE. # # ----- # # Design notes: # # Todo items have the following attributes: # - Type (character or constant) # - Priority (positive integer, the precise meaning of this field # depends on type, for some types it does not appear) # For the other types that currently use it, this # indicates the number of days away when this should # appear on the list. # - Date. In the .todo file this is in YYYY MM DD format. For # some types this does not appear. # - Description (string) # - Category (string) (not in item line, derived from category in # previous lines) # - Days away (derived from type, date and/or priority) # # # For each valid type of item, there must be: # - an entry in default validTypes # - a regex for the line entry # - a section in parseTodoLine for the type # # TODO: - Priorities setting colours early? # - Handle white backgrounds neatly # - Group by category with original file order # - Better factoring on filter arguments # - Implement p (pending) and f (followup) items import os, sys, re, optparse import datetime # Program defaults and environment variables # Constants are in UPPERCASE. EDITOR=os.getenv('EDITOR', default='vim') HOMEDIR=os.getenv('HOME') DEFAULTTODOFILE = "%s/.todo"%HOMEDIR # Default for help todofname = DEFAULTTODOFILE today = datetime.date.today() validTypes = 'tsacw' useColours = True # ANSI colours # these are all with black background (40) ANSI_RED ="\033[0;31m" ANSI_GREEN ="\033[0;32m" ANSI_YELLOW ="\033[0;33m" ANSI_BLUE ="\033[0;34m" ANSI_MAGENTA="\033[0;35m" ANSI_CYAN ="\033[0;36m" ANSI_WHITE ="\033[0;37m" # return to normal - use at end of output! ANSI_NORMAL ="\033[0m" #Filtering items (XXX: defaults) daysCutoff = 22 # Days away to display items onlyTypes = validTypes onlyCategories = None # If none, dont filter on this exCategories = None # If none, dont filter on this # if only and ex are specified only use only # Classes class TodoItem (object): # Every todo item has a description and type # If cat/days/date are not given, we do not use or display. def __init__(self, type, desc, \ category=None, days=None, date=None, wake=None): self.type = type # validation TODO self.desc = desc self.category = category self.date = date self.wake = wake self.days = days # end of constructor def daysAway(self): # for wishlist items without a date, fudges days = the # cutoff so they are filtered and sorted properly. if (self.days is None): return daysCutoff else: return int(self.days) def prettyPrint(self, showType=True): # XXX: types on for debugging preamble = "" # For colours and status if (useColours): if (self.days is None): preamble = ANSI_BLUE elif (self.days > 4): preamble = ANSI_GREEN elif (self.days > 0): preamble = ANSI_YELLOW elif (self.days == 0): preamble = ANSI_MAGENTA else: preamble = ANSI_RED if (showType): preamble="%s%s "%(preamble, self.type) # add date and days to preamble? XXX if (self.date is None): date = ' ' elif (self.type == 'a'): date = '%02d-%02d (%s) '%(self.date.month, \ self.date.day, self.date.strftime('%a')) else: date = '%02d-%02d'%(self.date.month, self.date.day) if (self.days is None): days = ' ' else: days = '[%02d]'%self.days if (useColours): if (self.category is None): print '%s%s %s %s%s'%\ (preamble, days, date, self.desc, ANSI_NORMAL) else: print '%s%s %s [%s] %s%s'%(preamble, days, \ date, self.category, self.desc, ANSI_NORMAL) else: if (self.category is None): print '%s%s %s %s'%\ (preamble, days, date, self.desc) else: print '%s%s %s [%s] %s'%(preamble, days, \ date, self.category, self.desc) # end prettyPrint # end TodoItem class # Regexes for parsing lines regexT = re.compile(r'[Tt]\s+(\d{4}-\d{2}-\d{2})\s+(.+)') regexS = re.compile(r'[Ss](\d+)\s+(\d{4}-\d{2}-\d{2})\s+(.+)') regexA = re.compile(r'[Aa](\d+)\s+(\d{4}-\d{2}-\d{2})\s+(.+)') regexC = re.compile(r'[Cc](\d+)\s+(.+)') regexW = re.compile(r'[Ww]\s+(.+)') regexP = re.compile(r'[Pp]\s+(\d{4}-\d{2}-\d{2})\s+(.+)') regexF = re.compile(r'[Ff]\s+(\d{4}-\d{2}-\d{2})\s+(.+)') # Standalone functions def parseISODate(s): '''Given a string in ISO 8602 format (yyyy-mm-dd), returns a date object representing the date (see datetime module) Throws TypeError or ValueError on bad format''' (y,m,d) = s.split('-') return datetime.date(int(y),int(m),int(d)) def parseTodoLine(line, category=None): '''Takes a single line string (and optionally the current categoy); returns a todo item or None if it is invalid.''' # Determine type of line if (line[0] == 't'): # Todo item mat = regexT.match(line) if (mat): date = parseISODate(mat.group(1)) desc = mat.group(2) days = (date - today).days return TodoItem('t', desc, category, days, date) else: return None elif (line[0] == 's'): # 'Sleeping' item mat = regexS.match(line) if (mat): wake = int(mat.group(1)) date = parseISODate(mat.group(2)) desc = mat.group(3) days = (date - today).days return TodoItem('s', desc, category, days, date, wake) else: return None elif (line[0] == 'a'): # Appointments mat = regexA.match(line) if (mat): wake = int(mat.group(1)) date = parseISODate(mat.group(2)) desc = mat.group(3) days = (date - today).days return TodoItem('a', desc, category, days, date, wake) else: return None elif (line[0] == 'c'): # Constant mat = regexC.match(line) if (mat): days = int(mat.group(1)) desc = mat.group(2) return TodoItem('c', desc, category, days) else: return None elif (line[0] == 'w'): # Wishlist mat = regexW.match(line) if (mat): desc = mat.group(1) return TodoItem('w', desc, category) else: return None else: # no recognized type return None def todoInclude(item): '''Returns true if the todo item should be included based on the global options. Otherwise returns false.''' # Filter items if (not cliopts.all): if (item.wake is not None): if (item.wake < item.daysAway()): return False if (item.daysAway() > daysCutoff): return False # type of item if (onlyTypes.find(item.type) == -1): return False # category if (onlyCategories is not None): if (item.category not in onlyCategories): return False else: if (exCategories is not None): if (item.category in exCategories): return False # end if not cliopts.all return True def parseTodoFile(file): '''Takes a file-like object, returns a list containing filtered but unsorted todo objects''' ret = [] category = None linecount = 0 for line in file: linecount += 1 if (line == "" or line.isspace() or line[0] == '#'): continue # skip blanks and comments # handle categories if (line[0] == '['): category=line.lstrip('[').rstrip(']\n') else: # try parsing as a todo line todoitem = parseTodoLine(line, category) if (todoitem): if (todoInclude(todoitem)): ret.append(todoitem) else: print "Syntax error at line", linecount # end for line in file return ret if (__name__ == '__main__'): # Parse commandline arguments optparser = optparse.OptionParser() optparser.add_option('-f', '--file', \ help='File to parse (defaults to %s)'%DEFAULTTODOFILE) optparser.add_option('-e', '--edit', action='store_true', \ help='Invoke your EDITOR (%s) on todo file.'%EDITOR) optparser.add_option('-r', '--reverse', action='store_true', \ help='Reversed order of sorting.') optparser.add_option('--mono', action='store_true', \ dest='monochrome', help='Monochrome output') optparser.add_option('--sort-cat', action='store_true', \ help='Group by category') optparser.add_option('--all', action='store_true', \ help='Shows all items, regardless of date and filtering') optparser.add_option('-d', '--days', \ help='Days after which item will not be included') # Don't use store_const for only-types for if-else later XXX optparser.add_option('--only-types', \ help='Only include these types (string of letters)') optparser.add_option('--appointments', \ action='store_const', const='a', dest='onlyTypes', \ help='Shows appointments only (equivalent to --only-types=a') optparser.add_option('--ex-types', \ help='Exclude these types (string of letters)') optparser.add_option('--only-cat', \ help='Only include these categories (comma delimited)') optparser.add_option('--ex-cat', \ help='Exclude these categories (comma delimited)') (cliopts, cliargs) = optparser.parse_args() # XXX: optparser has cyclic refs - destroy ? # Check file argument first if (cliopts.file): todofname = cliopts.file if (not os.access(todofname, os.F_OK)): sys.exit("%s does not exist; use the -f option to specify a todo file"%todofname) if (not os.access(todofname, os.R_OK)): sys.exit("%s is not readable."%todofname) # If edit mode, send to defined editor then die. if (cliopts.edit): os.execlp(EDITOR, (todofname,)) # replaces this process # Determine any cutoff dates, categories or types to be # excluded beforehand so that we don't include those items when # loading from the file. XXX if (cliopts.days): daysCutoff = int(cliopts.days) if (cliopts.only_cat): onlyCategories = cliopts.only_cat.split(',') else: if (cliopts.ex_cat): exCategories = cliopts.ex_cat.split(',') if (cliopts.only_types): onlyTypes = cliopts.only_types else: if (cliopts.ex_types): for type in cliopts.ex_types: onlyTypes = onlyTypes.replace(type, '') # end if only types # Open the file and give it to the parsing function. todoFile = open(todofname) todoList = parseTodoFile(todoFile) todoFile.close() # Sort todoList.sort(key=lambda x: x.daysAway(), \ reverse=not cliopts.reverse) if (cliopts.sort_cat): todoList.sort(key=lambda x: x.category) # Display items if (cliopts.monochrome): useColours = False; for item in todoList: item.prettyPrint() #if (item.type == 's'): # sys.exit('imitation crash!') # end if main