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