|
|
@ -10,7 +10,7 @@ import org.joda.time.DateTime
|
|
|
|
|
|
|
|
|
|
|
|
public class GTDCLI {
|
|
|
|
public class GTDCLI {
|
|
|
|
|
|
|
|
|
|
|
|
public static final String VERSION = "0.3"
|
|
|
|
public static final String VERSION = "0.8"
|
|
|
|
private static String EOL = System.getProperty("line.separator")
|
|
|
|
private static String EOL = System.getProperty("line.separator")
|
|
|
|
private static GTDCLI nailgunInst
|
|
|
|
private static GTDCLI nailgunInst
|
|
|
|
|
|
|
|
|
|
|
@ -95,9 +95,10 @@ public class GTDCLI {
|
|
|
|
case ~/done/: done(parsedArgs); break
|
|
|
|
case ~/done/: done(parsedArgs); break
|
|
|
|
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 ~/new/: newAction(parsedArgs); break
|
|
|
|
default:
|
|
|
|
default:
|
|
|
|
parsedArgs.addFirst(command)
|
|
|
|
println "Unrecognized command: ${command}"
|
|
|
|
process(parsedArgs)
|
|
|
|
|
|
|
|
break } } }
|
|
|
|
break } } }
|
|
|
|
|
|
|
|
|
|
|
|
protected void process(LinkedList args) {
|
|
|
|
protected void process(LinkedList args) {
|
|
|
@ -105,7 +106,7 @@ public class GTDCLI {
|
|
|
|
def path = args.poll()
|
|
|
|
def path = args.poll()
|
|
|
|
if (path) {
|
|
|
|
if (path) {
|
|
|
|
def givenDir = new File(path)
|
|
|
|
def givenDir = new File(path)
|
|
|
|
if (!(gtdDirs = findGtdRootDir(givenPath))) {
|
|
|
|
if (!(gtdDirs = findGtdRootDir(givenDir))) {
|
|
|
|
println "'$path' is not a valid directory."; return }}
|
|
|
|
println "'$path' is not a valid directory."; return }}
|
|
|
|
|
|
|
|
|
|
|
|
// Start processing items
|
|
|
|
// Start processing items
|
|
|
@ -114,15 +115,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,29 +259,26 @@ 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.
|
|
|
|
gtdDirs["next-actions"].eachFileRecurse({ file ->
|
|
|
|
findAllCopies(oldFile, gtdDirs."next-actions").each { file ->
|
|
|
|
if (file.isFile() && md5.digest(file.bytes) == itemMd5) {
|
|
|
|
println "Deleting duplicate entry from the " +
|
|
|
|
println "Deleting duplicate entry from the " +
|
|
|
|
|
|
|
|
"${file.parentFile.name} context."
|
|
|
|
"${file.parentFile.name} context."
|
|
|
|
file.delete() }})
|
|
|
|
file.delete() }
|
|
|
|
|
|
|
|
|
|
|
|
// Delete any copies of this item in the waiting folder.
|
|
|
|
// Delete any copies of this item in the waiting folder.
|
|
|
|
gtdDirs.waiting.eachFileRecurse({ file ->
|
|
|
|
findAllCopies(oldFile, gtdDirs.waiting).each { file ->
|
|
|
|
if (file.isFile() && md5.digest(file.bytes) == itemMd5) {
|
|
|
|
println "Deleting duplicate entry from the " +
|
|
|
|
println "Deleting duplicate entry from the " +
|
|
|
|
"${file.parentFile.name} waiting context."
|
|
|
|
"${file.parentFile.name} waiting context."
|
|
|
|
file.delete() }}
|
|
|
|
file.delete() }})}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this item was in the next-action or waiting folder.
|
|
|
|
// Check if this item was in the next-action or waiting folder.
|
|
|
|
if (inPath(gtdDirs["next-actions"], oldFile) ||
|
|
|
|
if (inPath(gtdDirs["next-actions"], oldFile) ||
|
|
|
|
inPath(gtdDirs.waiting, oldFile)) {
|
|
|
|
inPath(gtdDirs.waiting, oldFile)) {
|
|
|
|
|
|
|
|
|
|
|
|
// Delete any copies of this item in the projects folder.
|
|
|
|
// Delete any copies of this item in the projects folder.
|
|
|
|
gtdDirs.projects.eachFileRecurse({ file ->
|
|
|
|
findAllCopies(oldFile, gtdDirs.projects).each { file ->
|
|
|
|
if (file.isFile() && md5.digest(file.bytes) == itemMd5) {
|
|
|
|
println "Deleting duplicate entry from the " +
|
|
|
|
println "Deleting duplicate entry from the " +
|
|
|
|
"${file.parentFile.name} project."
|
|
|
|
"${file.parentFile.name} project."
|
|
|
|
file.delete() }}
|
|
|
|
file.delete() }})}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Delete the original
|
|
|
|
// Delete the original
|
|
|
|
oldFile.delete()
|
|
|
|
oldFile.delete()
|
|
|
@ -318,31 +307,85 @@ 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 }
|
|
|
|
|
|
|
|
|
|
|
|
println " $item" } }
|
|
|
|
println " $item" } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected void listCopies(LinkedList args) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
args.each { filePath ->
|
|
|
|
|
|
|
|
def file = new File(filePath)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!file.isAbsolute()) file = new File(workingDir, filePath)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!file.isFile()) {
|
|
|
|
|
|
|
|
println "${file.canonicalPath} is not a regular file."
|
|
|
|
|
|
|
|
return }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
String originalRelativePath = getRelativePath(gtdDirs.root, file)
|
|
|
|
|
|
|
|
println "Copies of $originalRelativePath:"
|
|
|
|
|
|
|
|
println ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
findAllCopies(file, gtdDirs.root).each { copy ->
|
|
|
|
|
|
|
|
if (copy.canonicalPath != file.canonicalPath) {
|
|
|
|
|
|
|
|
String relativePath = getRelativePath(gtdDirs.root, copy)
|
|
|
|
|
|
|
|
println " $relativePath" }} }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 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."""
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
def command = args.poll()
|
|
|
|
def command = args.poll()
|
|
|
|
|
|
|
|
|
|
|
@ -398,10 +441,47 @@ 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected List<File> findAllCopies(File original, File inDir) {
|
|
|
|
|
|
|
|
def copies = []
|
|
|
|
|
|
|
|
def originalMD5 = md5.digest(original.bytes)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
inDir.eachFileRecurse { file ->
|
|
|
|
|
|
|
|
if (file.isFile() && md5.digest(file.bytes) == originalMD5)
|
|
|
|
|
|
|
|
copies << file }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return copies }
|
|
|
|
|
|
|
|
|
|
|
|
protected boolean inPath(File parent, File child) {
|
|
|
|
protected boolean inPath(File parent, File child) {
|
|
|
|
def parentPath = parent.canonicalPath.split("/")
|
|
|
|
def parentPath = parent.canonicalPath.split("/")
|
|
|
|
def childPath = child.canonicalPath.split("/")
|
|
|
|
def childPath = child.canonicalPath.split("/")
|
|
|
@ -422,6 +502,18 @@ exact file contents (MD5 has of the file contents)."""
|
|
|
|
// parent path.
|
|
|
|
// parent path.
|
|
|
|
return true }
|
|
|
|
return true }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected String getRelativePath(File parent, File child) {
|
|
|
|
|
|
|
|
def parentPath = parent.canonicalPath.split("/")
|
|
|
|
|
|
|
|
def childPath = child.canonicalPath.split("/")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (parentPath.length > childPath.length) return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int b = 0
|
|
|
|
|
|
|
|
while (b < parentPath.length && parentPath[b] == childPath[b] ) b++;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (b != parentPath.length) return ""
|
|
|
|
|
|
|
|
return (['.'] + childPath[b..<childPath.length]).join('/') }
|
|
|
|
|
|
|
|
|
|
|
|
protected Map findGtdRootDir(File givenDir) {
|
|
|
|
protected Map findGtdRootDir(File givenDir) {
|
|
|
|
|
|
|
|
|
|
|
|
def gtdDirs = [:]
|
|
|
|
def gtdDirs = [:]
|
|
|
@ -440,6 +532,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() }
|
|
|
|
|
|
|
|
|
|
|
@ -447,5 +549,6 @@ exact file contents (MD5 has of the file contents)."""
|
|
|
|
return s.replaceAll(/\s/, '-').
|
|
|
|
return s.replaceAll(/\s/, '-').
|
|
|
|
replaceAll(/[';:(\.$)]/, '').
|
|
|
|
replaceAll(/[';:(\.$)]/, '').
|
|
|
|
toLowerCase() }
|
|
|
|
toLowerCase() }
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|