7 Commits
v0.4 ... v1.1

2 changed files with 163 additions and 32 deletions

View File

@ -1,7 +1,8 @@
#Wed, 01 May 2013 09:40:28 -0500 #Mon, 05 Aug 2013 10:16:09 -0500
lib.local=true lib.local=true
name=jdb-gtd name=jdb-gtd
version=0.4 version=1.1
nailgun.classpath.dir=/home/jdbernard/programs/nailgun/classpath nailgun.classpath.dir=/home/jdbernard/programs/nailgun/classpath
executable.jar=true
build.number=5 main.class=com.jdblabs.gtd.cli.GTDCLI
build.number=2

View File

@ -10,7 +10,7 @@ import org.joda.time.DateTime
public class GTDCLI { public class GTDCLI {
public static final String VERSION = "0.4" public static final String VERSION = "1.1"
private static String EOL = System.getProperty("line.separator") private static String EOL = System.getProperty("line.separator")
private static GTDCLI nailgunInst private static GTDCLI nailgunInst
@ -96,6 +96,9 @@ public class GTDCLI {
case ~/cal|calendar/: calendar(parsedArgs); break case ~/cal|calendar/: calendar(parsedArgs); break
case ~/process/: process(parsedArgs); break case ~/process/: process(parsedArgs); break
case ~/list-copies/: listCopies(parsedArgs); break case ~/list-copies/: listCopies(parsedArgs); break
case ~/new/: newAction(parsedArgs); break
case ~/tickler/: tickler(parsedArgs); break
case ~/ls|list/: ls(parsedArgs); break;
default: default:
println "Unrecognized command: ${command}" println "Unrecognized command: ${command}"
break } } } break } } }
@ -114,15 +117,6 @@ public class GTDCLI {
println "" println ""
def response def response
def readline = {stdin.nextLine().trim()} def readline = {stdin.nextLine().trim()}
def prompt = { msg ->
if (msg instanceof List) msg = msg.join(EOL)
msg += "> "
print msg
def line
while(!(line = readline())) print msg
return line }
// 1. Is it actionable? // 1. Is it actionable?
if (!item.title) item.title = filenameToString(item.file) if (!item.title) item.title = filenameToString(item.file)
@ -267,7 +261,7 @@ public class GTDCLI {
if (inPath(gtdDirs.projects, oldFile)) { if (inPath(gtdDirs.projects, oldFile)) {
// Delete any copies of this item in the next actions folder. // Delete any copies of this item in the next actions folder.
findAllCopies(oldFile, gtdDrs."next-actions").each { file -> findAllCopies(oldFile, gtdDirs."next-actions").each { file ->
println "Deleting duplicate entry from the " + println "Deleting duplicate entry from the " +
"${file.parentFile.name} context." "${file.parentFile.name} context."
file.delete() } file.delete() }
@ -315,6 +309,7 @@ public class GTDCLI {
itemsOnCalendar.each { item -> itemsOnCalendar.each { item ->
def itemDay = new DateMidnight(item.date) def itemDay = new DateMidnight(item.date)
if (itemDay != currentDate) { if (itemDay != currentDate) {
if (currentDate != null) println ""
println itemDay.toString("EEE, MM/dd") println itemDay.toString("EEE, MM/dd")
println "----------" println "----------"
currentDate = itemDay } currentDate = itemDay }
@ -343,25 +338,106 @@ public class GTDCLI {
args.clear() } args.clear() }
protected void newAction(LinkedList args) {
def response = prompt(["Next action?", ""])
def file = new File(workingDir, stringToFilename(response))
file.createNewFile()
def item = new Item(file)
item.action = response
println "Enter extra info. One 'key: value' pair per line."
println "(ex: date: YYYY-MM-DD, project=my-project)"
println "End with an empty line."
print "> "
while (response = stdin.nextLine().trim()) {
if (!(response =~ /[:=]/)) continue
def parts = response.split(/[:=]/)
item[parts[0].trim().toLowerCase()] =
PropertyHelp.parse(parts[1].trim())
print "> " }
item.save() }
protected void tickler(LinkedList args) {
gtdDirs.tickler.eachFileRecurse { file ->
def item = new Item(file)
def today = new DateMidnight()
// If the item is scheduled to be tickled today (or in the past)
// then move it into the next-actions folder
if ((item.tickle as DateMidnight) <= today) {
println "Moving '${item.action}' out of the tickler."
def oldFile = item.file
item.file = new File(gtdDirs."next-actions",
stringToFilename(item.action))
item.gtdProperties.remove("tickle")
item.save()
oldFile.delete() }}}
protected void ls(LinkedList args) {
def target = args.poll()
def printItems = { dir ->
if (!dir.exists() || !dir.isDirectory()) return
println "-- ${getRelativePath(gtdDirs.root, dir)} --"
dir.eachFile { file ->
if (!file.exists() || !file.isFile() || file.isHidden())
return
def item = new Item(file)
println item.action }
println "" }
// If we have a named context or project, look for those items
// specifically
if (target) {
printItems(new File(gtdDirs['next-actions'], target))
printItems(new File(gtdDirs.waiting, target))
printItems(new File(gtdDirs.projects, target)) }
else {
printItems(gtdDirs['next-actions'])
printItems(gtdDirs['waiting'])
gtdDirs['next-actions'].eachDir(printItems)
gtdDirs['waiting'].eachDir(printItems) } }
protected void printUsage(LinkedList args) { protected void printUsage(LinkedList args) {
if (!args) { if (!args) {
println "Jonathan Bernard's Getting Things Done CLI v$VERSION" println """\
println "usage: gtd [option...] <command>..." Jonathan Bernard's Getting Things Done CLI v$VERSION
println "" usage: gtd [option...] <command>...
println "options are:"
println "" options are:
println " -h, --help Print this usage information."
println " -d, --directory Set the GTD root directory." -h, --help Print this usage information.
println " -v, --version Print the GTD CLI version." -d, --directory Set the GTD root directory.
println "" -v, --version Print the GTD CLI version.
println "top-leve commands:"
println "" top-level commands:
println " help <command> Print detailed help about a command."
println " process Process inbox items systematically." help <command> Print detailed help about a command.
println " done <action-file> Mark an action as done. This will automatically " process Process inbox items systematically.
println " take care of duplicates of the action in project " done <action-file> Mark an action as done. This will automatically
println " or next-actions sub-folders." take care of duplicates of the action in project
or next-actions sub-folders.
calendar Show the tasks with specific days assigned to
them, sorted by date.
list-copies <action-file> Given an action item, list all the other places
there the same item is filed (cross-reference
with a project folder, for example).
new Interactively create a new action item in the
current folder.
tickler Search the tickler file for items that need to be
delivered and move them to the *next-actions*
folder."""
} else { } else {
def command = args.poll() def command = args.poll()
@ -417,6 +493,49 @@ one place and deal with them all in one fell swoop. Duplicates are determined by
exact file contents (MD5 has of the file contents).""" exact file contents (MD5 has of the file contents)."""
break break
case ~/calendar/: println """\
usage: gtd calendar
Print an agenda of all the actions that are on the calendar, sorted by date.
This prints a date heading first, then all of the actions assogned to that day.
Remember that in the GTD calendar items are supposed to be hard dates, IE.
things that *must* be done on the assigned date."""
break
case ~/list-copies/: println """\
usage: gtd list-copies <action-file>
Where <action-file> is expected to be the path (absolute or relative) to an
action item file.
This command searched through the current GTD repository for any items that are
duplicates of this item."""
break
case ~/new/: println """\
usage: gtd new
This command is interactive (maybe allow it to take interactive prompts in the
future?). It prompts the user for the next action and any extended properties
that should be associated with it, then creates the action file in the current
directory."""
break
case ~/tickler/: println """\
usage: gtd tickler
This command should be scheduled for execution once a day. It checks the tickler
file for any items that should become active (based on their <tickle> property)
and moves them out of the tickler file and into the next-actions file."""
break
case ~/ls|list-context/: println """\
usage gtd ls [<context> ...]
This command lists all the tasks for a given context or project. The purpose is
to list in one place items that are sitting in the next-actions folder or the
waiting folder for a specific context or list items for a given project. If no
context or project is named, all contexts are listed."""
} }
} }
} }
@ -451,7 +570,7 @@ exact file contents (MD5 has of the file contents)."""
// parent path. // parent path.
return true } return true }
protected String getRelativePath(File parent, File child) { protected static String getRelativePath(File parent, File child) {
def parentPath = parent.canonicalPath.split("/") def parentPath = parent.canonicalPath.split("/")
def childPath = child.canonicalPath.split("/") def childPath = child.canonicalPath.split("/")
@ -481,6 +600,16 @@ exact file contents (MD5 has of the file contents)."""
return [:] } return [:] }
protected String prompt(def msg) {
if (msg instanceof List) msg = msg.join(EOL)
msg += "> "
print msg
def line
while(!(line = stdin.nextLine().trim())) print msg
return line }
static String filenameToString(File f) { static String filenameToString(File f) {
return f.name.replaceAll(/[-_]/, " ").capitalize() } return f.name.replaceAll(/[-_]/, " ").capitalize() }
@ -488,5 +617,6 @@ exact file contents (MD5 has of the file contents)."""
return s.replaceAll(/\s/, '-'). return s.replaceAll(/\s/, '-').
replaceAll(/[';:(\.$)]/, ''). replaceAll(/[';:(\.$)]/, '').
toLowerCase() } toLowerCase() }
} }