9 Commits
v1.4 ... v1.10

Author SHA1 Message Date
2c8180d9b2 Upgraded to jdb-util-3.2 2014-11-19 12:52:03 -06:00
12f87afe63 Added jlp-docs and target to generate documentation. 2014-06-21 14:32:48 -05:00
3496e21af5 Added online help and documentation for delegate command. 2014-04-16 21:37:30 -05:00
40906eebf8 Updated web.xml for current deployed config. 2014-04-15 20:45:00 +00:00
acaf58f456 Added delegate command to delegate existing actions.
* Created `delegate`. This will move an action from a next actions context
  folder to the waiting folder. Any duplicate items in project folders will be
  renamed and updated to reflect any action change.
* Updated `ls` to take multiple named contexts or projects and list them all.
2013-11-05 08:46:06 -06:00
5ac69157dc Fixed bug in done command. Replaced output system with logging system.
* Fixed the bug in `done` where it would incorrectly reference relative files.
* Moved to using SLF4J and Logback loggers for all output.
2013-11-03 00:42:58 -05:00
b4e01b6098 done supports a list of actions, bugfix.
* `done` command now accepts an unlimited list of tasks to mark as done.
* `GTDCLI.stringToFilename` now removes forward slashes.
2013-10-30 11:31:28 -05:00
415c0e622f Fixed documentation typos, error code typos. 2013-10-28 12:25:22 -05:00
a9ba9d94f8 Bumped version number. 2013-10-21 14:08:58 +00:00
13 changed files with 341 additions and 85 deletions

View File

@ -8,6 +8,16 @@
<mkdir dir="${build.dir}/main/classes"/> <mkdir dir="${build.dir}/main/classes"/>
</target> </target>
<target name="jlp-docs">
<exec executable="jlp">
<arg value="--no-source"/>
<arg value="--output-dir"/>
<arg value="doc"/>
<arg value="src"/>
<arg value="README.md"/>
</exec>
</target>
<target name="ng-deploy" depends="build"> <target name="ng-deploy" depends="build">
<!-- Stop the Nailgun Server --> <!-- Stop the Nailgun Server -->
<exec executable="cmd" os="Windows XP"> <exec executable="cmd" os="Windows XP">

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

View File

View File

@ -1,7 +1,7 @@
#Mon, 21 Oct 2013 14:00:48 +0000 #Wed, 19 Nov 2014 12:50:01 -0600
lib.local=true lib.local=true
name=jdb-gtd name=jdb-gtd
version=1.4 version=1.10
nailgun.classpath.dir=/home/jdbernard/programs/nailgun/classpath nailgun.classpath.dir=/home/jdbernard/programs/nailgun/classpath
executable.jar=true executable.jar=true
main.class=com.jdblabs.gtd.cli.GTDCLI main.class=com.jdblabs.gtd.cli.GTDCLI

View File

@ -9,7 +9,7 @@
<init-param> <init-param>
<param-name>gtdRootDir</param-name> <param-name>gtdRootDir</param-name>
<param-value>/home/jdbernard/Dropbox/gtd</param-value> <param-value>/home/jdbernard/gtd</param-value>
</init-param> </init-param>
</servlet> </servlet>

View File

@ -5,6 +5,14 @@
*/ */
package com.jdblabs.gtd.cli package com.jdblabs.gtd.cli
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.classic.filter.LevelFilter
import ch.qos.logback.classic.filter.ThresholdFilter
import ch.qos.logback.core.OutputStreamAppender
import ch.qos.logback.core.spi.FilterReply
import com.jdblabs.gtd.Item import com.jdblabs.gtd.Item
import com.jdblabs.gtd.PropertyHelp import com.jdblabs.gtd.PropertyHelp
import com.jdbernard.util.LightOptionParser import com.jdbernard.util.LightOptionParser
@ -12,8 +20,8 @@ import com.martiansoftware.nailgun.NGContext
import java.security.MessageDigest import java.security.MessageDigest
import org.joda.time.DateMidnight import org.joda.time.DateMidnight
import org.joda.time.DateTime import org.joda.time.DateTime
//import org.slf4j.Logger import org.slf4j.Logger as SFL4JLogger
//import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import static com.jdblabs.gtd.Util.* import static com.jdblabs.gtd.Util.*
@ -23,7 +31,7 @@ import static com.jdblabs.gtd.Util.*
* @org gtd.jdb-labs.com/cli/GTDCLI */ * @org gtd.jdb-labs.com/cli/GTDCLI */
public class GTDCLI { public class GTDCLI {
public static final String VERSION = "1.3" public static final String VERSION = "1.9"
private static String EOL = System.getProperty("line.separator") private static String EOL = System.getProperty("line.separator")
/// We have a persistent instance when we are in the context of a Nailgun /// We have a persistent instance when we are in the context of a Nailgun
@ -41,7 +49,21 @@ public class GTDCLI {
/// [root-map]: jlp://gtd.jdb-labs.com/notes/root-directory-map /// [root-map]: jlp://gtd.jdb-labs.com/notes/root-directory-map
private Map<String, File> gtdDirs private Map<String, File> gtdDirs
//private Logger log = LoggerFactory.getLogger(getClass()) /// Logging objects
private Logger log
private OutputStreamAppender otherAppender
private OutputStreamAppender infoAppender
private ThresholdFilter thresholdFilter
private LevelFilter rejectInfo
private String loggingThreshold
public void setLoggingThreshold(String level) {
if (thresholdFilter) {
System.out.println "Changing logging level to $level"
thresholdFilter.stop()
thresholdFilter.level = level
thresholdFilter.start() }
this.loggingThreshold = level }
/** #### `main` /** #### `main`
* Main entry point for a normal GTD CLI process. */ * Main entry point for a normal GTD CLI process. */
@ -54,16 +76,18 @@ public class GTDCLI {
/// Actual processing is done by the /// Actual processing is done by the
/// [`run`](jlp://gtd.jdb-labs.com/cli/GTDCLI/run) method /// [`run`](jlp://gtd.jdb-labs.com/cli/GTDCLI/run) method
if (args.length > 0) args[-1] = args[-1].trim() if (args.length > 0) args[-1] = args[-1].trim()
inst.run(args) } inst.run(args) }
/** #### `nailMain` /** #### `nailMain`
* Entry point for a GTD CLI process under [Nailgun][ng]. * Entry point for a GTD CLI process under [Nailgun][ng].
* [ng]: http://www.martiansoftware.com/nailgun/ */ * [ng]: http://www.martiansoftware.com/nailgun/ */
public static void nailMain(NGContext context) { public static void nailMain(NGContext context) {
if (nailgunInst == null)
if (nailgunInst == null) {
nailgunInst = new GTDCLI(new File( nailgunInst = new GTDCLI(new File(
System.getProperty("user.home"), ".gtdclirc")) System.getProperty("user.home"), ".gtdclirc")) }
else nailgunInst.stdin = new Scanner(context.in) else { nailgunInst.stdin = new Scanner(context.in) }
/// Trim the last argument; not all cli's are well-behaved /// Trim the last argument; not all cli's are well-behaved
if (context.args.length > 0) context.args[-1] = context.args[-1].trim() if (context.args.length > 0) context.args[-1] = context.args[-1].trim()
@ -74,6 +98,7 @@ public class GTDCLI {
* This method reloads the configuration before invoking the run function, * This method reloads the configuration before invoking the run function,
* allowing a long-lived instance to react to configuration changes. */ * allowing a long-lived instance to react to configuration changes. */
public static void reconfigure(String[] args) { public static void reconfigure(String[] args) {
/// If we do not have a long-running Nailgun instance we just call /// If we do not have a long-running Nailgun instance we just call
/// main. /// main.
if (nailgunInst == null) main(args) if (nailgunInst == null) main(args)
@ -95,6 +120,55 @@ public class GTDCLI {
if (configFile.exists()) if (configFile.exists())
config = new ConfigSlurper().parse(configFile.toURL()) config = new ConfigSlurper().parse(configFile.toURL())
/// Setup logging
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory()
lc.reset()
thresholdFilter = new ThresholdFilter()
loggingThreshold = config.defaultLoggingLevel ?: 'INFO'
thresholdFilter.level = loggingThreshold
infoAppender = new OutputStreamAppender()
otherAppender = new OutputStreamAppender()
PatternLayoutEncoder infoLayout = new PatternLayoutEncoder()
PatternLayoutEncoder otherLayout = new PatternLayoutEncoder()
LevelFilter acceptInfo = new LevelFilter()
rejectInfo = new LevelFilter()
[infoAppender, otherAppender, infoLayout, otherLayout, acceptInfo,
rejectInfo].each { it.context = lc }
// Setup filter and layout for INFO appender
infoLayout.context = lc
infoLayout.pattern = '%msg'
infoLayout.start()
acceptInfo.level = Level.INFO
acceptInfo.onMatch = FilterReply.ACCEPT
acceptInfo.onMismatch = FilterReply.DENY
acceptInfo.start()
infoAppender.encoder = infoLayout
infoAppender.outputStream = System.out
infoAppender.addFilter(acceptInfo)
infoAppender.start()
// Setup filters and layout for non-INFO appender
otherLayout.context = lc
otherLayout.pattern = '%level -- %msg%n'
otherLayout.start()
rejectInfo.level = Level.INFO
rejectInfo.onMatch = FilterReply.DENY
rejectInfo.start()
thresholdFilter.start()
otherAppender.encoder = otherLayout
otherAppender.outputStream = System.err
otherAppender.addFilter(rejectInfo)
otherAppender.addFilter(thresholdFilter)
otherAppender.start()
log = lc.getLogger(getClass())
log.addAppender(infoAppender)
log.addAppender(otherAppender)
/// Configure the terminal width /// Configure the terminal width
terminalWidth = (System.getenv().COLUMNS ?: config.terminalWidth ?: 79) as int terminalWidth = (System.getenv().COLUMNS ?: config.terminalWidth ?: 79) as int
@ -111,6 +185,8 @@ public class GTDCLI {
* @org gtd.jdb-labs.com/cli/GTDCLI/run */ * @org gtd.jdb-labs.com/cli/GTDCLI/run */
protected void run(String[] args) { protected void run(String[] args) {
log.debug("Args: $args")
/// Simple CLI options: /// Simple CLI options:
def cliDefinition = [ def cliDefinition = [
/// -h, --help /// -h, --help
@ -127,7 +203,7 @@ public class GTDCLI {
if (opts.h) { printUsage(null); return } if (opts.h) { printUsage(null); return }
if (opts.v) { println "GTD CLI v$VERSION"; return } if (opts.v) { println "GTD CLI v$VERSION"; return }
if (opts.d) workingDir = new File(opts.d) if (opts.d) workingDir = new File(opts.d[0])
/// View the arguments as a [`LinkedList`][1] so we can use [`peek`][2] /// View the arguments as a [`LinkedList`][1] so we can use [`peek`][2]
/// and [`poll`][3]. /// and [`poll`][3].
@ -137,18 +213,23 @@ public class GTDCLI {
/// [3]: http://docs.oracle.com/javase/6/docs/api/java/util/LinkedList.html#poll() /// [3]: http://docs.oracle.com/javase/6/docs/api/java/util/LinkedList.html#poll()
def parsedArgs = (opts.args as List) as LinkedList def parsedArgs = (opts.args as List) as LinkedList
log.debug("Parsed args: ${parsedArgs}")
if (parsedArgs.size() < 1) printUsage() if (parsedArgs.size() < 1) printUsage()
/// Make sure we are in a GTD directory. /// Make sure we are in a GTD directory.
gtdDirs = findGtdRootDir(workingDir) gtdDirs = findGtdRootDir(workingDir)
log.debug("gtdDirs:$EOL\t${gtdDirs}")
if (!gtdDirs) { if (!gtdDirs) {
println "fatal: '${workingDir.canonicalPath}'" log.error "fatal: '${workingDir.canonicalPath}'"
println " is not a GTD repository (or any of the parent directories)." log.error " is not a GTD repository (or any of the parent directories)."
return } return }
while (parsedArgs.peek()) { while (parsedArgs.peek()) {
/// Pull off the first argument. /// Pull off the first argument.
def command = parsedArgs.poll() def command = parsedArgs.poll()
log.trace("Processing command: ${command}")
/// Match the first argument and invoke the proper command method. /// Match the first argument and invoke the proper command method.
switch (command.toLowerCase()) { switch (command.toLowerCase()) {
@ -160,8 +241,10 @@ public class GTDCLI {
case ~/new/: newAction(parsedArgs); break case ~/new/: newAction(parsedArgs); break
case ~/tickler/: tickler(parsedArgs); break case ~/tickler/: tickler(parsedArgs); break
case ~/ls|list/: ls(parsedArgs); break; case ~/ls|list/: ls(parsedArgs); break;
case ~/debug/: debug(parsedArgs); break;
case ~/delegate/: delegateAction(parsedArgs); break;
default: default:
println "Unrecognized command: ${command}" log.error "Unrecognized command: ${command}"
break } } } break } } }
/** #### `process` /** #### `process`
@ -176,7 +259,7 @@ public class GTDCLI {
if (path) { if (path) {
def givenDir = new File(path) def givenDir = new File(path)
if (!(gtdDirs = findGtdRootDir(givenDir))) { if (!(gtdDirs = findGtdRootDir(givenDir))) {
println "'$path' is not a valid directory."; return }} log.error "'$path' is not a valid directory."; return }}
/// Start processing items /// Start processing items
gtdDirs.in.listFiles().collect { new Item(it) }.each { item -> gtdDirs.in.listFiles().collect { new Item(it) }.each { item ->
@ -267,28 +350,28 @@ public class GTDCLI {
if (response =~ /del/) { if (response =~ /del/) {
item.action = prompt([ item.action = prompt([
"Next action (who needs to do what).", ""]) "Next action (who needs to do what)?", ""])
item.file = new File(promptContext(gtdDirs.waiting), item.file = new File(promptContext(gtdDirs.waiting),
stringToFilename(item.action)) } stringToFilename(item.toString())) }
/// Defer, move to the *next-actions* folder. /// Defer, move to the *next-actions* folder.
else if (response =~ /def/) { else if (response =~ /def/) {
item.action = prompt(["Next action.", ""]) item.action = prompt(["Next action?", ""])
item.file = new File(promptContext(gtdDirs["next-actions"]), item.file = new File(promptContext(gtdDirs["next-actions"]),
stringToFilename(item.action)) } stringToFilename(item.toString())) }
/// Forget for now, move it to the *tickler* folder. /// Forget for now, move it to the *tickler* folder.
else { else {
item.action = prompt(["Next action.", ""]) item.action = prompt(["Next action?", ""])
item.tickle = prompt([ item.tickle = prompt([
"When do you want it to become active?", "When do you want it to become active?",
"(YYYY-MM-DD)"]) "(YYYY-MM-DD)"])
item.file = new File(gtdDirs.tickler, item.file = new File(gtdDirs.tickler,
stringToFilename(item.action)) } stringToFilename(item.toString())) }
item.save() item.save()
oldFile.delete() oldFile.delete()
@ -303,7 +386,7 @@ public class GTDCLI {
if (item.project && projectDir.exists() && if (item.project && projectDir.exists() &&
projectDir.isDirectory()) { projectDir.isDirectory()) {
item.file = new File(projectDir, item.file = new File(projectDir,
stringToFilename(item.action)) stringToFilename(item.toString()))
item.save() item.save()
println "Copied to " + println "Copied to " +
getRelativePath(gtdDirs.root, item.file.parentFile) } } } } } getRelativePath(gtdDirs.root, item.file.parentFile) } } } } }
@ -317,52 +400,61 @@ public class GTDCLI {
*/ */
protected void done(LinkedList args) { protected void done(LinkedList args) {
def selectedFilePath = args.poll() def selectedFilePath
def selectedFile = new File(selectedFilePath)
if (!selectedFile) { if (!args) {
println "gtd done command requires a <action-file> parameter." log.error "The 'gtd done' command requires an <action-file> parameter."
return } return }
def item while ((selectedFilePath = args.poll())) {
if (selectedFile.isAbsolute()) item = new Item(selectedFile) def item
else item = new Item(new File(workingDir, selectedFilePath)) def selectedFile = new File(selectedFilePath)
/// Move to the done folder. if (!selectedFile.isAbsolute())
def oldFile = item.file selectedFile = new File(workingDir, selectedFilePath)
def date = new DateMidnight().toString("YYYY-MM-dd")
item.file = new File(gtdDirs.done, "$date-${item.file.name}")
item.save()
/// Check if this item was in a project folder. if (!selectedFile.exists() || !selectedFile.isFile()) {
if (inPath(gtdDirs.projects, oldFile)) { log.error "File does not exist or is a directory:"
log.error "\t" + selectedFile.canonicalPath
continue }
/// Delete any copies of this item from the next actions folder. item = new Item(selectedFile)
findAllCopies(oldFile, gtdDirs."next-actions").each { file ->
println "Deleting duplicate entry from the " +
"${file.parentFile.name} context."
file.delete() }
/// Delete any copies of this item from the waiting folder. /// Move to the done folder.
findAllCopies(oldFile, gtdDirs.waiting).each { file -> def oldFile = item.file
println "Deleting duplicate entry from the " + def date = new DateMidnight().toString("YYYY-MM-dd")
"${file.parentFile.name} waiting context." item.file = new File(gtdDirs.done, "$date-${item.file.name}")
file.delete() }} item.save()
/// Check if this item was in the next-action or waiting folder. /// Check if this item was in a project folder.
if (inPath(gtdDirs["next-actions"], oldFile) || if (inPath(gtdDirs.projects, oldFile)) {
inPath(gtdDirs.waiting, oldFile)) {
/// Delete any copies of this item from the projects folder. /// Delete any copies of this item from the next actions folder.
findAllCopies(oldFile, gtdDirs.projects).each { file -> findAllCopies(oldFile, gtdDirs["next-actions"]).each { file ->
println "Deleting duplicate entry from the " + println "Deleting duplicate entry from the " +
"${file.parentFile.name} project." "${file.parentFile.name} context."
file.delete() }} if (file.exists()) file.delete() }
/// Delete the original /// Delete any copies of this item from the waiting folder.
oldFile.delete() findAllCopies(oldFile, gtdDirs.waiting).each { file ->
println "Deleting duplicate entry from the " +
"${file.parentFile.name} waiting context."
if (file.exists()) file.delete() }}
println "'$item' marked as done." } /// Check if this item was in the next-action or waiting folder.
if (inPath(gtdDirs["next-actions"], oldFile) ||
inPath(gtdDirs.waiting, oldFile)) {
/// Delete any copies of this item from the projects folder.
findAllCopies(oldFile, gtdDirs.projects).each { file ->
println "Deleting duplicate entry from the " +
"${file.parentFile.name} project."
if (file.exists()) file.delete() }}
/// Delete the original
oldFile.delete()
println "'$item' marked as done." } }
/** #### `calendar` /** #### `calendar`
* Implement the `calendar` command to show all the items which are * Implement the `calendar` command to show all the items which are
@ -424,7 +516,7 @@ public class GTDCLI {
if (!file.isAbsolute()) file = new File(workingDir, filePath) if (!file.isAbsolute()) file = new File(workingDir, filePath)
if (!file.isFile()) { if (!file.isFile()) {
println "${file.canonicalPath} is not a regular file." log.error "${file.canonicalPath} is not a regular file."
return } return }
String originalRelativePath = getRelativePath(gtdDirs.root, file) String originalRelativePath = getRelativePath(gtdDirs.root, file)
@ -482,7 +574,7 @@ public class GTDCLI {
/// exists, copy the item there. /// exists, copy the item there.
def projectDir = new File(gtdDirs.projects, item.project ?: '') def projectDir = new File(gtdDirs.projects, item.project ?: '')
if (item.project && projectDir.exists() && projectDir.isDirectory()) { if (item.project && projectDir.exists() && projectDir.isDirectory()) {
item.file = new File(projectDir, stringToFilename(item.action)) item.file = new File(projectDir, stringToFilename(item.toString()))
item.save() item.save()
println "Copied to " + println "Copied to " +
getRelativePath(gtdDirs.root, item.file.parentFile) } } getRelativePath(gtdDirs.root, item.file.parentFile) } }
@ -504,10 +596,10 @@ public class GTDCLI {
/// If the item is scheduled to be tickled today (or in the past) /// If the item is scheduled to be tickled today (or in the past)
/// then move it into the next-actions folder /// then move it into the next-actions folder
if ((item.tickle as DateMidnight) <= today) { if ((item.tickle as DateMidnight) <= today) {
println "Moving '${item.action}' out of the tickler." println "Moving '${item}' out of the tickler."
def oldFile = item.file def oldFile = item.file
item.file = new File(gtdDirs."next-actions", item.file = new File(gtdDirs."next-actions",
stringToFilename(item.action)) stringToFilename(item.toString()))
item.gtdProperties.remove("tickle") item.gtdProperties.remove("tickle")
item.save() item.save()
oldFile.delete() }}} oldFile.delete() }}}
@ -522,7 +614,7 @@ public class GTDCLI {
*/ */
protected void ls(LinkedList args) { protected void ls(LinkedList args) {
def target = args.poll() def target
/// Temporary helper function to print all the items in a given /// Temporary helper function to print all the items in a given
/// directory. /// directory.
@ -535,25 +627,168 @@ public class GTDCLI {
return return
def item = new Item(file) def item = new Item(file)
println item.action } println item}
println "" } println "" }
/// If we have a named context or project, look for those items /// If we have no named context or project, print all items in the
/// specifically /// *next-actions* and *waiting* folders and all their subfolders.
if (target) { if (!args) {
printItems(new File(gtdDirs['next-actions'], target))
printItems(new File(gtdDirs.waiting, target))
printItems(new File(gtdDirs.projects, target)) }
/// Otherwise print all items in the *next-actions* and *waiting*
/// folders and all their subfolders.
else {
printItems(gtdDirs['next-actions']) printItems(gtdDirs['next-actions'])
printItems(gtdDirs['waiting']) printItems(gtdDirs['waiting'])
gtdDirs['next-actions'].eachDir(printItems) gtdDirs['next-actions'].eachDir(printItems)
gtdDirs['waiting'].eachDir(printItems) } } gtdDirs['waiting'].eachDir(printItems) }
/// For every name we do have, look for a project or context and
/// recursively print their contents.
else while ((target = args.poll())) {
printItems(new File(gtdDirs['next-actions'], target))
printItems(new File(gtdDirs.waiting, target))
printItems(new File(gtdDirs.projects, target)) } }
/** #### `debug`
* Print out debug information. Currently this prints out the internal
* state of the CLI. I may add other subcommands if the need arises. */
protected void debug(LinkedList args) {
def command = args.poll()
if (!command || "state" == command) {
println "GTD CLI v${VERSION}"
println ""
println "-- General"
println " Running under nailgun? ${nailgunInst ? 'yes' : 'no'}"
println " Terminal width ${terminalWidth}"
println " Working directory ${workingDir.canonicalPath}"
println ""
println "-- GTD Directories"
gtdDirs.each { k, v -> println " ${k.padRight(12)} ${v.canonicalPath}" }
println ""
println "-- Logging"
println " Threshold ${loggingThreshold}"
log.trace " Message from TRACE"
log.debug " Message from DEBUG"
log.info " Message from INFO${EOL}"
log.warn " Message from WARN"
log.error " Message from ERROR" }
else if ("loglevel" == command) {
def level = args.poll()
if (!level)
log.error "debug loglevel command requires additional arguments."
else setLoggingThreshold(level) }
else log.error "Unrecognized debug command: '${command}'." }
/** #### `delegate`
* Implement the `delegate` command. This allows you to move an action
* from the next action list to the delegate list, providing the name of
* the responsible party and optionally renaming the item. For detailed
* information see the [online help][help-delegate] by running
* `gtd help delegate`.
*
* [help-delegate]: jlp://gtd.jdb-labs.com/cli/GTDCLI/help/delegate
*/
protected void delegateAction(LinkedList args) {
def selectedFilePath
if (!args) {
log.error("The 'gtd delegate' command requires an " +
"<action-file> parameter.")
return }
while ((selectedFilePath = args.poll())) {
Item item
File oldFile, newContextDir
File selectedFile = new File(selectedFilePath)
if (!selectedFile.isAbsolute())
selectedFile = new File(workingDir, selectedFilePath)
if (!selectedFile.exists() || !selectedFile.isFile()) {
log.error "File does not exist or is a directory:"
log.error "\t" + selectedFile.canonicalPath
continue }
item = new Item(selectedFile)
oldFile = item.file
/// Move to the waiting folder, with the name of the delegatee and
/// optionally a new next action.
def delegatee = prompt(
["Who is responsible for the next action? You may also update the next action",
"by including it after a colon (e.g. 'Delegatee Name: New next action.').",
""])
if (delegatee.indexOf(':') > 0) item.action = delegatee
else item.action = delegatee + ': ' + item.action
/// Check if this item was in a project folder.
if (inPath(gtdDirs.projects, oldFile)) {
/// Rename the file in the project folder
item.file = new File(oldFile.parentFile,
stringToFilename(item.toString()))
item.save()
/// Move any copies of this item from the next actions folder
/// to the waiting folder.
findAllCopies(oldFile, gtdDirs['next-actions']).each { dupFile ->
println "Moving duplicate entry from the " +
"${dupFile.parentFile.name} context."
/// Retain the item's context if possible
newContextDir = new File(gtdDirs.waiting,
dupFile.parentFile.name)
/// Instead of creating a new Item object, let's just
/// create a copy of the existing one on the filesystem by
/// saving the existing object to a the new location.
if (newContextDir.exists() && newContextDir.isDirectory()) {
item.file = new File(newContextDir,
stringToFilename(item.toString())) }
else { item.file = new File(gtdDirs.waiting,
stringToFilename(item.toString())) }
item.save()
dupfile.delete() }}
/// Check if this item was in the next-action folder.
else if (inPath(gtdDirs["next-actions"], oldFile) ||
inPath(gtdDirs.waiting, oldFile)) {
/// Retain the item's context if possible.
newContextDir = new File(gtdDirs.waiting,
oldFile.parentFile.name)
/// Move the file to the waiting folder.
if (newContextDir.exists() && newContextDir.isDirectory()) {
item.file = new File(newContextDir,
stringToFilename(item.toString())) }
else { item.file = new File(gtdDirs.waiting,
stringToFilename(item.toString())) }
item.save()
/// Rename any copies of this item from the projects folder.
findAllCopies(oldFile, gtdDirs.projects).each { dupFile ->
println "Renaming duplicate entry from the " +
"${dupFile.parentFile.name} project."
item.file = new File(dupFile.parentFile,
stringToFilename(item.toString()))
item.save()
dupFile.delete() } }
/// Delete the original file.
oldFile.delete() } }
private void print(String msg) { log.info(msg) }
private void println(String line) { log.info(line + EOL) }
/** #### `help` /** #### `help`
* Implement the `help` command which provides the online-help. Users can * Implement the `help` command which provides the online-help. Users can
@ -700,6 +935,16 @@ 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 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 waiting folder for a specific context or list items for a given project. If no
context or project is named, all contexts are listed.""" context or project is named, all contexts are listed."""
/// Online help for the `delegate` command.
/// @org gtd.jdb-labs.com/cli/GTDCLI/help/delegate
case ~/delegate/: println """\
usage gtd delegate [<action-file> ...]
This command moves an action item from a next-action context or project folder
to the delegate folder. It allows the user to attach the name of the newly
responsible party and optionally rename the item."""
} }
} }
} }
@ -729,6 +974,7 @@ context or project is named, all contexts are listed."""
contextFile = line ? new File(baseDir, line) : baseDir contextFile = line ? new File(baseDir, line) : baseDir
while (!contextFile.exists() || !contextFile.isDirectory()) { while (!contextFile.exists() || !contextFile.isDirectory()) {
log.warn "'$line' is not a valid context."
println "Available contexts:" println "Available contexts:"
baseDir.eachDir { print "\t${it.name}"} baseDir.eachDir { print "\t${it.name}"}
println "" println ""
@ -749,7 +995,7 @@ context or project is named, all contexts are listed."""
* palatable for a filename. */ * palatable for a filename. */
public static String stringToFilename(String s) { public static String stringToFilename(String s) {
return s.replaceAll(/\s/, '-'). return s.replaceAll(/\s/, '-').
replaceAll(/[';:(\.$)]/, ''). replaceAll(/[';:(\.$\/)]/, '').
toLowerCase() } toLowerCase() }
} }

View File

@ -120,7 +120,7 @@ public class GTDServlet extends HttpServlet {
/// Get this user's session /// Get this user's session
HttpSession session = request.getSession(true); HttpSession session = request.getSession(true);
/// If the user is posting to `/gtd/login` then let's try to /// If the user is posting to `/login` then let's try to
/// authenticate them. We don't care about the state of the existing /// authenticate them. We don't care about the state of the existing
/// session. /// session.
if (request.servletPath == '/login') { if (request.servletPath == '/login') {
@ -161,8 +161,8 @@ public class GTDServlet extends HttpServlet {
/// Right now there is no other endpoint that supports `POST`, so return /// Right now there is no other endpoint that supports `POST`, so return
/// `404 Not Found` or `405 Method Not Allowed` /// `404 Not Found` or `405 Method Not Allowed`
switch (request.servletPath) { switch (request.servletPath) {
case ~/\/gtd\/contexts.*/: case ~/\/contexts.*/:
case ~/\/gtd\/projects.*/: case ~/\/projects.*/:
response.status = SC_METHOD_NOT_ALLOWED response.status = SC_METHOD_NOT_ALLOWED
return return
default: default:
@ -207,14 +207,14 @@ public class GTDServlet extends HttpServlet {
switch(request.servletPath) { switch(request.servletPath) {
/// If they are invoking `/gtd/logout` then invalidate their session /// If they are invoking `/logout` then invalidate their session
/// and return `200 OK` /// and return `200 OK`
case "/logout": case "/logout":
session.removeAttribute("authenticated") session.removeAttribute("authenticated")
session.invalidate() session.invalidate()
break break
/// ##### `/gtd/contexts` /// ##### `/contexts`
/// Return the list of contexts that are readable by this user. /// Return the list of contexts that are readable by this user.
case "/contexts": case "/contexts":
@ -230,7 +230,7 @@ public class GTDServlet extends HttpServlet {
writeJSON(returnData, response) writeJSON(returnData, response)
break break
/// ##### `/gtd/contexts/<contextId>` /// ##### `/contexts/<contextId>`
/// Return data for the requested context, assuming it is /// Return data for the requested context, assuming it is
/// readable for this user. /// readable for this user.
case ~'/contexts/(.+)': case ~'/contexts/(.+)':
@ -251,7 +251,7 @@ public class GTDServlet extends HttpServlet {
writeJSON(returnData, response) writeJSON(returnData, response)
break break
/// ##### `/gtd/projects` /// ##### `/projects`
/// Return the list of projects that are readable for this user. /// Return the list of projects that are readable for this user.
case "/projects": case "/projects":
/// Filter the project directories to find the ones that the /// Filter the project directories to find the ones that the
@ -264,7 +264,7 @@ public class GTDServlet extends HttpServlet {
writeJSON(returnData, response) writeJSON(returnData, response)
break break
/// ##### `/gtd/projects/<projectId>` /// ##### `/projects/<projectId>`
/// Return data for the requested project, assuming it is readable /// Return data for the requested project, assuming it is readable
/// for this user. /// for this user.
case ~'/projects/(.+)': case ~'/projects/(.+)':
@ -286,7 +286,7 @@ public class GTDServlet extends HttpServlet {
writeJSON(returnData, response) writeJSON(returnData, response)
break break
/// ##### `/gtd/next-actions/<contexts-and-projects>` /// ##### `/next-actions/<contexts-and-projects>`
/// Return all of the items contained in the named contexts and /// Return all of the items contained in the named contexts and
/// projects, assuming the user has access to them. /// projects, assuming the user has access to them.
/// `<contexts-and-projects>` is expected to be a comma-delimited /// `<contexts-and-projects>` is expected to be a comma-delimited