To Do List Manager (written in REBOL)

Based on Todo.sh shellscript over at: Todo.txt and GTD method. This small application leverages REBOL's (grammar-based) parse features, as well as REBOL's DSL capabilities (referred to as 'dialecting' in REBOL parlance). Finally, it also borrows a dynamic interpreter REPL technique which is explained in the article Dining with Dynamic Interpreters.

Instructions

This tiny app is written in REBOL and requires the free, cross-platform REBOL interpreter (Core or View, both under .5MB). Download REBOL/View here. The GUI text editor (triggered by the editor and gui commands) will not be functional in REBOL/Core.

To run this in REBOL, install the interpreter, and enter do %/PATH-TO-FILE/to-do-guru.r at the REBOL command prompt (">>"). (Note: Do not overlook the '%' in that expression, as percent symbols are used to identify the File datatype within REBOL.) To quit the app and return to the normal REBOL prompt, type quit (or use the q alias), or, alternatively, hit the Esc key. To exit the REBOL shell/interpreter completely, type quit at the command prompt of the REBOL console.

This app is programmed to create up to two files when necessary: When you add a task to your to-do list, a simple text log file (named 'todo.txt') will be written to your local drive. When you perform an archive operation, another file (named 'done.txt') will be written to the same directory. Look at the commands block ('cmds') in the script to learn the keyword commands and functionality available. Feel free to change/modify to create your preferred to do lexicon. Obviously there is a lot that can be improved upon (the dull print-tasks function is on the top of my list).

Sample Session

This program is currently designed to accept three forms of text in the following format:

keyword-command (optional integer (and/or) "string")

For string input, you need to type the double-quotes (use curly braces {} if the string contains double-quotes). I may be able to remove this requirement in future versions.

Sample User Session:

to-do >> add "take sonny boy to the ballgame @home"
to-do >> ls
CURRENT TASKS
2: @home alphabetize spice rack
3: take sonny boy to the ballgame @home
1: wake up and make the donuts @home
3 tasks listed.

to-do >> replace 3 "take son to get hair-cut @home"
to-do >> done 1
to-do >> ls
CURRENT TASKS
2: @home alphabetize spice rack
3: take son to get hair-cut @home
1: X wake up and make the donuts @home
3 tasks listed.

to-do >> archive
to-do >> ls
CURRENT TASKS
1: @home alphabetize spice rack
2: take son to get hair-cut @home
2 tasks listed.

Important Note: This script is a beta and has been tested lightly. Many design decisions have been made in the spirit of learning (as opposed to speed or efficiency). This script should work without modification on all platforms supported by REBOL 2.x (Win/Linux/Mac).


The Script

Download Script File Here
REBOL [
    Title: "To Do Guru"
    File-name: %to-do-guru.r
    Date: 10-June-2008
    Author: "Ed O'Connor"
    License: "Free for any/all US law-abiding use, including modification and distribution."
    Version: 0.0.1
]

cmds: [
    ['add     | 'create ]  set str string!                  (add-task str)        |
    ['replace | 'swap   ]  set id integer! set str string!  (replace-task id str) |
    ['append  | 'affix  ]  set id integer! set str string!  (append-task id str)  |
    ['pri     | 'rank   ]  set id integer! set str string!  (prioritize)          |
    ['done    | 'do   ]    set id integer!                  (mark-complete id)    |
    ['find    | 'search ]  set str string!                  (find-tasks str)      |
    ['listpri | 'lspri  ]                                   (print-tasks/pri)     |
    ['listhab | 'lshab  ]                                   (print-tasks/hab)     |
    ['list    | 'ls     ]                                   (print-tasks)         |
    ['help    | '?      ]                                   (print "help tbd")    |
    ['editor  | 'gui    ]                                   (launch-editor)       |
    ['archive | 'arch   ]                                   (archive)             |
    ['restore | 'rs     ]                                   (restore)             |
    ['quit    | 'q      ]  (print "Session ended." halt)
]

read-tasks: func [/local id][
    tasks: any [attempt [read/lines %todo.txt] copy []]
    id: 0
    forskip tasks 2 [
        id: id + 1
        insert tasks id
    ]
    sort/skip/compare tasks 2 2
]

save-tasks: func [/local task-write output out][
    task-write: copy tasks
    sort/skip task-write 2
    output: extract/index task-write 2 2
    out: make string! 100
    foreach item output [repend out [item newline]]
    write %todo.txt out
]

add-task: func [task [string!]][
    either not empty? trim task [
        next-id: add 1 (length? tasks) / 2
        repend tasks [next-id task]
        save-tasks
    ][
        print "Error: Missing task description."
    ]
]

append-task: func [index [integer!] fragm [string!] /local task][
    either find tasks index [
        task: select tasks index
        append task rejoin [" " fragm]
        save-tasks
    ][
        print "Task number not found."
    ]
]

replace-task: func [index [integer!] newstr [string!] /local task][
    either find tasks index [
        task: select tasks index
        task: newstr
        save-tasks
    ][
        print "Task number not found."
    ]
]

mark-complete: func [index [integer!] /local task][
    either > length? tasks 0 [
        task: select tasks index
        insert task "X "
        save-tasks
    ][
        print "Task number does not exist."
    ]
]

archive: does [
    either > length? tasks 0 [
        foreach [i t] tasks [
            if #"X" = first t [write/append %done.txt rejoin [t newline]]
        ]
        remove-each [i t] tasks [#"X" = first t]
        save-tasks
        read-tasks
    ][
        print "No tasks currently marked for archiving."
    ]
]

restore: does [
    done: any [attempt [read/lines %done.txt] copy []]
    either all [
        > length? done 0
        exists? %todo.txt
    ][
        foreach task done [
            unless #"X" = first task [write/append %todo.txt rejoin [task newline]]
        ]
        remove-each [i t] done [#"X" <> first t]
        write/lines %done.txt done
        save-tasks
        read-tasks
    ][
        print "Unable to restore tasks: Missing todo.txt and/or done.txt."
    ]
]

alpha: charset [#"A" - #"Z" #"a" - #"z"]
pri-format: ["(" some alpha ")"]

print-tasks: func [/pri /hab /local output] [
    output: copy []
    print "CURRENT TASKS"
    foreach [i t] tasks [
        if pri [parse/all t [some [pri-format (repend output [i t]) break | skip]]]
        if hab [parse/all t [any ["~recur"   (repend output [i t]) break | skip]]]
        unless any [pri hab][repend output [i t]]
    ]
    foreach [i t] output [print rejoin [i ": " t]]
    print [(length? output) / 2 "tasks listed." newline]
]

find-tasks: func [params [string!] /local criteria ][
    criteria: parse/all params " "
    rule: copy [skip]
    foreach param criteria [
        insert rule [(print rejoin [i ": " t]) break |]
        insert rule compose [(param)]
    ]
    foreach [i t] tasks [bind rule 'i parse/all t [some rule]]
]

launch-editor: does [
    either system/product = 'view [
        editor %todo.txt
    ][
        print "Sorry, REBOL/View is required to launch the GUI editor".
    ]
]

read-tasks

forever [
    unless parse load/all ask "to-do >> " [cmds] [print "? Input not recognized."]
]

Support

This script is provided as a starting point. REBOL is one of the easiest and most productive scripting languages around, so with a little patience you should be able to turn this idea into something interesting. Support for REBOL can be found in a handful of places -- I'd start on the REBOL mailing list (details provided here). Good luck!