work in progress delicious import and path fix
[gitology.git] / ikog
blobd3bffe115b61282b14155c168bb87b432fcf62b8
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 # Run the script for details of the licence
5 # or refer to the notice section later in the file.
6 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7 #!<^DATA
8 # create new website design template @Anywhere :pDesign :created2009-01-06 :d2009-01-06
9 # rss feeds for labels @Anywhere :pDjango :created2009-01-01
10 # meta blog all, this will contain posts across all blogs @Anywhere :pDjango :created2009-01-03
11 # flexible blog, +/- blog/labels, multi filters. @Anywhere :pDjango :created2009-01-03
12 # blog view permission @Anywhere :pDjango :created2009-01-03
13 # encrypted documents support, index.enc. @Anywhere :pCore :created2009-01-03
14 # login page with yahoo/gmail logo along with openid textinput @Anywhere :pDesign :created2009-01-07
15 # utilities to find ancestory in wiki pages @Anywhere :pCore :created2009-01-07
16 # user info store indexed on openid @Anywhere :pCore :created2009-01-07
17 # user groups, multi user in one group @Anywhere :pCore :created2009-01-07
18 # permissions based on groups and individuals, on blog/wiki/document basis @Anywhere :pCore :created2009-01-07
19 #!<^CONFIG
20 cfgColor = 1
21 cfgAutoSave = True
22 cfgReviewMode = False
23 cfgSysCalls = False
24 cfgEditorNt = "edit"
25 cfgEditorPosix = "nano,pico,vim,emacs"
26 cfgShortcuts = ['', '', '', '', '', '', '', '', '', '']
27 cfgAbbreviations = {'@C': '@Computer', '@A': '@Anywhere', '@Pw': '@Password', '@D': '@Desk', '@E': '@Errands', '@H': '@Home', '@I': '@Internet', '@N': '@Next', '@O': '@Other', '@L': '@Lunch', '@M': '@Meeting', '@S': '@Someday/Maybe', '@P': '@Phone', '@W': '@Work', '@W4': '@Waiting_For'}
28 cfgPAbbreviations = {':Pw': ':pWebsite'}
29 #!<^CODE
30 import sys
31 import os
32 import re
33 from datetime import date
34 from datetime import timedelta
35 import platform
36 import urllib
37 import getpass
38 from md5 import md5
39 import struct
40 import tempfile
41 from threading import Timer
42 import stat
44 supportAes = True
45 try:
46 import pyRijndael
47 except:
48 supportAes = False
50 try:
51 import readline
52 except:
53 pass
55 notice = [
56 "ikog.py v 1.88 2007-11-23",
57 "Copyright (C) 2006-2007 S. J. Butler",
58 "Visit http://www.henspace.co.uk for more information.",
59 "This program is free software; you can redistribute it and/or modify",
60 "it under the terms of the GNU General Public Licence as published by",
61 "the Free Software Foundation; either version 2 of the License, or",
62 "(at your option) any later version.",
63 "",
64 "This program is distributed in the hope that it will be useful,",
65 "but WITHOUT ANY WARRANTY; without even the implied warranty of",
66 "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the",
67 "GNU General Public License for more details. The license is available",
68 "from http://www.gnu.org/licenses/gpl.txt"
71 banner = [
72 " _ _ ",
73 " (_) | | __ ___ __ _",
74 " | | | |/ / / _ \ / _` |",
75 " | | | < | (_) | | (_| |",
76 " |_| |_|\_\ \___/ \__, |",
77 " _ _ _ |___/",
78 " (_) | |_ | | __ ___ ___ _ __ ___ ___ _ __",
79 " | | | __| | |/ / / _ \ / _ \ | '_ \ / __| / _ \ | '_ \ ",
80 " | | | |_ | < | __/ | __/ | |_) | \__ \ | (_) | | | | |",
81 " |_| \__| |_|\_\ \___| \___| | .__/ |___/ \___/ |_| |_|",
82 " |_| _",
83 " __ _ _ __ ___ __ __ (_) _ __ __ _ ",
84 " / _` | | '__| / _ \ \ \ /\ / / | | | '_ \ / _` |",
85 " | (_| | | | | (_) | \ V V / | | | | | | | (_| | _",
86 " \__, | |_| \___/ \_/\_/ |_| |_| |_| \__, | (_)",
87 " |___/ |___/",
92 magicTag = "#!<^"
93 gMaxLen = 80
94 try:
95 ruler = "~".ljust(gMaxLen - 1, "~")
96 divider = "_".ljust(gMaxLen - 1, "_")
97 except Exception:
98 print "Error found. Probably wrong version of Python"
100 gReqPythonMajor = 2
101 gReqPythonMinor = 4
104 def safeRawInput(prompt):
105 try:
106 entry = raw_input(prompt)
107 except:
108 print "\n"
109 entry = ""
110 return entry
112 ### global compare function
113 def compareTodo(a, b):
114 return cmp(a.getEffectivePriority(), b.getEffectivePriority())
116 def printError(msg):
117 print gColor.code("error") + "ERROR: " + msg + gColor.code("normal")
120 def clearScreen(useSys = False):
121 if useSys:
122 if os.name == "posix":
123 os.system("clear")
124 elif os.name in ("dos", "ce", "nt"):
125 os.system("cls")
126 print "\n"*25
127 for l in banner:
128 print l
130 ### XTEA algorithm public domain
131 class Xtea:
132 def __init__(self):
133 pass
135 def crypt(self, key,data,iv='\00\00\00\00\00\00\00\00',n=32):
136 def keygen(key,iv,n):
137 while True:
138 iv = self.xtea_encrypt(key,iv,n)
139 for k in iv:
140 yield ord(k)
141 xor = [ chr(x^y) for (x,y) in zip(map(ord,data),keygen(key,iv,n)) ]
142 return "".join(xor)
144 def xtea_encrypt(self, key,block,n=32):
145 v0,v1 = struct.unpack("!2L",block)
146 k = struct.unpack("!4L",key)
147 sum,delta,mask = 0L,0x9e3779b9L,0xffffffffL
148 for round in range(n):
149 v0 = (v0 + (((v1<<4 ^ v1>>5) + v1) ^ (sum + k[sum & 3]))) & mask
150 sum = (sum + delta) & mask
151 v1 = (v1 + (((v0<<4 ^ v0>>5) + v0) ^ (sum + k[sum>>11 & 3]))) & mask
152 return struct.pack("!2L",v0,v1)
154 class WordWrapper:
155 def __init__(self, width):
156 self.width = width
157 self.nLines = 0
158 self.pos = 0
160 def addLine(self, pos):
161 self.pos = pos
162 self.nLines = self.nLines + 1
164 def getNLines(self):
165 return self.nLines
167 def intelliLen(self, text):
168 return len(gColor.stripCodes(text))
170 def wrap(self, text):
171 self.nLines = 0
172 formatted = text.replace("<br>", "\n").replace("<BR>", "\n")
173 lines = formatted.splitlines()
174 out = ""
175 self.pos = 0
176 for thisline in lines:
177 newline = True
178 words = thisline.split()
179 if self.pos != 0:
180 out = out + "\n"
181 self.addLine(0)
182 for w in words:
183 wlen = self.intelliLen(w) + 1
184 if (self.pos + wlen) == self.width:
185 out = out + " " + w
186 self.addLine(0)
187 elif (self.pos + wlen) < self.width:
188 if newline:
189 out = out + w
190 self.pos = wlen
191 else:
192 out = out + " " + w
193 self.pos = self.pos + wlen + 1
194 else:
195 out = out + "\n" + w
196 self.addLine(wlen)
197 newline = False
198 return out
200 ### Color code class for handling color text output
201 class ColorCoder:
202 NONE = -1
203 ANSI = 0
204 codes = [{"normal":"\x1b[0;37;40m",
205 "title":"\x1b[1;32;40m",
206 "heading":"\x1b[1;35;40m",
207 "bold":"\x1b[1;35;40m",
208 "important":"\x1b[1;31;40m",
209 "error":"\x1b[1;31;40m",
210 "reverse":"\x1b[0;7m",
211 "row0":"\x1b[0;35;40m",
212 "row1":"\x1b[0;36;40m"},
213 {"normal":"\x1b[0;37m",
214 "title":"\x1b[1;32m",
215 "heading":"\x1b[1;35m",
216 "bold":"\x1b[1;35m",
217 "important":"\x1b[1;31m",
218 "error":"\x1b[1;31m",
219 "reverse":"\x1b[0;7m",
220 "row0":"\x1b[0;35m",
221 "row1":"\x1b[0;36m"}]
223 def __init__(self, set):
224 self.codeSet = self.NONE
225 self.setCodeSet(set)
227 def stripCodes(self, text):
228 # strip out the ansi codes
229 ex = re.compile("\x1b\[[0-9;]*m")
230 return ex.sub("", text)
232 def setCodeSet(self, set):
233 old = self.codeSet
234 if set < 0:
235 self.codeSet = self.NONE
236 elif set < len(self.codes):
237 self.codeSet = set
238 return (old != self.codeSet)
240 def isValidSet(self, myset):
241 if myset < len(self.codes):
242 return True
243 else:
244 return False
246 def colorSupported(self):
247 return (os.name == "posix" or os.name == "mac")
249 def usingColor(self):
250 return (self.codeSet <> self.NONE and self.colorSupported())
252 def code(self, type):
253 if self.codeSet == self.NONE or not self.colorSupported():
254 return ""
255 else:
256 return self.codes[self.codeSet][type]
258 def printCode(self, type):
259 if self.codeSet != self.NONE:
260 print self.code(type),
262 def getCodeSet(self):
263 return self.codeSet
265 ### Viewer class for paging through multiple lines
266 class ListViewer:
267 def __init__(self, maxlines):
268 self.maxlines = maxlines
270 def show(self, list, pause):
271 count = 0
272 for line in list:
273 if count >= self.maxlines or line == pause:
274 io = safeRawInput("--- Press enter for more. Enter s to skip ---").strip()
275 print ""
276 if len(io) > 0 and io.upper()[0] == "S":
277 break
278 count = 0
279 if line != pause:
280 print line
281 count = count + 1
283 ### Handler for encryption
284 class Encryptor:
285 TYPE_OBSCURED = "xtea_"
286 TYPE_AES = "aes_"
287 SALT_64 = "1hJ8*gpQ"
289 def __init__(self):
290 self.key = ""
291 self.encryptionType = self.TYPE_OBSCURED
293 def setType(self, codeType):
294 if codeType == self.TYPE_AES and supportAes == False:
295 self.encryptionType = self.TYPE_OBSCURED
296 else:
297 self.encryptionType = codeType
298 return self.encryptionType
300 def setKey(self, key):
301 self.key = key
303 def getKey(self):
304 return self.key
306 def enterKey(self, prompt1, prompt2):
307 done = False
308 while not done:
309 input1 = getpass.getpass(prompt1 + " >>>")
310 if prompt2 != "":
311 input2 = getpass.getpass(prompt2 + " >>>")
312 if input1 != input2:
313 print "You must enter the same password. Start again"
314 else:
315 done = True
316 else:
317 done = True
318 self.key = input1
319 return input1
321 def complexKey(self):
322 return md5(self.key).digest()
324 def getSecurityClass(self, encrypted):
325 if encrypted.startswith(self.TYPE_OBSCURED):
326 return "private xtea"
327 if encrypted.startswith(self.TYPE_AES):
328 return "secret aes"
329 return "unknown"
331 def obscure(self, plainText):
332 key = self.complexKey()
333 obscured = Xtea().crypt(key, plainText, self.SALT_64)
334 return self.TYPE_OBSCURED + obscured.encode('hex_codec')
336 def unobscure(self, obscured):
337 plain = ""
338 data = obscured[len(self.TYPE_OBSCURED):]
339 data = data.decode('hex_codec')
340 key = self.complexKey()
341 plain = Xtea().crypt(key, data, self.SALT_64)
342 return plain
344 def encryptAes(self, plainText):
345 if len(self.key) < 16:
346 key = self.complexKey()
347 else:
348 key = self.key
349 obscured = pyRijndael.EncryptData(key, plainText)
350 return self.TYPE_AES + obscured.encode('hex_codec')
352 def decryptAes(self, encrypted):
353 plain = ""
354 data = encrypted[len(self.TYPE_AES):]
355 data = data.decode('hex_codec')
356 if len(self.key) < 16:
357 key = self.complexKey()
358 else:
359 key = self.key
360 plain = pyRijndael.DecryptData(key, data)
361 return plain
363 def enterKeyAndEncrypt(self, plainText):
364 self.enterKey("Enter the master password.", "Re-enter the master password")
365 return self.encrypt(plainText)
367 def encrypt(self, plainText):
368 if self.encryptionType == self.TYPE_AES:
369 return self.encryptAes(plainText)
370 else:
371 return self.obscure(plainText)
373 def enterKeyAndDecrypt(self, encryptedText):
374 self.enterKey("Enter your master password", "")
375 return self.decrypt(encryptedText)
377 def decrypt(self, encryptedText):
378 if encryptedText.startswith(self.TYPE_AES):
379 if not supportAes:
380 return "You do not have the pyRinjdael module so the text cannot be decrypted."
381 else:
382 return self.decryptAes(encryptedText)
383 else:
384 return self.unobscure(encryptedText)
386 ### Handler for user input
387 class InputParser:
388 def __init__(self, prompt):
389 self.prompt = prompt
391 def read(self, entry = ""):
392 if entry == "":
393 entry = safeRawInput(self.prompt)
394 entry = entry.strip()
395 if entry == "":
396 command = ""
397 line = ""
398 else:
399 if entry.find(magicTag) == 0:
400 printError("You cannot begin lines with the sequence " + magicTag)
401 command = ""
402 line = ""
403 elif entry.find(TodoItem.ENCRYPTION_MARKER) >= 0:
404 printError ("You cannot use the special sequence " + TodoItem.ENCRYPTION_MARKER)
405 command = ""
406 line = ""
407 else:
408 n = entry.find(" ")
409 if n >= 0:
410 command = entry[:n]
411 line = entry[n + 1:]
412 else:
413 command = entry
414 line = ""
416 return (command, line)
418 class EditorLauncher:
419 WARNING_TEXT = "# Do not enter secret or private information!"
420 def __init__(self):
421 pass
423 def edit(self, text):
424 ed = ""
425 terminator = "\n"
426 if os.name == "posix":
427 ed = cfgEditorPosix
428 elif os.name == "nt":
429 ed = cfgEditorNt
430 terminator = "\r\n"
431 if ed == "":
432 printError("Sorry, but external editing not supported on " + os.name.upper())
433 success = False
434 else:
435 fname = self.makeFile(text, terminator)
436 if fname == "":
437 printError("Unable to create temporary file.")
438 else:
439 success = self.run(ed, fname)
440 if success:
441 (success, text) = self.readFile(fname)
442 if text == self.orgText:
443 print("No changes made.");
444 success = False
445 self.scrubFile(fname)
446 if success:
447 return text
448 else:
449 return ""
451 def scrubFile(self, fname):
452 try:
453 os.remove(fname)
454 except Exception, e:
455 printError("Failed to remove file " + fname + ". If you entered any private data you should delete this file yourself.")
457 def readFile(self, fname):
458 success = False
459 try:
460 fh = open(fname, "rt")
461 line = fh.readline()
462 text = ""
463 first = True
464 while line != "":
465 thisLine = self.safeString(line)
466 if thisLine != self.WARNING_TEXT:
467 if not first:
468 text = text + "<br>"
469 text = text + thisLine
470 first = False
471 line = fh.readline()
472 fh.close()
473 success = True
474 except Exception, e:
475 printError("Error reading the edited text. " + str(e))
476 return (success, text)
478 def safeString(self, text):
479 return text.replace("\r","").replace("\n","")
481 def makeFile(self, text, terminator):
482 fname = ""
483 (fh, fname) = tempfile.mkstemp(".tmpikog","ikog")
484 fout = os.fdopen(fh,"wt")
485 text = text.replace("<BR>", "<br>")
486 self.orgText = text
487 lines = text.split("<br>")
488 fout.write(self.WARNING_TEXT + terminator)
489 for thisline in lines:
490 fout.write(self.safeString(thisline) + terminator)
491 fout.close()
492 return fname
494 def run(self, program, file):
495 progs = program.split(",")
496 for prog in progs:
497 success = self.runProgram(prog.strip(), file)
498 if success:
499 break;
500 return success
502 def runProgram(self, program, file):
503 success = False
504 if os.name == "posix":
505 try:
506 progarg = program
507 os.spawnlp(os.P_WAIT, program, progarg, file)
508 success = True
509 except os.error:
510 pass
511 except Exception, e:
512 printError(str(e))
513 elif os.name == "nt":
514 if file.find(" ") >= 0:
515 file = "\"" + file + "\""
516 for path in os.environ["PATH"].split(os.pathsep):
517 try:
518 prog = os.path.join(path, program)
519 if prog.find(" ") >= 0:
520 progarg = "\"" + prog + "\""
521 else:
522 progarg = prog
523 os.spawnl(os.P_WAIT, prog, progarg, file)
524 success = True
525 if success:
526 break
527 except os.error:
528 pass
529 except Exception, e:
530 printError(str(e))
531 return success
534 ### The main todo list
535 class TodoList:
536 quickCard = ["Quick reference card:",
537 "? ADD/A/+ text FILTER/FI [filter]",
538 "HELP/H IMMEDIATE/I/++ text TOP/T [N]",
539 "COLOR/COLOUR/C [N] KILL/K/X/- N NEXT/N",
540 "MONOCHROME/MONO CLEAR PREV/P",
541 "EXPORT REP/R N [text] GO/G N",
542 "IMPORT file MOD/M N [text] LIST/L [filter]",
543 "REVIEW/REV ON/OFF EXTEND/E N [text] LIST>/L> [filter]",
544 "V0 EDIT/ED [N] @",
545 "V1 SUB/SU N /s1/s2/ :D",
546 "WEB FIRST/F N :P>",
547 "SAVE/S DOWN/D N @>",
548 "AUTOSAVE/AS ON|OFF UP/U N :D>",
549 "VER/VERSION NOTE/NOTES text :P>",
550 "CLEARSCREEN/CLS O/OPEN file SHOW N",
551 "SYS ON|OFF NEW file SETEDxx editor",
552 "!CMD command 2 ABBREV/AB @x @full",
553 "ABBREV/AB ? PAB ? PAB :px :pfull",
554 "SHORTCUT/SC N cmd SHORTCUT/SC ? =N",
555 "ARCHIVE/DONE N [text]",
558 help = [ "",
559 "Introduction",
560 "------------",
561 "The program is designed to help manage tasks using techniques",
562 "such as Getting Things Done by David Allen. Check out",
563 "http://www.henspace.co.uk for more information and detailed help.",
564 "To use the program, simply enter the task at the prompt.",
565 "All of the commands are displayed in the next section.",
566 "!PAUSE!",
567 "COMMANDS",
568 "--------",
569 "Commands that have more than one method of entry are shown separated by /",
570 "e.g HELP/H means that you can enter either HELP or an H.",
571 "All commands can be entered in upper or lower case.",
572 "Items shown in square brackets are optional.",
573 "Items shown separated by the | symbol are alternatives. e.g ON|OFF means",
574 "you should type either ON or OFF.",
575 "Note that some commands refer to adding tasks to the top or bottom of the",
576 "list. However the task's position in the list is also determined by its.",
577 "priority. So, for example, adding a task to the top will still not allow",
578 "it to precede tasks that have been assigned a higher priority number. ",
579 "!PAUSE!",
580 "GENERAL COMMANDS",
581 "----------------",
582 "? : displays a quick reference card",
583 "HELP/H : displays this help.",
584 "VERSION/VER : display the version.",
585 "WEB : Go to the website for more information",
586 "CLEARSCREEN/CLS : Clear the screen",
587 "COLOR/COLOUR/C [N] : Use colour display (not Windows) N=1 for no background",
588 "MONOCHROME/MONO : Use monochrome display",
589 "EXPORT : Export the tasks only to filename.tasks.txt",
590 "IMPORT file : Import tasks from the file",
591 "REVIEW/REV ON|OFF : If on, hitting enter moves to the next task",
592 " : If off, enter re-displays the current task",
593 "V0 : Same as REVIEW OFF",
594 "V1 : Same as REVIEW ON",
595 "SAVE/S : Save the tasks",
596 "O/OPEN file : Open a new data file.",
597 "NEW file : Create a new data file.",
598 "AUTOSAVE/AS ON|OFF : Switch autosave on or off",
599 "SYS ON|OFF : Allow the program to use system calls.",
600 "!CMD command : Run a system command.",
601 "2 : Start a two minute timer (for GTD)",
602 "QUIT/Q : quit the program",
603 "!PAUSE!",
604 "TASK ENTRY AND EDITING COMMANDS",
605 "-------------------------------",
606 "For the editing commands that require a task number, you can",
607 "replace N by '^' or 'this' to refer to the current task.",
608 "ADD/A/+ the task : add a task to the bottom of the list.",
609 " : Entering any line that does not begin with",
610 " : a valid command and which is greater than 10",
611 " : characters long is also assumed to be an addition.",
612 "EDIT/ED [N] : Create task, or edit task N, using external editor.",
613 "SUB/SU N /s1/s2/ : Replace text s1 with s2 in task N. Use \/ if you",
614 " : need to include the / character.",
615 "NOTE/NOTES text : shorthand for ADD #0 @Notes text",
616 "IMMEDIATE/I/++ : add a task to the top of the list to do today.",
617 "REP/R N [text] : replace task N",
618 "MOD/M N [text] : modify task N.",
619 "EXTEND/E N [text] : add more text to task N",
620 "FIRST/F N : move task N to the top.",
621 "DOWN/D/ N : move task N down the queue",
622 "UP/U/ N : move task N up the queue",
623 "!PAUSE!",
624 "TASK REMOVAL COMMANDS",
625 "---------------------",
626 "KILL/K/X/- N : kill (delete) task N. You must define N",
627 "DONE N [text] : Remove task N and move to an archive file",
628 "ARCHIVE N [text] : Same as DONE",
629 "CLEAR : Remove all tasks",
630 "!PAUSE!",
631 "DISPLAY COMMANDS",
632 "----------------",
633 "SHOW N : display encrypted text for task N",
634 "FILTER/FI [filter] : set a filter. Applies to all displays",
635 " : See list for details of the filter",
636 " : Setting the filter to nothing clears it.",
637 "TOP/T [N] : Go to top, list N tasks, and display the top task",
638 "NEXT/N : display the next task. Same as just hitting enter",
639 "PREV/P : display previous task",
640 "GO/G N : display task N",
641 "LIST/L [filter] : list tasks. Filter = context, project, priority, date",
642 " : or word. Contexts begin with @ and projects with :p",
643 " : Dates begin with :d, anything else is a search word.",
644 " : Precede term with - to exclude e.g. -@Computer",
645 " : e.g LIST @computer or LIST #5",
646 "@ : sorted list by Context.",
647 ":D : sorted list by Dates",
648 ":P : sorted list by Projects",
649 "LIST>/L> [filter] : standard list sent to an HTML report",
650 "@> : sorted list by Context sent to an HTML report",
651 ":D> : sorted list by Dates sent to an HTML report",
652 ":P> : sorted list by Projects sent to an HTML report",
653 " : The HTML reports are sent to todoFilename.html",
654 "!PAUSE!",
655 "ADVANCED OPTIONS",
656 "----------------",
657 "The SETEDxxx commands allow you to use an external editor.",
658 "Note the editor you pick should be a simple text editor. If you pick",
659 "something that doesn't work, try the defaults again.",
660 "Because some systems may have different editors installed, you can set",
661 "more than one by separating the editors usng commas. The program will",
662 "use the first one it finds.",
663 "For Windows the default is edit, which works quite well in the terminal",
664 "but you could change it to notepad.",
665 "For Linux, the default is nano,pico,vim,emacs.",
666 "To use external editors you must switch on system calls using the SYS ON",
667 "command",
668 "SETEDNT command : Set the external editor for Windows (NT).",
669 "SETEDPOSIX command : Set the editor for posix systems.",
670 " : e.g. SETEDNT edit",
671 " : SETEDPOSIX nano,vim",
672 "SHORTCUT/SC ? : list shortcuts",
673 "SHORTCUT/SC N cmd : Set shortcut N to command cmd",
674 "=N : Run shortcut N",
675 "!PAUSE!",
676 "ABBREV/AB @x @full : Create new abbreviation. @x expands to @full",
677 "ABBREV/AB ? : List context abbreviations.",
678 "PAB :px :pfull : Project abbreviation. :px expands to :pfull",
679 "PAB ? : List project abbreviations.",
680 "!PAUSE!",
681 "ENTERING TASKS",
682 "--------------",
683 "When you enter a task, you can embed any number of contexts in the task.",
684 "You can also embed a project description by preceding it with :p",
685 "You can assign a priority by preceding a number by #. e.g. #9.",
686 "If you don't enter a number, a default of 5 is used. The higher the ",
687 "number the more important it is. Priorities range from 1 to 10.",
688 "Only the first # is used for the priority so you can use # as",
689 "a normal character as long as you precede it with a priority number.",
690 "You can define a date when the task must be done by preceding the date",
691 "with :d, i.e :dYYYY/MM/DD or :dMM/DD or :dDD. If you omit the year/month",
692 "they default to the current date. Adding a date automatically creates an",
693 "@Date context for the task.",
694 "So, for example, to add a new task to e-mail Joe, we could enter:",
695 "+ e-mail joe @computer",
696 "or to add a task to the decorating project, we could enter:",
697 "+ buy wallpaper :pdecorating",
698 "to enter a task with an importance of 9 we could enter:",
699 "+ book that holiday #9 @Internet",
700 "!PAUSE!",
701 "MODIFYING AND EXTENDING TASKS",
702 "-----------------------------",
703 "The modify command allows you to change part of an existing task.",
704 "So for example, imagine you have a task:",
705 "[05] Buy some food #9 @Internet Projects:Shopping",
706 "Enter the command M 5 and then type:",
707 "@C",
708 "Because the only element we have entered is a new context, only",
709 "that part is modified, so we get.",
710 "[05] Buy some food #9 @Computer Projects:Shopping",
711 "Likewise, had we entered:",
712 "Buy some tea :pEating",
713 "We would have got",
714 "[05] Buy some tea #9 @Internet Projects:Eating",
715 "The extend command is similar but it appends the entry. So had",
716 "we used the command E 5 instead of M 5 the result would have been",
717 "[05] Buy some food ... Buy some tea #9 @Internet Projects:Eating",
718 "!PAUSE!",
719 "CONTEXTS",
720 "--------",
721 "Any word preceded by @ will be used as a context. Contexts are like",
722 "sub-categories or sub-lists. There are a number of pre-defined",
723 "abbreviations that you can use as well. The recognised abbreviations",
724 "are:",
725 "@A = @Anywhere (this is the default)",
726 "@C = @Computer",
727 "@D = @Desk",
728 "@E = @Errands",
729 "@H = @Home",
730 "@I = @Internet",
731 "@L = @Lunch",
732 "@M = @Meeting",
733 "@N = @Next",
734 "@O = @Other",
735 "@P = @Phone",
736 "@PW= @Password",
737 "@S = @Someday/maybe",
738 "@W4= @Waiting_for",
739 "@W = @Work",
740 "!PAUSE!",
741 "ENTERING DATES",
742 "--------------",
743 "An @Date context is created if you embed a date in the task.",
744 "Dates are embedded using the :dDATE format.",
745 "Valid DATE formats are yyyy-mm-dd, mm-dd or dd",
746 "You can also use : or / as the separators. So, for example:",
747 ":d2006/12/22 or :d2006-11-7 or :d9/28 are all valid entries.",
749 "If you set a date, then until that date is reached, the task is given",
750 "an effective priority of 0. Once the date is reached, the task's",
751 "priority is increased by 11, moving it to the of the list.",
753 "A date entry of :d0 can be used to clear a date entry.",
754 "A date entry of :d+X can be used to create a date entry of today + X days.",
755 "So :d+1 is tomorrow and :d+0 is today.",
756 "!PAUSE!",
757 "ENCRYPTING TEXT",
758 "---------------",
759 "If you want to encrypt text you can use the <private> or <secret> tags or",
760 "their abbreviations <p> and <s>.",
761 "These tags will result in all text following the tag to be encrypted.",
762 "Note that any special commands, @contexts for example, are treated as plain",
763 "text in the encrypted portion.",
764 "To display the text you will need to use the SHOW command.",
766 "The <private> tag uses the inbuilt XTEA algorithm. This is supposedly a",
767 "relatively secure method but probably not suitable for very sensitive data.",
769 "The <secret> tag can only be used if you have the pyRijndael.py module.",
770 "This uses a 256 bit Rinjdael cipher. The module can be downloaded from ",
771 "http://jclement.ca/software/pyrijndael/",
772 "You can install this in your Python path or just place it alongside your",
773 "ikog file.",
774 "Note you cannot use the extend command with encrypted text.",
776 "!PAUSE!",
777 "MARKING TASKS AS COMPLETE",
778 "-------------------------",
779 "The normal way to mark a task as complete is just to remove it using the",
780 "KILL command. If you want to keep track of tasks you have finished, you",
781 "can use the ARCHIVE or DONE command. This gives the task an @Archived",
782 "context, changes the date to today and then moves it from the current",
783 "file to a file with archive.dat appended. The archive file is a valid",
784 "ikog file so you can use the OPEN command to view it, edit it and run",
785 "reports in the normal way. So assuming your current script is ikog.py,",
786 "to archive the current task you could enter:",
788 "ARCHIVE ^ I have finished this",
790 "This would move the task to a file called ikog.py.archive.dat",
792 "!PAUSE!",
793 "USING EXTERNAL DATA",
794 "-------------------",
795 "Normally the tasks are embedded in the main program file so all you have",
796 "to carry around with you is the ikog.py file. The advantage is that you",
797 "only have one file to look after; the disadvantage is that every time you",
798 "save a task you have to save the program as well. If you want, you can",
799 "keep your tasks in a separate file.",
800 "To do this, use the EXPORT function to create a file ikog.py.tasks.txt",
801 "Use the CLEAR command to remove the tasks from your main ikog.py program.",
802 "Rename the exported file from ikog.py.tasks.txt to ikog.py.dat",
803 "Ikog will now use this file for storing your tasks.",
805 "!PAUSE!",
806 "PASSING TASKS VIA THE COMMAND LINE",
807 "----------------------------------",
808 "It is possible to add tasks via the command line. The general format of",
809 "the command line is:",
810 " ikog.py filename commands",
811 "The filename is the name of the data file containing your tasks. You",
812 "can use . to represent the default internal tasks.",
813 "Commands is a set of normal ikog commands separated by the / ",
814 "character. Note there must be a space either side of the /.",
815 "So to add a task and then exit the program we could just enter:",
816 " ikog.py . + here is my task / QUIT",
817 "Note that we added the quit command to exit ikog.",
818 "You must make sure that you do not use any commands that require user",
819 "input. Deleting tasks via the command line is more complicated as you",
820 "need to find the task automatically. If you do try to delete this way,",
821 "use the filter command to find some unique text and then delete it. eg.",
822 " ikog.py . FI my_unique_text / KILL THIS / QUIT",
823 "Use THIS instead of ^ as a caret has a special meaning in Windows.",
824 "If you do intend automating ikog from the command line, you should add",
825 "a unique reference to each task so you can find it later using FILTER. eg.",
826 "+ this is my task ref_1256",
827 "!PAUSE!"]
829 MOVE_DOWN = 0
830 MOVE_UP = 1
831 MOVE_TOP = 2
832 MAX_SHORTCUTS = 10
834 def __init__(self, todoFile, externalDataFile):
835 self.setShortcuts()
836 self.dirty = False
837 self.code = []
838 self.todo = []
839 self.autoSave = True
840 self.review = True
841 self.sysCalls = False
842 self.currentTask = 0
843 self.globalFilterText = ""
844 self.globalFilters = []
845 self.localFilterText = ""
846 self.localFilters = []
847 # split the file into the source code and the todo list
848 self.filename = todoFile
849 try:
850 self.filename = os.readlink(todoFile)
851 except Exception:
852 pass # probably windows
853 externalDataFile = self.makeFilename(externalDataFile)
854 self.externalData = self.findDataSource(externalDataFile)
855 if self.externalData:
856 self.filename = externalDataFile
857 else:
858 self.splitFile(self.filename, False)
859 self.exactPriority = False
860 self.htmlFile = ""
862 def setPAbbreviation(self, line):
863 save = False
864 elements = line.split(" ", 1)
865 if not elements[0].lower().startswith(":p"):
866 self.showError("Project abbreviations must begin with :p")
867 elif len(elements) > 1:
868 if not elements[1].lower().startswith(":p"):
869 abb = ":p" + elements[1].title()
870 else:
871 abb = ":p" +elements[1][2:].title()
872 globalPAbbr.addAbbreviation(elements[0], abb)
873 save = True
874 else:
875 if not globalPAbbr.removeAbbreviation(elements[0]):
876 self.showError("Could not find project abbreviation " + line)
877 else:
878 print "Project abbreviation ", line, " removed."
879 save = True
880 return save
882 def showPAbbreviations(self):
883 print globalPAbbr.toStringVerbose()
885 def setAbbreviation(self, line):
886 save = False
887 elements = line.split(" ", 1)
888 if not elements[0].startswith("@"):
889 self.showError("Abbreviations must begin with @")
890 elif len(elements) > 1:
891 if not elements[1].startswith("@"):
892 abb = "@" + elements[1].title()
893 else:
894 abb = "@" +elements[1][1:].title()
895 globalAbbr.addAbbreviation(elements[0], abb)
896 save = True
897 else:
898 if not globalAbbr.removeAbbreviation(elements[0]):
899 self.showError("Could not find abbreviation " + line)
900 else:
901 print "Abbreviation ", line, " removed."
902 save = True
903 return save
905 def showAbbreviations(self):
906 print globalAbbr.toStringVerbose()
908 def setShortcut(self, line, force = False):
909 elements = line.split(" ", 1)
910 try:
911 index = int(elements[0])
912 if len(elements) > 1:
913 command = elements[1]
914 else:
915 command = ""
916 except Exception, e:
917 self.showError("Did not understand the command. Format should be SHORTCUT N my command.")
918 return False
920 if index < 0 or index > len(self.shortcuts):
921 self.showError("The maximum number of shortcuts is " + str(len(self.shortcuts)) + ". Shortcuts ignored.")
922 return False
923 else:
924 if self.shortcuts[index] != "" and not force:
925 if safeRawInput("Do you want to change the current command '" + self.shortcuts[index] + "'? Enter Yes to overwrite. >>>").upper() != "YES":
926 return False
927 self.shortcuts[index] = command
928 return True
931 def showShortcuts(self):
932 index = 0
933 for s in self.shortcuts:
934 if s == "":
935 msg = "unused"
936 else:
937 msg = s
938 print "=%1d %s" %(index, msg)
939 index = index + 1
941 def setShortcuts(self, settings = []):
942 if len(settings) > self.MAX_SHORTCUTS:
943 self.showError("The maximum number of shortcuts is " + str(self.MAX_SHORTCUTS) + ". Shortcuts ignored.")
944 self.shortcuts = ["" for n in range(self.MAX_SHORTCUTS)]
945 if len(settings) > 0:
946 self.shortcuts[0:len(settings)] = settings
948 def getShortcutIndex(self, command):
949 if len(command) == 2 and command[0:1].upper() == "=":
950 index = ord(command[1]) - ord("0")
951 if index >= self.MAX_SHORTCUTS:
952 index = -1
953 else:
954 index = -1
955 return index
957 def getShortcut(self, command):
958 index = self.getShortcutIndex(command)
959 if index >= 0:
960 return self.shortcuts[index]
961 else:
962 return ""
964 def safeSystemCall(self, line):
965 words = line.split()
966 if len(words) == 0:
967 self.showError("Nothing to do.")
968 elif words[0].upper() == "RM" or words[0].upper() == "RMDIR" or words[0].upper() == "DEL":
969 self.showError("Sorry, but deletion commands are not permitted.")
970 else:
971 os.system(line)
972 self.pause()
974 def processCfgLine(self, line):
975 params = line.split("=")
976 if len(params) < 2:
977 return
978 cmd = params[0].strip()
979 if cmd == "cfgEditorNt":
980 global cfgEditorNt
981 cfgEditorNt = params[1].replace("\"", "").strip()
982 elif cmd == "cfgEditorPosix":
983 global cfgEditorPosix
984 cfgEditorPosix = params[1].replace("\"", "").strip()
985 elif cmd == "cfgShortcuts":
986 elements = params[1].strip()[1:-1].split(",")
987 index = 0
988 for e in elements:
989 self.setShortcut(str(index) + " " + e.strip()[1:-1], True)
990 index = index + 1
991 elif cmd == "cfgAutoSave":
992 if params[1].upper().strip() == "TRUE":
993 as_ = True
994 else:
995 as_ = False
996 self.setAutoSave(as_, False)
997 elif cmd == "cfgReviewMode":
998 if params[1].upper().strip() == "TRUE":
999 self.setReview("ON")
1000 else:
1001 self.setReview("OFF")
1002 elif cmd == "cfgSysCalls":
1003 if params[1].upper().strip() == "TRUE":
1004 self.setSysCalls("ON")
1005 else:
1006 self.setSysCalls("OFF")
1007 elif cmd == "cfgColor":
1008 gColor.setCodeSet(int(params[1].strip()))
1009 elif cmd == "cfgAbbreviations":
1010 abbrs = eval(params[1].strip())
1011 globalAbbr.setAbbreviations(abbrs)
1012 elif cmd == "cfgPAbbreviations":
1013 abbrs = eval(params[1].strip())
1014 globalPAbbr.setAbbreviations(abbrs)
1015 else:
1016 self.showError("Unrecognised command " + cmd)
1018 def makeFilename(self, name):
1019 (root, ext) = os.path.splitext(name)
1020 if ext.upper() != ".DAT":
1021 name = name + ".dat"
1022 try:
1023 name = os.path.expanduser(name)
1024 except Exception, e:
1025 self.showError("Failed to expand path. " + str(e))
1026 return name
1028 def findDataSource(self, filename):
1029 success = False
1031 try:
1032 self.splitFile(filename, False)
1033 print "Using external data file ", filename
1034 success = True
1035 except IOError:
1036 print "No external data file ", filename, ", so using internal tasks."
1037 return success
1039 def setSysCalls(self, mode):
1040 oldCalls = self.sysCalls
1041 mode = mode.strip().upper()
1042 if mode == "ON":
1043 self.sysCalls = True
1044 print "Using system calls for clear screen"
1045 elif mode == "OFF":
1046 self.sysCalls = False
1047 print "No system calls for clear screen"
1048 else:
1049 self.showError("Could not understand the sys command. Use SYS ON or OFF.")
1050 return (self.sysCalls != oldCalls)
1052 def setAutoSave(self, as_, save):
1053 if as_:
1054 if self.autoSave == False:
1055 self.autoSave = True
1056 if save:
1057 self.save("")
1058 elif self.autoSave == True:
1059 self.autoSave = False
1060 if save:
1061 self.save("")
1062 if self.autoSave:
1063 print "Autosave is on."
1064 else:
1065 print "Autosave is off."
1068 def showError(self, msg):
1069 printError(msg)
1071 def pause(self, prompt = "Press enter to continue."):
1072 if safeRawInput(prompt).strip() != "":
1073 print "Entry ignored!"
1075 def setReview(self, mode):
1076 oldReview = self.review
1077 mode = mode.strip().upper()
1078 if mode == "ON":
1079 self.review = True
1080 print "In review mode. Enter advances to the next task"
1081 elif mode == "OFF":
1082 self.review = False
1083 print "Review mode off. Enter re-displays the current task"
1084 else:
1085 self.showError("Could not understand the review command. Use REVIEW ON or OFF.")
1086 return (self.review != oldReview)
1088 def sortByPriority(self):
1089 self.todo.sort(key=TodoItem.getEffectivePriority, reverse = True)
1092 def run(self, commandList):
1093 if not supportAes:
1094 print "AES encryption not available."
1095 print("\nEnter HELP for instructions.")
1097 done = False
1098 printCurrent = True
1099 self.sortByPriority()
1100 reopen = ""
1101 enteredLine = ""
1102 truncateTask = False
1103 while not done:
1104 self.checkCurrentTask()
1105 if printCurrent:
1106 self.moveToVisible()
1107 print ruler
1108 if truncateTask:
1109 self.printItemTruncated(self.currentTask, "Current: ")
1110 else:
1111 self.printItemVerbose(self.currentTask)
1112 print ruler
1113 printCurrent= True
1114 truncateTask = False
1115 if self.dirty:
1116 prompt = "!>>"
1117 else:
1118 prompt = ">>>"
1119 if len(commandList) >= 1:
1120 enteredLine = commandList[0]
1121 commandList = commandList[1:]
1122 print enteredLine
1123 (rawcommand, line) = InputParser(prompt).read(enteredLine)
1124 enteredLine = ""
1125 command = rawcommand.upper()
1126 if self.getShortcutIndex(command) >= 0:
1127 sc = self.getShortcut(command)
1128 if sc != "":
1129 (rawcommand, line) = InputParser("").read(self.getShortcut(command))
1130 else:
1131 rawcommand = ""
1132 line = ""
1133 continue
1134 print "Shortcut: ", rawcommand, " ", line
1135 command = rawcommand.upper()
1136 if command == "":
1137 if self.review:
1138 self.incTaskLoop()
1139 elif command == "PAB":
1140 if line.strip() == "?":
1141 self.showPAbbreviations()
1142 elif self.setPAbbreviation(line):
1143 self.save("")
1144 elif command == "ABBREV" or command == "AB":
1145 if line.strip() == "?":
1146 self.showAbbreviations()
1147 elif self.setAbbreviation(line):
1148 self.save("")
1149 elif command == "SHORTCUT" or command == "SC":
1150 if line.strip() == "?":
1151 self.showShortcuts()
1152 else:
1153 if self.setShortcut(line):
1154 self.save("")
1155 printCurrent = False
1156 elif command == "2":
1157 enteredLine = self.runTimer(2)
1158 printCurrent = False
1159 elif command == "CLS" or command == "CLEARSCREEN":
1160 clearScreen(self.sysCalls)
1161 elif command == "SETEDNT":
1162 global cfgEditorNt
1163 cfgEditorNt = line
1164 self.save("")
1165 elif command == "SETEDPOSIX":
1166 global cfgEditorPosix
1167 cfgEditorPosix = line
1168 self.save("")
1169 elif command == "SYS":
1170 if self.setSysCalls(line):
1171 self.save("")
1172 elif command == "!CMD":
1173 if self.sysCalls:
1174 self.safeSystemCall(line)
1175 else:
1176 self.showError("System calls are not allowed. Use SYS ON to enable them.")
1177 elif command == "SHOW" or command == "SH":
1178 self.decrypt(line)
1179 self.pause("Press enter to clear screen and continue. ")
1180 clearScreen(self.sysCalls)
1181 elif command == "VERSION" or command == "VER":
1182 print notice[0]
1183 elif command == "SAVE" or command == "S":
1184 if not self.dirty:
1185 print "There's no need to save now. If the prompt shows >>> "
1186 print "then there is nothing to save. You only need to save if the prompt "
1187 print "shows !>>"
1188 else:
1189 self.forceSave("")
1190 elif command == "NEW":
1191 filename = self.makeFilename(line)
1192 if self.createFile(filename):
1193 reopen = filename
1194 if self.dirty:
1195 self.forceSave("")
1196 done = True
1197 printCurrent = False
1198 elif command == "OPEN" or command == "O":
1199 filename = self.makeFilename(line)
1200 reopen = filename
1201 if self.dirty:
1202 self.forceSave("")
1203 done = True
1204 printCurrent = False
1205 elif command == "AUTOSAVE" or command == "AS":
1206 if line== "":
1207 self.showError("You must enter ON or OFF for the autosave command")
1208 else:
1209 self.setAutoSave(line.upper() == "ON", True)
1210 elif command == "REVIEW" or command == "REV":
1211 if self.setReview(line):
1212 self.save("")
1213 elif command == "V0":
1214 if self.setReview("OFF"):
1215 self.save("")
1216 elif command == "V1":
1217 if self.setReview("ON"):
1218 self.save("")
1219 elif command == "?":
1220 self.printHelp(self.quickCard)
1221 elif command == "HELP" or command == "H":
1222 self.printHelp(self.help)
1223 elif command == "QUIT" or command == "Q":
1224 if self.dirty:
1225 self.forceSave("")
1226 done = True
1227 printCurrent = False
1228 elif command == "WEB":
1229 try:
1230 webbrowser.open("http://www.henspace.co.uk")
1231 except Exception, e:
1232 self.showError("Unable to launch browser. " + str(e))
1233 elif command == "COLOR" or command == "COLOUR" or command == "C":
1234 try:
1235 set = int(line, 10)
1236 except ValueError:
1237 set = gColor.ANSI
1238 if not gColor.isValidSet(set):
1239 self.showError("Invalid colour set ignored.")
1240 elif gColor.setCodeSet(set):
1241 self.save("")
1242 elif command == "MONOCHROME" or command == "MONO":
1243 if gColor.setCodeSet(gColor.NONE):
1244 self.save("")
1245 elif command == "EXPORT":
1246 self.exportTasks()
1247 elif command == "IMPORT":
1248 if self.importTasks(line):
1249 self.save("")
1250 elif command == "CLEAR" and line == "":
1251 if self.clear():
1252 self.save("")
1253 elif command == "FILTER" or command == "FI" or command == "=":
1254 self.setFilterArray(False, line)
1255 elif command == "NEXT" or command == "N":
1256 self.incTaskLoop()
1257 elif command == "PREV" or command == "P":
1258 self.decTaskLoop()
1259 elif command == "TOP" or command == "T" or command == "0":
1260 self.currentTask = 0
1261 if line != "":
1262 self.setFilterArray(True, "")
1263 self.showLocalFilter()
1264 self.printShortList(line)
1265 truncateTask = True
1266 elif command == "GO" or command == "G":
1267 self.moveTo(line)
1268 elif command == "IMMEDIATE" or command == "I" or command == "++":
1269 newItem = self.createItem(":d+0 " + line)
1270 if newItem.hasHiddenTask():
1271 clearScreen(self.sysCalls)
1272 if newItem.hasError():
1273 print "Errors were found:"
1274 print newItem.getError()
1275 print "The task was not added."
1276 printCurrent = False
1277 else:
1278 self.todo.insert(0, newItem)
1279 self.currentTask = 0
1280 self.sortByPriority()
1281 self.save("")
1282 elif command == "KILL" or command == "K" or command == "-" or command == "X":
1283 if self.removeTask(line):
1284 self.save("")
1285 elif command == "ARCHIVE" or command == "DONE":
1286 if self.archiveTask(line):
1287 self.save("")
1288 elif command == "REP" or command =="R":
1289 if self.modifyTask(line, TodoItem.REPLACE):
1290 self.sortByPriority()
1291 self.save("")
1292 else:
1293 printCurrent = False
1294 elif command == "SUB" or command == "SU":
1295 if self.substituteText(line):
1296 self.sortByPriority()
1297 self.save("")
1298 elif command == "EDIT" or command == "ED":
1299 if not self.sysCalls:
1300 self.showError("External editing needs to use system calls. Use SYS ON to enable them.")
1301 elif line == "":
1302 self.addTaskExternal()
1303 elif self.modifyTask(line, TodoItem.MODIFY, externalEditor = True):
1304 self.sortByPriority()
1305 self.save("")
1306 else:
1307 printCurrent = False
1308 elif command == "MOD" or command == "M":
1309 if self.modifyTask(line, TodoItem.MODIFY):
1310 self.sortByPriority()
1311 self.save("")
1312 else:
1313 printCurrent = False
1314 elif command == "EXTEND" or command == "E":
1315 if self.modifyTask(line, TodoItem.APPEND):
1316 self.sortByPriority()
1317 self.save("")
1318 else:
1319 printCurrent = False
1320 elif command == "FIRST" or command == "F":
1321 if self.moveTask(line, self.MOVE_TOP):
1322 self.sortByPriority()
1323 self.save("")
1324 self.currentTask = 0
1325 elif command == "DOWN" or command == "D":
1326 if self.moveTask(line, self.MOVE_DOWN):
1327 self.sortByPriority()
1328 self.save("")
1329 elif command == "UP" or command == "U":
1330 if self.moveTask(line, self.MOVE_UP):
1331 self.sortByPriority()
1332 self.save("")
1333 elif command == "LIST" or command == "L":
1334 print ruler
1335 self.setFilterArray(True, line)
1336 self.showLocalFilter()
1337 self.printList(False, "", "")
1338 self.clearFilterArray(True)
1339 print ruler
1340 truncateTask = True
1341 elif command == "LIST>" or command == "L>":
1342 self.startHtml("")
1343 self.setFilterArray(True, line)
1344 self.showLocalFilter()
1345 self.printList(False, "", "")
1346 self.clearFilterArray(True)
1347 self.endHtml()
1348 elif command == "@":
1349 self.listByAction()
1350 truncateTask = True
1351 elif command == ":P":
1352 self.listByProject()
1353 truncateTask = True
1354 elif command == ":D":
1355 self.listByDate()
1356 truncateTask = True
1357 elif command == "@>":
1358 self.startHtml("Report by Context")
1359 self.listByAction()
1360 self.endHtml()
1361 elif command == ":P>":
1362 self.startHtml("Report by Project")
1363 self.listByProject()
1364 self.endHtml()
1365 elif command == ":D>":
1366 self.startHtml("Report by Date")
1367 self.listByDate()
1368 self.endHtml()
1369 elif command == "ADD" or command == "A" or command == "+":
1370 self.addTask(line)
1371 elif command == "NOTE" or command == "NOTES":
1372 self.addTask("#0 @Notes " + line)
1373 elif (len(command) + len(line)) > 10:
1374 self.addTask(rawcommand + " " + line)
1375 elif len(command) > 0:
1376 self.showError("Didn't understand. (Make sure you have a space after the command or your entry is longer than 10 characters)")
1377 printCurrent = False
1378 return reopen
1380 def timeout(self):
1381 self.timerActive = False
1382 clearScreen()
1383 print "\n\x07Timer\x07 complete.\x07\n\x07Press enter to continue.\x07"
1385 def runTimer(self, delay):
1386 self.timerActive = True
1387 t = Timer(delay * 60 , self.timeout)
1388 t.start()
1389 s = raw_input(str(delay) + " minute timer running.\nAny entry will cancel the timer:\n>>>")
1390 if self.timerActive:
1391 t.cancel()
1392 print "Timer cancelled."
1393 elif s != "":
1394 s = ""
1395 print "Input discarded as timer has finished."
1396 return s.strip()
1398 def addTaskExternal(self):
1399 exEdit = EditorLauncher()
1400 entry = exEdit.edit("")
1401 if entry != "":
1402 self.addTask(entry)
1403 else:
1404 self.showError("Nothing to add")
1406 def addTask(self, line):
1407 newItem = self.createItem(line)
1408 if newItem.hasError():
1409 print "Errors were found:"
1410 print newItem.getError()
1411 print "The task was not added."
1412 printCurrent = False
1413 else:
1414 if newItem.hasHiddenTask():
1415 clearScreen(self.sysCalls)
1416 self.todo.append(newItem)
1417 self.sortByPriority()
1418 self.save("")
1420 def checkCurrentTask(self):
1421 if self.currentTask > len(self.todo) - 1:
1422 self.currentTask = len(self.todo) - 1
1423 if self.currentTask < 0:
1424 self.currentTask = 0
1426 def writeArchive(self, item):
1427 success = False
1428 filename = self.filename + ".archive.dat"
1429 try:
1430 if not os.path.exists(filename):
1431 f = open(filename,"wb")
1432 f.write("# " + notice[0] + "\n")
1433 f.write(magicTag + "DATA\n")
1434 else:
1435 f = open(filename,"a+b")
1437 f.write(item.toString())
1438 f.write("\n")
1439 f.close()
1440 print "Tasks archived to " + filename
1441 success = True
1442 except Exception, e:
1443 self.showError("Error trying to archive the tasks.\n" + str(e))
1444 return success
1446 def exportTasks(self):
1447 filename = self.filename + ".tasks.txt"
1448 try:
1449 f = open(filename,"wb")
1450 f.write("# " + notice[0] + "\n")
1451 f.write(magicTag + "DATA\n")
1452 for item in self.todo:
1453 f.write(item.toString())
1454 f.write("\n")
1455 f.close()
1456 print "Tasks exported to " + filename
1457 except Exception, e:
1458 self.showError("Error trying to export the file.\n" + str(e))
1460 def importTasks(self, filename):
1461 success = False
1462 orgNTasks = len(self.todo)
1463 if filename == "":
1464 self.showError("You must supply the name of the file to import.")
1465 return success
1467 try:
1468 self.splitFile(filename, True)
1469 if len(self.todo) == orgNTasks:
1470 self.showError("Failed to find any tasks to import.")
1471 else:
1472 success = True
1473 except Exception, e:
1474 self.showError("Error importing tasks. " + str(e))
1475 return success
1477 def createFile(self, filename):
1478 success = False
1479 if os.path.exists(filename):
1480 self.showError("Sorry but " + filename + " already exists.")
1481 else:
1482 try:
1483 f = open(filename, "wb")
1484 f.write("#!/usr/bin/env python\n")
1485 f.write("#" + ruler + "\n")
1486 f.close()
1487 success = True
1488 except Exception, e:
1489 self.showError("Error trying to create the file " + filename + ". " + str(e))
1490 return success
1492 def save(self, filename):
1493 if filename != "" or self.autoSave:
1494 self.forceSave(filename)
1495 else:
1496 self.dirty = True
1497 print "Autosave is off, so changes not saved yet."
1499 def forceSave(self, filename):
1500 if filename == "":
1501 filename = self.filename
1502 tmpFilename = filename + ".tmp"
1503 backupFilename = filename + ".bak"
1504 success = False
1505 try:
1506 f = open(tmpFilename,"wb")
1507 f.write("#!/usr/bin/env python\n")
1508 f.write("# -*- coding: utf-8 -*-\n")
1509 f.write("#" + ruler + "\n")
1510 f.write("# Run the script for details of the licence\n")
1511 f.write("# or refer to the notice section later in the file.\n")
1512 f.write("#" + ruler + "\n")
1513 f.write(magicTag + "DATA\n")
1514 for item in self.todo:
1515 f.write(item.toString())
1516 f.write("\n")
1517 f.write(magicTag + "CONFIG\n")
1518 f.write("cfgColor = " + str(gColor.getCodeSet()) + "\n")
1519 f.write("cfgAutoSave = " + str(self.autoSave) + "\n")
1520 f.write("cfgReviewMode = " + str(self.review) + "\n")
1521 f.write("cfgSysCalls = " + str(self.sysCalls) + "\n")
1522 f.write("cfgEditorNt = \"" + cfgEditorNt + "\"\n")
1523 f.write("cfgEditorPosix = \"" + cfgEditorPosix + "\"\n")
1524 f.write("cfgShortcuts = " + str(self.shortcuts) + "\n")
1525 f.write("cfgAbbreviations = " +str(globalAbbr.toString()) +"\n")
1526 f.write("cfgPAbbreviations = " +str(globalPAbbr.toString()) +"\n")
1527 f.write(magicTag + "CODE\n")
1528 for codeline in self.code:
1529 f.write(codeline.rstrip())
1530 f.write("\n")
1531 f.close()
1532 success = True
1534 except Exception, e:
1535 self.showError("Error trying to save the file.\n" + str(e))
1536 if success:
1537 try:
1538 os.remove(backupFilename)
1539 except Exception:
1540 pass
1541 try:
1542 oldstat = os.stat(filename)
1543 os.rename(filename, backupFilename)
1544 os.rename(tmpFilename, filename)
1545 os.chmod(filename, stat.S_IMODE(oldstat.st_mode)) # ensure permissions carried over
1546 self.filename = filename
1547 self.dirty = False
1548 print "Tasks saved."
1549 except Exception, e:
1550 self.showError("Error trying to rename the backups.\n" + str(e))
1552 def moveTo(self, indexStr):
1553 try:
1554 index = int(indexStr, 10)
1555 if index < 0 or index > len(self.todo) - 1:
1556 self.showError("Sorry but there is no task " + indexStr)
1557 else:
1558 if not self.isViewable(self.todo[index]):
1559 print "Switching off your filter so that the task can be displayed."
1560 self.clearFilterArray(False)
1561 self.currentTask = index
1562 except ValueError:
1563 self.showError("Unable to understand the task " + indexStr + " you want to show.")
1566 def moveToVisible(self):
1567 start = self.currentTask
1568 find = True
1569 if start < 0 or start >= len(self.todo):
1570 return
1571 while not self.isViewable(self.todo[self.currentTask]):
1572 self.incTaskLoop()
1573 if self.currentTask == start:
1574 print "Nothing matched your filter. Removing your filter so that the current task can be displayed."
1575 self.clearFilterArray(False)
1576 break
1578 def decrypt(self, indexStr):
1579 index = self.getRequiredTask(indexStr)
1580 if index < 0 or index > len(self.todo) - 1:
1581 self.showError("Sorry but there is no task " + indexStr + " to show.")
1582 elif self.todo[index].hasHiddenTask():
1583 ec = Encryptor()
1584 print WordWrapper(gMaxLen).wrap(ec.enterKeyAndDecrypt(self.todo[index].getHiddenTask()))
1585 else:
1586 print "Task ", index, " has no encrypted data."
1588 def moveTask(self, indexStr, where):
1589 success = False
1590 if indexStr == "":
1591 print "You must supply the number of the task to move."
1592 return False
1593 try:
1594 index = self.getRequiredTask(indexStr)
1595 if index < 0 or index > len(self.todo) - 1:
1596 self.showError("Sorry but there is no task " + indexStr + " to move.")
1597 elif where == self.MOVE_DOWN:
1598 if index <= len(self.todo) - 2:
1599 item = self.todo[index]
1600 self.todo[index] = self.todo[index + 1]
1601 self.todo[index + 1] = item
1602 print "Task ", index, " moved down."
1603 success = True
1604 else:
1605 self.showError("Task " + str(index) + " is already at the bottom.")
1606 else:
1607 if index > 0:
1608 if where == self.MOVE_TOP:
1609 self.todo.insert(0, self.todo.pop(index))
1610 else:
1611 dest = index - 1
1612 item = self.todo[dest]
1613 self.todo[dest] = self.todo[index]
1614 self.todo[index] = item
1615 print "Task ", index, " moved up."
1616 success = True
1617 else:
1618 self.showError("Task " + str(index) + " is already at the top.")
1621 except ValueError:
1622 self.showError("Unable to understand the task " + indexStr + " you want to move.")
1624 return success
1626 def clear(self):
1627 cleared = False
1628 if safeRawInput("Are you really sure you want to remove everything? Yes or No? >>>").upper() != "YES":
1629 print("Nothing has been removed.")
1630 else:
1631 del self.todo[0:]
1632 self.currentTask = 0
1633 cleared = True
1634 return cleared
1636 def getRequiredTask(self, indexStr):
1637 if indexStr == "^" or indexStr.upper() == "THIS":
1638 index = self.currentTask
1639 else:
1640 try:
1641 index = int(indexStr, 10)
1642 except ValueError:
1643 index = -1
1644 return index
1646 def archiveTask(self, indexStr):
1647 doit = False
1648 line = indexStr.split(" ", 1)
1649 if len(line) > 1:
1650 indexStr = line[0]
1651 entry = line[1]
1652 else:
1653 entry = ""
1654 index = self.getRequiredTask(indexStr)
1655 if index < 0 or index > len(self.todo) - 1:
1656 self.showError("Sorry but there is no task " + indexStr + " to mark as done and archive.")
1657 else:
1658 if indexStr == "^" or indexStr.upper() == "THIS":
1659 doit = True
1660 else:
1661 print "Are you sure you want to archive: ' " + self.todo[index].toStringSimple() + "'"
1662 if safeRawInput("Enter Yes to archive this task? >>>").upper() == "YES":
1663 doit = True
1664 if doit:
1665 newItem = self.createItem(":d+0")
1666 self.todo[index].copy(newItem, TodoItem.MODIFY)
1667 newItem = self.createItem(entry + " @Archived")
1668 self.todo[index].copy(newItem, TodoItem.APPEND)
1669 if self.writeArchive(self.todo[index]):
1670 self.todo[index:index + 1] = []
1671 print "Task ", index, " has been archived."
1672 else:
1673 doit = False
1674 print "Task ", index, " marked as archived but not removed."
1675 else:
1676 print "Task ", index, " has not been archived."
1677 return doit
1679 def removeTask(self, indexStr):
1680 doit = False
1681 index = self.getRequiredTask(indexStr)
1682 if index < 0 or index > len(self.todo) - 1:
1683 self.showError("Sorry but there is no task " + indexStr + " to delete.")
1684 else:
1685 if indexStr == "^" or indexStr.upper() == "THIS":
1686 doit = True
1687 else:
1688 print "Are you sure you want to remove ' " + self.todo[index].toStringSimple() + "'"
1689 if safeRawInput("Enter Yes to delete this task? >>>").upper() == "YES":
1690 doit = True
1691 if doit:
1692 self.todo[index:index + 1] = []
1693 print "Task ", index, " has been removed."
1694 else:
1695 print "Task ", index, " has not been removed."
1696 return doit
1698 def substituteText(self, indexStr):
1699 line = indexStr.split(" ", 1)
1700 if len(line) > 1:
1701 indexStr = line[0]
1702 entry = line[1]
1703 else:
1704 self.showError("You need to define the task and substitution phrases. e.g SUB 0 /old/new/")
1705 return False
1708 success = False
1709 if indexStr == "":
1710 print "You must supply the number of the task to change."
1711 return False
1713 index = self.getRequiredTask(indexStr)
1715 if index < 0 or index > len(self.todo) - 1:
1716 self.showError("Sorry but there is no task " + indexStr)
1717 else:
1718 text = entry.replace("/", "\n")
1719 text = text.replace("\\\n","/")
1720 phrases = text.split("\n")
1721 if len(phrases) != 4:
1722 self.showError("The format of the command is incorrect. The substitution phrases should be /s1/s2/ ")
1723 return False
1724 oldText = self.todo[index].getTask()
1725 newText = oldText.replace(phrases[1], phrases[2])
1726 if newText == oldText:
1727 self.showError("Nothing has changed.")
1728 return False
1729 newItem = self.createItem(newText)
1730 if newItem.hasError():
1731 print "With the substitution the task had errors:"
1732 print newItem.getError()
1733 print "Task ", index, " is unchanged."
1734 else:
1735 if newItem.hasHiddenTask():
1736 clearScreen(self.sysCalls)
1737 self.showError("It isn't possible to create private or secret data by using the substitition command.")
1738 else:
1739 self.todo[index].copy(newItem, TodoItem.MODIFY)
1740 print "Task ", index, " has been changed."
1741 success = True
1742 return success
1745 def modifyTask(self, indexStr, replace, externalEditor = False):
1746 line = indexStr.split(" ", 1)
1747 if len(line) > 1:
1748 indexStr = line[0]
1749 entry = line[1]
1750 else:
1751 entry = ""
1753 success = False
1754 if indexStr == "":
1755 print "You must supply the number of the task to change."
1756 return
1758 index = self.getRequiredTask(indexStr)
1760 if index < 0 or index > len(self.todo) - 1:
1761 self.showError("Sorry but there is no task " + indexStr)
1762 else:
1763 if entry == "":
1764 if externalEditor:
1765 exEdit = EditorLauncher()
1766 (key, entry) = self.todo[index].toStringEditable()
1767 entry = exEdit.edit(entry)
1768 else:
1769 if replace == TodoItem.REPLACE:
1770 print "This task will completely replace the existing entry,"
1771 print "including any projects and actions."
1772 elif replace == TodoItem.MODIFY:
1773 print "Only the elements you add will be replaced. So, for example,"
1774 print "if you don't enter any projects the original projects will remain."
1775 else:
1776 print "Elements you enter will be appended to the current task"
1777 entry = safeRawInput("Enter new details >>>")
1778 if entry != "":
1779 if replace == TodoItem.APPEND:
1780 newItem = self.createItem(entry, password="unused") # we will discard the encrypted part on extend
1781 elif externalEditor:
1782 newItem = self.createItem(entry, password = key)
1783 else:
1784 newItem = self.createItem(entry)
1785 if newItem.hasHiddenTask():
1786 clearScreen(self.sysCalls)
1787 if newItem.hasError():
1788 print "The task had errors:"
1789 print newItem.getError()
1790 print "Task ", index, " is unchanged."
1791 else:
1792 if newItem.hasHiddenTask() and replace == TodoItem.APPEND:
1793 self.showError("It isn't possible to extend the encrypted part of a task.\nThis part is ignored.")
1794 self.todo[index].copy(newItem, replace)
1795 print "Task ", index, " has been changed."
1796 success = True
1797 else:
1798 print "Task ", index, " has not been touched."
1799 return success
1801 def incTask(self):
1802 if self.currentTask < len(self.todo) - 1:
1803 self.currentTask = self.currentTask + 1
1805 def incTaskLoop(self):
1806 if self.currentTask < len(self.todo) - 1:
1807 self.currentTask = self.currentTask + 1
1808 else:
1809 self.currentTask = 0
1810 def decTask(self):
1811 if self.currentTask > 0:
1812 self.currentTask = self.currentTask - 1
1814 def decTaskLoop(self):
1815 if self.currentTask > 0:
1816 self.currentTask = self.currentTask - 1
1817 else:
1818 self.currentTask = len(self.todo) - 1
1820 def printItemTruncated(self, index, leader):
1821 if len(self.todo) < 1:
1822 print leader, "no tasks"
1823 else:
1824 scrnline = leader + "[%02d] %s" % (index, self.todo[index].toStringSimple())
1825 if len(scrnline) > gMaxLen:
1826 print scrnline[0:gMaxLen - 3] + "..."
1827 else:
1828 print scrnline
1831 def printItem(self, index, colorType):
1832 if len(self.todo) < 1:
1833 self.output("There are no tasks to be done.\n", 0)
1834 nlines = 1
1835 else:
1836 wrapper = WordWrapper(gMaxLen)
1837 scrnline = wrapper.wrap("[%02d] %s" % (index, self.todo[index].toStringSimple()))
1838 if colorType == "row0":
1839 style = "class=\"evenTask\""
1840 else:
1841 style = "class=\"oddTask\""
1842 self.output("<div %s>[%02d] %s</div>\n" % (style, index, self.todo[index].toStringSimple()),
1843 gColor.code(colorType) + scrnline + gColor.code("normal") + "\n" )
1844 nlines = wrapper.getNLines()
1845 return nlines
1847 def printItemVerbose(self, index):
1848 if len(self.todo) < 1:
1849 print "There are no tasks to be done."
1850 else:
1851 self.showFilter()
1852 wrapper = WordWrapper(gMaxLen)
1853 scrnline = wrapper.wrap("[%02d] %s" % (index, self.todo[index].toStringVerbose()))
1854 print scrnline
1856 def clearFilterArray(self, local):
1857 if local:
1858 self.localFilters = []
1859 self.localFilterText = ""
1860 else:
1861 self.globalFilters = []
1862 self.globalFilterText = ""
1864 def setFilterArray(self, local, requiredFilter):
1865 filters = requiredFilter.split()
1866 if local:
1867 destination = self.localFilters
1868 else:
1869 destination = self.globalFilters
1870 destination[:] = []
1871 humanVersion = ""
1872 for word in filters:
1873 if word[0:1] == "-":
1874 invert = True
1875 word = word[1:]
1876 else:
1877 invert = False
1878 if word[0:2].upper() == ":D" and len(word) > 2:
1879 filter = ":D" + TodoItem("").parseDate(word[2:].strip(), False)
1880 elif word[0:2].lower() == ":p":
1881 filter = globalPAbbr.expandProject(word)
1882 else:
1883 filter = globalAbbr.expandAction(word)
1884 if invert:
1885 filter = "-" + filter
1886 destination.append(filter)
1887 if humanVersion != "":
1888 humanVersion = humanVersion + " " + filter
1889 else:
1890 humanVersion = filter
1891 if local:
1892 for filter in self.globalFilters:
1893 destination.append(filter)
1894 if humanVersion != "":
1895 humanVersion = humanVersion + " " + filter
1896 else:
1897 humanVersion = filter
1898 if local:
1899 self.localFilterText = humanVersion
1900 else:
1901 self.globalFilterText = humanVersion
1903 def isViewable(self, item):
1904 if len(self.globalFilters) == 0 and len(self.localFilters) == 0:
1905 return True
1906 overallView = True
1907 ored = False
1908 if len(self.localFilters) > 0:
1909 filterArray = self.localFilters
1910 else:
1911 filterArray = self.globalFilters
1912 if "or" in filterArray or "OR" in filterArray:
1913 fast = False
1914 else:
1915 fast = True
1916 for filter in filterArray:
1917 if filter.upper() == "OR":
1918 ored = True
1919 continue
1920 view = False
1921 usePriority = False
1922 mustHave = False
1923 if filter[0:1] == "+":
1924 filter = filter[1:]
1925 mustHave = True
1926 if filter[0:1] == "-":
1927 invert = True
1928 filter = filter[1:]
1929 else:
1930 invert = False
1931 try:
1932 if filter[0:1] == "#":
1933 priority = int(filter[1:], 10)
1934 usePriority = True
1935 except ValueError:
1936 priority = 0
1937 if usePriority:
1938 if self.exactPriority:
1939 if item.hasPriority(priority):
1940 view = True
1941 elif item.hasPriorityOrAbove(priority):
1942 view = True
1943 elif filter[0:2].upper() == ":D":
1944 if item.hasDate(filter[2:]):
1945 view = True
1946 elif filter[0:2].upper() == ":P":
1947 view = item.hasProject(filter)
1948 elif filter[0:1].upper() == "@":
1949 view = item.hasAction(filter)
1950 elif item.hasWord(filter):
1951 view = True
1952 if invert:
1953 view = (view != True)
1954 if ored:
1955 if view == True:
1956 overallView = True
1957 break
1958 else:
1959 if view == False:
1960 overallView = False
1961 if fast or mustHave:
1962 break
1963 ored = False
1964 return overallView
1966 def listByAction(self):
1967 index = SearchIndex()
1968 for item in self.todo:
1969 index.addCollection(item.getActions())
1970 index.sort()
1971 (n, value) = index.getFirstItem()
1972 print ruler
1973 self.showFilter()
1974 while n >= 0:
1975 if not gColor.usingColor() and n > 0:
1976 div = True
1977 else:
1978 div = False
1979 self.setFilterArray(True, "+" + value)
1980 self.printList(div, "<H2 class=\"hAction\">" + value + "</H2>\n", gColor.code("title") + "\n" + value + "\n" + gColor.code("title"))
1981 self.clearFilterArray(True)
1982 (n, value) = index.getNextItem(n)
1983 print ruler
1985 def listByProject(self):
1986 index = SearchIndex()
1987 for item in self.todo:
1988 index.addCollection(item.getProjects())
1989 index.sort()
1990 (n, value) = index.getFirstItem()
1991 print ruler
1992 self.showFilter()
1993 while n >= 0:
1994 if not gColor.usingColor() and n > 0:
1995 div = True
1996 else:
1997 div = False
1998 self.setFilterArray(True, "+:p" + value)
1999 self.printList(div, "<H2 class =\"hProject\">Project: " + value + "</H2>\n", gColor.code("title") + "\nProject: " + value + gColor.code("normal") + "\n")
2000 self.clearFilterArray(True)
2001 (n, value) = index.getNextItem(n)
2002 print ruler
2004 def listByDate(self):
2005 index = SearchIndex()
2006 for item in self.todo:
2007 index.add(item.getDate())
2008 index.sort()
2009 (n, value) = index.getFirstItem()
2010 print ruler
2011 self.showFilter()
2012 while n >= 0:
2013 if not gColor.usingColor() and n > 0:
2014 div = True
2015 else:
2016 div = False
2017 self.setFilterArray(True, "+:D" + value)
2018 self.printList(div, "<H2 class =\"hDate\">Date: " + value + "</H2>\n", gColor.code("title") + "\nDate: " + value + gColor.code("normal") + "\n")
2019 self.clearFilterArray(True)
2020 (n, value) = index.getNextItem(n)
2021 print ruler
2024 def showFilter(self):
2025 if self.globalFilterText != "":
2026 self.output("<H3 class =\"hFilter\">Filter = " + self.globalFilterText + "</H3>\n",
2027 gColor.code("bold") + "Filter = " + self.globalFilterText + gColor.code("normal") + "\n")
2029 def showLocalFilter(self):
2030 if self.localFilterText != "":
2031 self.output("<H3 class =\"hFilter\">Filter = " + self.localFilterText + "</H3>\n",
2032 gColor.code("bold") + "Filter = " + self.localFilterText + gColor.code("normal") + "\n")
2034 def printList(self, div, outHtml, outStd):
2035 self.doPrintList(-1, div, outHtml, outStd)
2037 def printShortList(self, line):
2038 count = 0
2039 try:
2040 count = int(line, 10)
2041 except ValueError:
2042 self.showError("Didn't understand the number of tasks you wanted listed.")
2043 self.doPrintList(count, False, "", "")
2045 def doPrintList(self, limitItems, div, outHtml, outStd):
2046 n = 0
2047 displayed = 0
2048 count = 0
2049 color = "row0"
2050 first = True
2051 maxlines = 20
2052 if outHtml != "":
2053 self.outputHtml("<div class=\"itemGroup\">\n")
2054 for item in self.todo:
2055 if self.isViewable(item):
2056 if first:
2057 if div:
2058 print divider
2059 self.output(outHtml, outStd)
2060 if not gColor.usingColor() and not first:
2061 print divider
2062 count = count + 1
2063 count = count + self.printItem(n, color)
2064 first = False
2065 if color == "row0":
2066 color = "row1"
2067 else:
2068 color = "row0"
2069 displayed = displayed + 1
2070 n = n + 1
2071 if limitItems >= 0 and displayed >= limitItems:
2072 break
2073 if count >= maxlines:
2074 if self.htmlFile == "":
2075 msg = safeRawInput("---press Enter for more. Enter s to skip: ")
2076 if len(msg) > 0 and msg.strip().upper()[0] == "S":
2077 break;
2078 count = 0
2079 if outHtml != "":
2080 self.outputHtml("</div>\n")
2082 def printHelp(self, lines):
2083 ListViewer(24).show(lines,"!PAUSE!")
2085 def splitFile(self, filename, dataOnly):
2086 inData = False
2087 inCode = False
2088 inCfg = False
2089 f = open(filename, 'r')
2090 line = f.readline()
2091 if line[0:2] == "#!":
2092 line = f.readline()
2093 while line != "":
2094 if line.find(magicTag + "DATA") == 0:
2095 inData = True
2096 inCode = False
2097 inCfg = False
2098 elif line.find(magicTag + "CONFIG") == 0:
2099 inData = False
2100 inCode = False
2101 inCfg = True
2102 elif line.find(magicTag + "CODE") == 0:
2103 inCode = True
2104 inData = False
2105 inCfg = False
2106 if dataOnly:
2107 break
2108 elif inCode:
2109 self.code.append(line)
2110 elif inCfg:
2111 self.processCfgLine(line)
2112 elif inData:
2113 line = line.strip()
2114 if len(line) > 0 and line[0] == "#":
2115 line = line[1:].strip()
2116 if len(line) > 0:
2117 newItem = self.createItem(line)
2118 newItem.getError()
2119 self.todo.append(newItem)
2121 line = f.readline()
2122 f.close()
2124 def createItem(self, line, password = ""):
2125 item = TodoItem(line, password)
2126 return item
2128 def outputHtml(self, html):
2129 if self.htmlFile != "":
2130 self.htmlFile.write(html)
2132 def output(self, html, stdout):
2133 if self.htmlFile != "":
2134 #self.htmlFile.write(html.replace("\n", "<br>\n"))
2135 self.htmlFile.write(html)
2136 if stdout == 0:
2137 print html,
2138 else:
2139 if stdout:
2140 print stdout,
2142 def startHtml(self, title):
2143 htmlFilename = self.filename + ".html"
2144 try:
2145 self.htmlFile = open(htmlFilename, "w")
2146 self.htmlFile.write("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n")
2147 self.htmlFile.write("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n")
2148 self.htmlFile.write("<html>\n<head>\n")
2149 self.htmlFile.write("<style>\n")
2150 self.htmlFile.write(".footer {text-align:center;}\n")
2151 self.htmlFile.write("</style>\n")
2152 self.htmlFile.write("<link rel=\"stylesheet\" href=\"ikog.css\" type=\"text/css\">\n")
2153 self.htmlFile.write("</head>\n<body>\n")
2154 self.htmlFile.write("<div class=\"header\">\n")
2155 self.htmlFile.write("<H1 class=\"hTitle\">iKog Todo List</H1>\n")
2156 self.htmlFile.write("<H2 class=\"hSubTitle\">" + title + " printed " + date.today().isoformat() + "</H1>\n")
2157 self.htmlFile.write("</div>\n")
2158 self.htmlFile.write("<div class=\"taskArea\">\n")
2159 except Exception:
2160 print "Failed to create output file:", htmlFilename
2161 self.htmlFile = ""
2163 def endHtml(self):
2164 name = self.htmlFile.name
2165 success = False
2166 try:
2167 self.htmlFile.write("</div>\n")
2168 self.htmlFile.write("<div class=\"footer\">\n")
2169 self.htmlFile.write("--- end of todo list ---<br>\n")
2170 self.htmlFile.write("Created using " + notice[0] + "\n<br>" + notice[1] + "<br>\n")
2171 self.htmlFile.write("</div>\n")
2173 self.htmlFile.write("</body>\n</html>\n")
2174 self.htmlFile.close()
2175 self.htmlFile = ""
2176 print "HTML file " + name + " created."
2177 success = True
2178 except Exception, e:
2179 self.showError("Error writing to file. " + str(e))
2181 if success:
2182 try:
2183 safeName = os.path.abspath(name).replace("\\","/")
2184 safeName = "file://" + urllib.quote(safeName," /:")
2185 webbrowser.open(safeName)
2186 except Exception, e:
2187 self.showError("Unable to launch html output. " + str(e))
2189 class Abbreviations:
2190 def __init__(self, project = False):
2191 self.default(project)
2193 def default(self,project):
2194 if project:
2195 self.abbrevs = {}
2196 else:
2197 self.abbrevs = {"@A":"@Anywhere","@C":"@Computer",
2198 "@D":"@Desk", "@E": "@Errands",
2199 "@H":"@Home", "@I":"@Internet","@L":"@Lunch", "@M":"@Meeting", "@N":"@Next",
2200 "@P":"@Phone", "@Pw":"@Password", "@S":"@Someday/Maybe",
2201 "@O":"@Other", "@W4":"@Waiting_For", "@W":"@Work"}
2204 def setAbbreviations(self, abbr):
2205 self.abbrevs.update(abbr)
2207 def addAbbreviation(self, key, word):
2208 self.abbrevs.update({key.title():word})
2210 def removeAbbreviation(self, key):
2211 key = key.title()
2212 if self.abbrevs.has_key(key):
2213 del self.abbrevs[key]
2214 return True
2215 return False
2217 def expandAction(self, action):
2218 if action[0:1] != "@":
2219 return action
2221 action = action.title()
2222 if self.abbrevs.has_key(action):
2223 return self.abbrevs[action]
2224 return action
2226 def expandProject(self, project):
2227 if not project.lower().startswith(":p"):
2228 return project
2229 project = project.title()
2230 if self.abbrevs.has_key(project):
2231 return self.abbrevs[project]
2232 return project
2234 def toString(self):
2235 return str(self.abbrevs)
2237 def toStringVerbose(self):
2238 output = ""
2239 index = 0
2240 for key in self.abbrevs:
2241 output = output + key.ljust(5) + " = " + self.abbrevs[key].ljust(30)
2242 index = index + 1
2243 if index % 2 == 0:
2244 output = output + "\n"
2245 if index % 2 != 0:
2246 output = output + "\n"
2247 return output
2249 class SearchIndex:
2250 def __init__(self):
2251 self.items = []
2253 def add(self, ent):
2254 if ent != "" and not ent in self.items:
2255 self.items.append(ent)
2257 def addCollection(self, collection):
2258 for ent in collection:
2259 if ent != "" and not ent in self.items:
2260 self.items.append(ent)
2261 def sort(self):
2262 self.items.sort()
2264 def getFirstItem(self):
2265 if len(self.items) > 0:
2266 return (0, self.items[0])
2267 else:
2268 return (-1, "")
2270 def getNextItem(self, count):
2271 count = count + 1
2272 if count > len(self.items) - 1:
2273 return (-1, "")
2274 else:
2275 return (count, self.items[count])
2276 return
2278 class TodoItem:
2279 ENCRYPTION_MARKER = "{}--xx"
2280 REPLACE = 0
2281 MODIFY = 1
2282 APPEND = 2
2283 NOT_DUE_PRIORITY = 0
2284 DEFAULT_PRIORITY = 5
2285 OVERDUE_PRIORITY = 11
2286 MEETING_PRIORITY = 10
2287 def __init__(self,line, password = ""):
2288 self.actions = []
2289 self.task = ""
2290 self.hiddenTask = ""
2291 self.projects = []
2292 self.priority = -1
2293 self.when = ""
2294 self.created = date.today().isoformat()
2295 self.error = ""
2296 self.autoAction = False
2297 self.autoProject = False
2298 self.nullDate = False
2299 self.parse(line, password)
2301 def makeSafeDate(self, year, month, day):
2302 done = False
2303 while not done:
2304 if day < 1:
2305 done = True
2306 else:
2307 try:
2308 newDate = date(year, month, day)
2309 done = True
2310 except ValueError:
2311 day = day - 1
2312 newDate = ""
2313 return newDate
2315 def parseDate(self, dateStr, quiet):
2316 dateStr = dateStr.replace("/","-")
2317 dateStr = dateStr.replace(":","-")
2318 entry = dateStr.split("-")
2319 n = len(entry)
2320 if n < 1 or n > 3:
2321 fail = True
2322 elif dateStr == "0":
2323 self.nullDate = True
2324 return ""
2325 else:
2326 try:
2327 now = date.today()
2328 if dateStr[0:1] == "+":
2329 days = int(dateStr[1:].strip(), 10)
2330 when = now + timedelta(days)
2331 else:
2332 if n == 3:
2333 year = int(entry[0], 10)
2334 month = int(entry[1], 10)
2335 day = int(entry[2], 10)
2336 elif n == 2:
2337 year = now.year
2338 month = int(entry[0], 10)
2339 day = int(entry[1], 10)
2340 else:
2341 year = now.year
2342 month = now.month
2343 day = int(entry[0], 10)
2344 if day < now.day:
2345 month = month + 1
2346 if month > 12:
2347 month = 1
2348 year = year + 1
2349 if year < 1000:
2350 year = year + 2000
2351 when = self.makeSafeDate(year, month, day)
2352 fail = False
2353 self.nullDate = False
2354 except ValueError:
2355 fail = True
2356 except:
2357 fail = True
2358 if fail:
2359 self.addError("Could not decode the date. Use :dYYYY/MM/DD")
2360 return ""
2361 else:
2362 return when.isoformat()
2364 def parse(self, line, password):
2365 self.error = ""
2366 words = line.split(" ")
2367 taskToHide = ""
2368 encrypt = ""
2369 start = 0
2370 ecmLen = len(self.ENCRYPTION_MARKER)
2371 for word in words[start:]:
2372 wordUC = word.strip().upper()
2373 if len(word) > 0:
2374 if encrypt != "":
2375 taskToHide = taskToHide + word.strip() + " "
2376 elif word[0:ecmLen] == self.ENCRYPTION_MARKER:
2377 self.hiddenTask = word[ecmLen:]
2378 elif wordUC.startswith("<PRIVATE>") or wordUC.startswith("<SECRET>") or wordUC.startswith("<S>") or wordUC.startswith("<P>"):
2379 encrypt = wordUC[1]
2380 try:
2381 pos = word.index(">")
2382 taskToHide = taskToHide + word[pos + 1:].strip() + " "
2383 except ValueError:
2384 pass
2385 elif word[0] == "@" and len(word) > 1:
2386 if wordUC == "@DATE":
2387 self.addError("@Date contexts should not be entered. Use :dYYYY-MM-DD")
2388 else:
2389 act = globalAbbr.expandAction(word.strip())
2390 if not act in self.actions:
2391 self.actions.append(act)
2392 elif word[0:1] == "#" and len(word) > 1 and self.priority == -1:
2393 try:
2394 self.priority = int(word[1:].strip(), 10)
2395 if self.priority < 1:
2396 self.priority = 0
2397 elif self.priority > 10:
2398 self.priority = 10
2399 except ValueError:
2400 self.addError("Did not understand priority.")
2401 self.priority = -1
2402 elif wordUC[0:2] == ":P" and len(word) > 2:
2403 proj = globalPAbbr.expandProject(word.strip())[2:].title()
2404 if not proj in self.projects:
2405 self.projects.append(proj)
2406 elif wordUC[0:8] == ":CREATED" and len(word) > 8:
2407 self.created = word[8:].strip()
2408 elif wordUC[0:2] == ":D" and len(word) > 2:
2409 self.when = self.parseDate(word[2:].strip(), False)
2410 else:
2411 self.task = self.task + word.strip() + " "
2412 if taskToHide != "":
2413 ec = Encryptor()
2414 if encrypt == "S":
2415 if ec.setType(ec.TYPE_AES) != ec.TYPE_AES:
2416 self.addError("AES encryption is not available.")
2417 taskToHide = ""
2418 else:
2419 ec.setType(ec.TYPE_OBSCURED)
2420 if taskToHide != "":
2421 if password == "":
2422 self.hiddenTask = ec.enterKeyAndEncrypt(taskToHide)
2423 else:
2424 ec.setKey(password)
2425 self.hiddenTask = ec.encrypt(taskToHide)
2427 if len(self.actions) == 0:
2428 self.actions.append("@Anywhere")
2429 self.autoAction = True
2430 if len(self.projects) == 0:
2431 self.projects.append("None")
2432 self.autoProject = True
2434 def addError(self, err):
2435 if len(self.error) > 0:
2436 self.error = self.error + "\n"
2437 self.error = self.error + err
2439 def hasError(self):
2440 return self.error != ""
2442 def getError(self):
2443 tmp = self.error
2444 self.error = ""
2445 return tmp
2447 def hasWord(self, word):
2448 return (self.task.upper().find(word.upper()) >= 0)
2450 def hasAction(self, loc):
2451 if self.when != "" and loc.upper() == "@DATE":
2452 return True
2453 else:
2454 return loc.title() in self.actions
2456 def copy(self, todoItem, replace):
2457 if replace == TodoItem.REPLACE or len(todoItem.task.strip()) > 0:
2458 if replace == TodoItem.APPEND:
2459 self.task = self.task + " ..." + todoItem.task
2460 else:
2461 self.task = todoItem.task
2462 if replace == TodoItem.REPLACE or todoItem.autoAction == False:
2463 if replace != TodoItem.APPEND:
2464 self.actions = []
2465 for loc in todoItem.actions:
2466 if not loc in self.actions:
2467 self.actions.append(loc)
2468 if replace == TodoItem.REPLACE or todoItem.autoProject == False:
2469 if replace != TodoItem.APPEND:
2470 self.projects = []
2471 for proj in todoItem.projects:
2472 if not proj in self.projects:
2473 self.projects.append(proj)
2474 if replace == TodoItem.REPLACE or (todoItem.when != "" or todoItem.nullDate == True):
2475 self.when = todoItem.when
2476 if todoItem.priority >= 0:
2477 self.priority = todoItem.priority
2479 if replace == TodoItem.REPLACE or len(todoItem.hiddenTask.strip()) > 0:
2480 if replace == TodoItem.APPEND:
2481 pass
2482 else:
2483 self.hiddenTask = todoItem.hiddenTask
2485 def hasHiddenTask(self):
2486 return self.hiddenTask != ""
2488 def hasTask(self):
2489 return len(self.task.strip()) > 0
2491 def hasProject(self, proj):
2492 if proj[0:2].upper() == ":P":
2493 proj = proj[2:]
2494 return proj.title() in self.projects
2496 def hasDate(self, dt):
2497 dt = self.parseDate(dt, True)
2498 if dt == "":
2499 return False
2500 else:
2501 return self.when == dt
2503 def hasPriorityOrAbove(self, priority):
2504 return (self.getEffectivePriority() >= priority)
2506 def hasPriority(self, priority):
2507 return (self.getEffectivePriority() == priority)
2509 def getHiddenTask(self):
2510 return self.hiddenTask
2512 def getTask(self):
2513 return self.task
2515 def getActions(self):
2516 if self.when == "":
2517 return self.actions
2518 else:
2519 return self.actions + ["@Date"]
2521 def getProjects(self):
2522 return self.projects
2524 def getDate(self):
2525 return self.when
2527 def getPriority(self):
2528 if self.priority < 0:
2529 return self.DEFAULT_PRIORITY
2530 else:
2531 return self.priority
2533 def getEffectivePriority(self):
2534 userP = self.getPriority()
2535 if self.when != "":
2536 if self.when <= date.today().isoformat():
2537 userP = self.OVERDUE_PRIORITY + userP
2538 if self.hasAction("@Meeting"):
2539 userP = userP + self.MEETING_PRIORITY
2540 else:
2541 userP = self.NOT_DUE_PRIORITY
2542 return userP
2545 def toString(self):
2546 entry = "#"
2547 entry = entry + " " + self.task
2548 for action in self.actions:
2549 entry = entry + " " + action
2550 for project in self.projects:
2551 entry = entry + " :p" + project
2552 entry = entry + " :created" + self.created
2553 if self.when != "":
2554 entry = entry + " :d" + self.when
2555 if self.priority >= 0:
2556 entry = entry + " #" + str(self.priority)
2557 if self.hiddenTask != "":
2558 entry = entry + " " + self.ENCRYPTION_MARKER + self.hiddenTask
2559 return entry
2561 def toStringEditable(self, includeHidden = False):
2562 password = ""
2563 entry = ""
2564 if self.when != "":
2565 entry = entry + ":d" + self.when + " "
2566 entry = entry + "%s #%d" % (self.task, self.getPriority())
2567 if len(self.actions) > 0:
2568 for action in self.actions:
2569 entry = entry + " " + action
2570 if len(self.projects) > 0:
2571 for project in self.projects:
2572 # skip the none tag
2573 if project != "None":
2574 entry = entry + " :p" + project
2575 if self.hiddenTask != "" and includeHidden:
2576 ec = Encryptor()
2577 entry = entry + " <" + Encryptor().getSecurityClass(self.hiddenTask)[0:1] + ">"
2578 entry = entry + ec.enterKeyAndDecrypt(self.hiddenTask)
2579 password = ec.getKey()
2580 return (password, entry.strip())
2582 def toStringSimple(self):
2583 entry = ""
2584 if self.when != "":
2585 entry = entry + "@Date " + self.when + " "
2586 entry = entry + "%s #%d" % (self.task, self.getPriority())
2587 if self.hiddenTask != "":
2588 entry = entry + " <*** " + Encryptor().getSecurityClass(self.hiddenTask) + " ***> "
2589 if len(self.actions) > 0:
2590 for action in self.actions:
2591 entry = entry + " " + action
2592 if len(self.projects) > 0:
2593 first = True
2594 for project in self.projects:
2595 # skip the none tag
2596 if project != "None":
2597 if first:
2598 entry = entry + " Projects: " + project
2599 first = False
2600 else:
2601 entry = entry + ", " + project
2602 #entry = entry + " [" + self.created + "]"
2603 return entry
2605 def toStringVerbose(self):
2606 entry = gColor.code("title") + self.task
2607 if self.hiddenTask != "":
2608 entry = entry + " <*** " + Encryptor().getSecurityClass(self.hiddenTask) + " ***> "
2609 entry = entry + gColor.code("bold") + "\nPriority: %02d" % (self.getPriority())
2610 if len(self.actions) or self.when != "" > 0:
2611 entry = entry + gColor.code("heading") + "\nContext: "
2612 if self.when != "":
2613 entry = entry + gColor.code("important") + "@Date " + self.when
2614 entry = entry + gColor.code("normal")
2615 for action in self.actions:
2616 entry = entry + " " + action;
2617 if len(self.projects) > 0:
2618 first = True
2619 for project in self.projects:
2620 if project != "None":
2621 if first:
2622 entry = entry + gColor.code("heading") + "\nProjects: " + gColor.code("normal");
2623 entry = entry + project
2624 first = False
2625 else:
2626 entry = entry + ", " + project
2627 entry = entry + gColor.code("normal") + "\nCreated: [" + self.created + "]"
2628 return entry
2630 ### Entry point
2631 #for line in notice:
2632 # print line
2634 pythonVer = platform.python_version()
2635 ver = pythonVer.split(".")
2636 if int(ver[0]) < gReqPythonMajor or (int(ver[0]) == gReqPythonMajor and int(ver[1]) < gReqPythonMinor):
2637 print "\nSorry but this program requires Python ", \
2638 str(gReqPythonMajor) + "." + str(gReqPythonMinor), \
2639 "\nYour current version is ", \
2640 str(ver[0]) + "." + str(ver[1]), \
2641 "\nTo run the program you will need to install the current version of Python."
2642 else:
2643 import webbrowser
2644 # signal.signal(signal.SIGINT, signalHandler)
2645 gColor = ColorCoder(cfgColor)
2646 globalAbbr = Abbreviations()
2647 globalPAbbr = Abbreviations(project=True)
2648 commandList = []
2649 if len(sys.argv) > 2:
2650 command = ""
2651 reopen = sys.argv[1]
2652 if reopen == ".":
2653 reopen = sys.argv[0] + ".dat"
2654 for word in sys.argv[2:]:
2655 if word == "/":
2656 commandList.append(command)
2657 command = ""
2658 else:
2659 command = command + word + " "
2660 commandList.append(command)
2661 elif len(sys.argv) > 1:
2662 reopen = sys.argv[1]
2663 else:
2664 reopen = sys.argv[0] + ".dat"
2665 while reopen != "":
2666 #print commandList
2667 todoList = TodoList(sys.argv[0], reopen)
2668 reopen = todoList.run(commandList)
2669 commandList = []
2670 print "Goodbye"