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