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