13 Commits
v1.4 ... v1.14

Author SHA1 Message Date
4c5f514fb4 Bash completion script, list-projects, list-contexts commands.
* list-projects lists all the projects in the repo.
* list-contexts lists all the context in the repo.
* Created a bash completion script to allow auto-complete for GTD.
2014-12-16 14:04:58 -06:00
a780d972f1 List actions now sorts its output alphabetically. 2014-12-16 11:16:49 -06:00
43f0930cf2 Updated the version number (forgot and pushed the tags). 2014-12-01 12:37:20 -06:00
f95dc91707 New rename-project command.
The `rename-project` command will rename a project and update all of the action
items associated with that project.

* Fills in some missing information in the online help.
* Fixes a bug in the finGtdRootDir function. It was not properly handling
  relative paths.
* Fixes a bug where the reconfigure command was referring to the wrong
  configuration file.
2014-12-01 12:05:55 -06:00
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
16 changed files with 522 additions and 93 deletions

View File

@ -8,6 +8,16 @@
<mkdir dir="${build.dir}/main/classes"/>
</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">
<!-- Stop the Nailgun Server -->
<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,8 +1,8 @@
#Mon, 21 Oct 2013 14:00:48 +0000
#Tue, 16 Dec 2014 11:59:49 -0600
lib.local=true
name=jdb-gtd
version=1.4
version=1.14
nailgun.classpath.dir=/home/jdbernard/programs/nailgun/classpath
executable.jar=true
main.class=com.jdblabs.gtd.cli.GTDCLI
build.number=1
build.number=5

View File

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

View File

@ -0,0 +1,39 @@
_gtd()
{
local cur prev topOpts debugOpts logLevels
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
topOpts="help process done calendar list-copies new tickler list debug delegate rename-project list-projects list-contexts"
debugOpts="state loglevel"
logLevels="TRACE DEBUG INFO WARN ERROR"
case "${prev}" in
help)
COMPREPLY=( $(compgen -W "${topOpts}" -- ${cur}) )
return 0
;;
done|list-copies|delegate)
COMPREPLY=( $(compgen -f ${cur}) )
return 0
;;
ls|list)
COMPREPLY=( $(gtd list-projects) $(gtd list-contexts) )
return 0
;;
debug)
COMPREPLY=( $(compgen -W "${debugOpts}" -- ${cur}) )
return 0
;;
loglevel)
COMPREPLY=( $(compgen -W "${logLevels}" -- ${cur}) )
return 0
;;
*)
;;
esac
COMPREPLY=( $(compgen -W "${topOpts}" -- ${cur}) )
return 0
}
complete -F _gtd gtd

View File

@ -19,6 +19,7 @@ package com.jdblabs.gtd
* calendar to "schedule" items, but only to represent items which must be
* done by or on that day.
* * `details`: more information related to this item.
* * `project`: the name of the project with which this item is associated.
* @org gtd.jdb-labs.com/Item
*/
public class Item {

View File

@ -104,7 +104,7 @@ public class Util {
def gtdDirs = [:]
/// Start by considering the current directory as a candidate.
File currentDir = givenDir
File currentDir = givenDir.canonicalFile
while (currentDir != null) {
/// We recognize the GTD root directory when it contains all of the
/// GTD top-level directories.

View File

@ -5,17 +5,30 @@
*/
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.PropertyHelp
import com.jdbernard.util.LightOptionParser
import com.martiansoftware.nailgun.NGContext
import java.io.FileFilter
import java.nio.file.Files
import java.nio.file.Path
import java.security.MessageDigest
import groovy.io.FileType
import org.joda.time.DateMidnight
import org.joda.time.DateTime
//import org.slf4j.Logger
//import org.slf4j.LoggerFactory
import org.slf4j.Logger as SFL4JLogger
import org.slf4j.LoggerFactory
import static com.jdblabs.gtd.Util.*
import static java.nio.file.StandardCopyOption.*
/**
* Command-line helper for working with this implementation of the Getting
@ -23,7 +36,7 @@ import static com.jdblabs.gtd.Util.*
* @org gtd.jdb-labs.com/cli/GTDCLI */
public class GTDCLI {
public static final String VERSION = "1.3"
public static final String VERSION = "1.14"
private static String EOL = System.getProperty("line.separator")
/// We have a persistent instance when we are in the context of a Nailgun
@ -41,7 +54,21 @@ public class GTDCLI {
/// [root-map]: jlp://gtd.jdb-labs.com/notes/root-directory-map
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 entry point for a normal GTD CLI process. */
@ -54,16 +81,18 @@ public class GTDCLI {
/// Actual processing is done by the
/// [`run`](jlp://gtd.jdb-labs.com/cli/GTDCLI/run) method
if (args.length > 0) args[-1] = args[-1].trim()
inst.run(args) }
/** #### `nailMain`
* Entry point for a GTD CLI process under [Nailgun][ng].
* [ng]: http://www.martiansoftware.com/nailgun/ */
public static void nailMain(NGContext context) {
if (nailgunInst == null)
if (nailgunInst == null) {
nailgunInst = new GTDCLI(new File(
System.getProperty("user.home"), ".gtdclirc"))
else nailgunInst.stdin = new Scanner(context.in)
System.getProperty("user.home"), ".gtdclirc")) }
else { nailgunInst.stdin = new Scanner(context.in) }
/// Trim the last argument; not all cli's are well-behaved
if (context.args.length > 0) context.args[-1] = context.args[-1].trim()
@ -74,6 +103,7 @@ public class GTDCLI {
* This method reloads the configuration before invoking the run function,
* allowing a long-lived instance to react to configuration changes. */
public static void reconfigure(String[] args) {
/// If we do not have a long-running Nailgun instance we just call
/// main.
if (nailgunInst == null) main(args)
@ -82,7 +112,7 @@ public class GTDCLI {
/// read afresh the configuration file.
nailgunInst = null
nailgunInst = new GTDCLI(new File(
System.getProperty("user.home"), ".gritterrc"))
System.getProperty("user.home"), ".gtdclirc"))
nailgunInst.run(args) } }
@ -95,6 +125,55 @@ public class GTDCLI {
if (configFile.exists())
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
terminalWidth = (System.getenv().COLUMNS ?: config.terminalWidth ?: 79) as int
@ -111,6 +190,8 @@ public class GTDCLI {
* @org gtd.jdb-labs.com/cli/GTDCLI/run */
protected void run(String[] args) {
log.debug("Args: $args")
/// Simple CLI options:
def cliDefinition = [
/// -h, --help
@ -127,7 +208,7 @@ public class GTDCLI {
if (opts.h) { printUsage(null); 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]
/// and [`poll`][3].
@ -137,18 +218,23 @@ public class GTDCLI {
/// [3]: http://docs.oracle.com/javase/6/docs/api/java/util/LinkedList.html#poll()
def parsedArgs = (opts.args as List) as LinkedList
log.debug("Parsed args: ${parsedArgs}")
if (parsedArgs.size() < 1) printUsage()
/// Make sure we are in a GTD directory.
gtdDirs = findGtdRootDir(workingDir)
log.debug("gtdDirs:$EOL\t${gtdDirs}")
if (!gtdDirs) {
println "fatal: '${workingDir.canonicalPath}'"
println " is not a GTD repository (or any of the parent directories)."
log.error "fatal: '${workingDir.canonicalPath}'"
log.error " is not a GTD repository (or any of the parent directories)."
return }
while (parsedArgs.peek()) {
/// Pull off the first argument.
def command = parsedArgs.poll()
log.trace("Processing command: ${command}")
/// Match the first argument and invoke the proper command method.
switch (command.toLowerCase()) {
@ -159,9 +245,14 @@ public class GTDCLI {
case ~/list-copies/: listCopies(parsedArgs); break
case ~/new/: newAction(parsedArgs); break
case ~/tickler/: tickler(parsedArgs); break
case ~/list-contexts/: listContexts(parsedArgs); break;
case ~/list-projects/: listProjects(parsedArgs); break;
case ~/ls|list/: ls(parsedArgs); break;
case ~/debug/: debug(parsedArgs); break;
case ~/delegate/: delegateAction(parsedArgs); break;
case ~/rp|rename-project/: renameProject(parsedArgs); break;
default:
println "Unrecognized command: ${command}"
log.error "Unrecognized command: ${command}"
break } } }
/** #### `process`
@ -176,7 +267,7 @@ public class GTDCLI {
if (path) {
def givenDir = new File(path)
if (!(gtdDirs = findGtdRootDir(givenDir))) {
println "'$path' is not a valid directory."; return }}
log.error "'$path' is not a valid directory."; return }}
/// Start processing items
gtdDirs.in.listFiles().collect { new Item(it) }.each { item ->
@ -267,28 +358,28 @@ public class GTDCLI {
if (response =~ /del/) {
item.action = prompt([
"Next action (who needs to do what).", ""])
"Next action (who needs to do what)?", ""])
item.file = new File(promptContext(gtdDirs.waiting),
stringToFilename(item.action)) }
stringToFilename(item.toString())) }
/// Defer, move to the *next-actions* folder.
else if (response =~ /def/) {
item.action = prompt(["Next action.", ""])
item.action = prompt(["Next action?", ""])
item.file = new File(promptContext(gtdDirs["next-actions"]),
stringToFilename(item.action)) }
stringToFilename(item.toString())) }
/// Forget for now, move it to the *tickler* folder.
else {
item.action = prompt(["Next action.", ""])
item.action = prompt(["Next action?", ""])
item.tickle = prompt([
"When do you want it to become active?",
"(YYYY-MM-DD)"])
item.file = new File(gtdDirs.tickler,
stringToFilename(item.action)) }
stringToFilename(item.toString())) }
item.save()
oldFile.delete()
@ -303,7 +394,7 @@ public class GTDCLI {
if (item.project && projectDir.exists() &&
projectDir.isDirectory()) {
item.file = new File(projectDir,
stringToFilename(item.action))
stringToFilename(item.toString()))
item.save()
println "Copied to " +
getRelativePath(gtdDirs.root, item.file.parentFile) } } } } }
@ -317,16 +408,25 @@ public class GTDCLI {
*/
protected void done(LinkedList args) {
def selectedFilePath = args.poll()
def selectedFile = new File(selectedFilePath)
def selectedFilePath
if (!selectedFile) {
println "gtd done command requires a <action-file> parameter."
if (!args) {
log.error "The 'gtd done' command requires an <action-file> parameter."
return }
while ((selectedFilePath = args.poll())) {
def item
if (selectedFile.isAbsolute()) item = new Item(selectedFile)
else item = new Item(new File(workingDir, selectedFilePath))
def 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)
/// Move to the done folder.
def oldFile = item.file
@ -338,16 +438,16 @@ public class GTDCLI {
if (inPath(gtdDirs.projects, oldFile)) {
/// Delete any copies of this item from the next actions folder.
findAllCopies(oldFile, gtdDirs."next-actions").each { file ->
findAllCopies(oldFile, gtdDirs["next-actions"]).each { file ->
println "Deleting duplicate entry from the " +
"${file.parentFile.name} context."
file.delete() }
if (file.exists()) file.delete() }
/// Delete any copies of this item from the waiting folder.
findAllCopies(oldFile, gtdDirs.waiting).each { file ->
println "Deleting duplicate entry from the " +
"${file.parentFile.name} waiting context."
file.delete() }}
if (file.exists()) file.delete() }}
/// Check if this item was in the next-action or waiting folder.
if (inPath(gtdDirs["next-actions"], oldFile) ||
@ -357,12 +457,12 @@ public class GTDCLI {
findAllCopies(oldFile, gtdDirs.projects).each { file ->
println "Deleting duplicate entry from the " +
"${file.parentFile.name} project."
file.delete() }}
if (file.exists()) file.delete() }}
/// Delete the original
oldFile.delete()
println "'$item' marked as done." }
println "'$item' marked as done." } }
/** #### `calendar`
* Implement the `calendar` command to show all the items which are
@ -424,7 +524,7 @@ public class GTDCLI {
if (!file.isAbsolute()) file = new File(workingDir, filePath)
if (!file.isFile()) {
println "${file.canonicalPath} is not a regular file."
log.error "${file.canonicalPath} is not a regular file."
return }
String originalRelativePath = getRelativePath(gtdDirs.root, file)
@ -482,7 +582,7 @@ public class GTDCLI {
/// exists, copy the item there.
def projectDir = new File(gtdDirs.projects, item.project ?: '')
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()
println "Copied to " +
getRelativePath(gtdDirs.root, item.file.parentFile) } }
@ -504,10 +604,10 @@ public class GTDCLI {
/// 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."
println "Moving '${item}' out of the tickler."
def oldFile = item.file
item.file = new File(gtdDirs."next-actions",
stringToFilename(item.action))
stringToFilename(item.toString()))
item.gtdProperties.remove("tickle")
item.save()
oldFile.delete() }}}
@ -522,38 +622,254 @@ public class GTDCLI {
*/
protected void ls(LinkedList args) {
def target = args.poll()
def target
/// Temporary helper function to print all the items in a given
/// directory.
def printItems = { dir ->
if (!dir.exists() || !dir.isDirectory()) return
println "-- ${getRelativePath(gtdDirs.root, dir)} --"
dir.eachFile { file ->
dir.listFiles().sort { it.name }.each { file ->
if (!file.exists() || !file.isFile() || file.isHidden() ||
file.name.startsWith('.'))
return
def item = new Item(file)
println item.action }
println item}
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)) }
/// Otherwise print all items in the *next-actions* and *waiting*
/// folders and all their subfolders.
else {
/// If we have no named context or project, print all items in the
/// *next-actions* and *waiting* folders and all their subfolders.
if (!args) {
printItems(gtdDirs['next-actions'])
printItems(gtdDirs['waiting'])
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)) } }
/** #### `listProjects`
* Implement the `list-projects` command to list all the known projects
* for this repository. For detailed information see the
* [online help][help-list-projects] by running `gtd help list-projects`.
*
* [help-list-projects]: jlp://gtd.jdb-labs.com/cli/GTDCLI/help/list-projects
*/
protected void listProjects(LinkedList args) {
gtdDirs.projects.eachFile(FileType.DIRECTORIES) { println it.name } }
/** #### `listContexts`
* Implement the `list-contexts` command to list all the known contexts
* for this repository. For detailed information see the
* [online help][help-list-contexts] by running `gtd help list-contexts`.
*
* [help-list-contexts]: jlp://gtd.jdb-labs.com/cli/GTDCLI/help/list-contexts
*/
protected void listContexts(LinkedList args) {
def ctxNames = []
gtdDirs["next-actions"].eachFile(FileType.DIRECTORIES) { ctxNames << it.name }
gtdDirs.waiting.eachFile(FileType.DIRECTORIES) { ctxNames << it.name }
ctxNames.unique().each { println it } }
/** #### `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() } }
/** #### `rename-project`
* Implement the `rename-project` command. This will rename the project
* directory in TODO as well as change the project reference in any of the
* items from the `next-actions` contexts.
*
* `gtd help rename-project`.
*
* [help-rename-project]: jlp://gtd.jdb-labs.com/cli/GTDCLI/help/rename-project
*/
protected void renameProject(LinkedList args) {
def projectName = args.poll()
def newName = args.poll()
if (!projectName || !newName) {
log.error "The 'gtd rename-project' command requires two " +
"parameters: <existing-project-name> and a <new-name>."
return }
def projectDir = new File(gtdDirs.projects, projectName)
if (!projectDir.exists() || !projectDir.isDirectory()) {
log.error "There is no directory named '$projectName' in the " +
"'projects' directory."
return }
def newDir = new File(gtdDirs.projects, newName)
if (newDir.exists()) {
log.error "There is already a project named '$newName'."
return }
// Perform the rename of the directory itself.
try { Files.move(projectDir.toPath(), newDir.toPath(), REPLACE_EXISTING) }
catch (Exception e) {
log.error "Unable to rename the project: ${e.localizedMessage}."
return }
// Update all of the items associated with this project.
def projectFiles = newDir.
listFiles({ File f ->
f.exists() && !f.isHidden() &&
f.isFile() && !f.name.startsWith('.') } as FileFilter)
def allProjectItems = projectFiles.collectMany { f ->
findAllCopies(f, gtdDirs.root).collect { new Item(it) } }
allProjectItems.each {
it.project = newName
it.save() }
println "Project renamed. ${allProjectItems.size()} items updated." }
private void print(String msg) { log.info(msg) }
private void println(String line) { log.info(line + EOL) }
/** #### `help`
* Implement the `help` command which provides the online-help. Users can
@ -574,20 +890,41 @@ options are:
top-level commands:
help <command> Print detailed help about a command.
process Process inbox items systematically.
done <action-file> Mark an action as done. This will automatically
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."""
folder.
list, ls [<context> ...] List all the tasks for a given set of contexts
projects.
debug n
delegate Move an item from a next-action context or a
project folder to a waiting context and attach
the name of the party now responsible for the
item.
rename-project, rp <existing-project> <new-name>
Rename a project directory and update any task
items that reference it."""
} else {
def command = args.poll()
@ -691,15 +1028,56 @@ 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
/// Online help for the `ls`/`list-context` command.
/// Online help for the `ls`/`list` command.
/// @org gtd.jdb-labs.com/cli/GTDCLI/help/ls
case ~/ls|list-context/: println """\
usage gtd ls [<context> ...]
case ~/ls|list/: println """\
usage gtd list [<context> ...]
or 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."""
break
/// 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."""
break
/// Online help for the `list-projects` command.
/// @org gtd.jdb-labs.com/cli/GTDCLI/hemp/list-projects
case ~/list-projects/: println """\
usage gtd list-projects
This command lists all of the project folders defined in this repository (all
the folders in the /projects folder."""
break
/// Online help for the `list-contexts` command.
/// @org gtd.jdb-labs.com/cli/GTDCLI/hemp/list-contexts
case ~/list-contexts/: println """\
usage gtd list-contexts
This command lists all of the context folders defined in this repository (all
the folders in the /next-actions and /waiting folders."""
break
/// Online help for the `rename-project` command.
/// @org gtd.jdb-labs.com/cli/GTDCLI/help/rename-project
case ~/delegate/: println """\
usage gtd rename-project <existing-project> <new-name>
or gtd rp <existing-project> <new-name>
This command renames a project directory and updates any items that reference
the project."""
break
}
}
}
@ -729,6 +1107,7 @@ context or project is named, all contexts are listed."""
contextFile = line ? new File(baseDir, line) : baseDir
while (!contextFile.exists() || !contextFile.isDirectory()) {
log.warn "'$line' is not a valid context."
println "Available contexts:"
baseDir.eachDir { print "\t${it.name}"}
println ""
@ -749,7 +1128,7 @@ context or project is named, all contexts are listed."""
* palatable for a filename. */
public static String stringToFilename(String s) {
return s.replaceAll(/\s/, '-').
replaceAll(/[';:(\.$)]/, '').
replaceAll(/[';:(\.$\/)]/, '').
toLowerCase() }
}

View File

@ -120,7 +120,7 @@ public class GTDServlet extends HttpServlet {
/// Get this user's session
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
/// session.
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
/// `404 Not Found` or `405 Method Not Allowed`
switch (request.servletPath) {
case ~/\/gtd\/contexts.*/:
case ~/\/gtd\/projects.*/:
case ~/\/contexts.*/:
case ~/\/projects.*/:
response.status = SC_METHOD_NOT_ALLOWED
return
default:
@ -207,14 +207,14 @@ public class GTDServlet extends HttpServlet {
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`
case "/logout":
session.removeAttribute("authenticated")
session.invalidate()
break
/// ##### `/gtd/contexts`
/// ##### `/contexts`
/// Return the list of contexts that are readable by this user.
case "/contexts":
@ -230,7 +230,7 @@ public class GTDServlet extends HttpServlet {
writeJSON(returnData, response)
break
/// ##### `/gtd/contexts/<contextId>`
/// ##### `/contexts/<contextId>`
/// Return data for the requested context, assuming it is
/// readable for this user.
case ~'/contexts/(.+)':
@ -251,7 +251,7 @@ public class GTDServlet extends HttpServlet {
writeJSON(returnData, response)
break
/// ##### `/gtd/projects`
/// ##### `/projects`
/// Return the list of projects that are readable for this user.
case "/projects":
/// Filter the project directories to find the ones that the
@ -264,7 +264,7 @@ public class GTDServlet extends HttpServlet {
writeJSON(returnData, response)
break
/// ##### `/gtd/projects/<projectId>`
/// ##### `/projects/<projectId>`
/// Return data for the requested project, assuming it is readable
/// for this user.
case ~'/projects/(.+)':
@ -286,7 +286,7 @@ public class GTDServlet extends HttpServlet {
writeJSON(returnData, response)
break
/// ##### `/gtd/next-actions/<contexts-and-projects>`
/// ##### `/next-actions/<contexts-and-projects>`
/// Return all of the items contained in the named contexts and
/// projects, assuming the user has access to them.
/// `<contexts-and-projects>` is expected to be a comma-delimited