8 Commits
v1.8 ... v1.13

Author SHA1 Message Date
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
10 changed files with 247 additions and 35 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

@ -1,8 +1,8 @@
#Sat, 02 Nov 2013 23:33:05 -0500 #Tue, 16 Dec 2014 10:49:37 -0600
lib.local=true lib.local=true
name=jdb-gtd name=jdb-gtd
version=1.8 version=1.13
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
build.number=72 build.number=2

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

@ -19,6 +19,7 @@ package com.jdblabs.gtd
* calendar to "schedule" items, but only to represent items which must be * calendar to "schedule" items, but only to represent items which must be
* done by or on that day. * done by or on that day.
* * `details`: more information related to this item. * * `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 * @org gtd.jdb-labs.com/Item
*/ */
public class Item { public class Item {

View File

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

View File

@ -17,6 +17,9 @@ 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
import com.martiansoftware.nailgun.NGContext import com.martiansoftware.nailgun.NGContext
import java.io.FileFilter
import java.nio.file.Files
import java.nio.file.Path
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
@ -24,6 +27,7 @@ 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.*
import static java.nio.file.StandardCopyOption.*
/** /**
* Command-line helper for working with this implementation of the Getting * Command-line helper for working with this implementation of the Getting
@ -31,7 +35,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.8" public static final String VERSION = "1.13"
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
@ -107,7 +111,7 @@ public class GTDCLI {
/// read afresh the configuration file. /// read afresh the configuration file.
nailgunInst = null nailgunInst = null
nailgunInst = new GTDCLI(new File( nailgunInst = new GTDCLI(new File(
System.getProperty("user.home"), ".gritterrc")) System.getProperty("user.home"), ".gtdclirc"))
nailgunInst.run(args) } } nailgunInst.run(args) } }
@ -203,7 +207,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].
@ -242,6 +246,8 @@ public class GTDCLI {
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 ~/debug/: debug(parsedArgs); break;
case ~/delegate/: delegateAction(parsedArgs); break;
case ~/rp|rename-project/: renameProject(parsedArgs); break;
default: default:
log.error "Unrecognized command: ${command}" log.error "Unrecognized command: ${command}"
break } } } break } } }
@ -349,7 +355,7 @@ 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.toString())) } stringToFilename(item.toString())) }
@ -357,14 +363,14 @@ public class GTDCLI {
/// 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.toString())) } 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)"])
@ -399,13 +405,13 @@ public class GTDCLI {
*/ */
protected void done(LinkedList args) { protected void done(LinkedList args) {
def selectedFilePath = args.poll() def selectedFilePath
if (!selectedFilePath) { if (!args) {
log.error "gtd done command requires a <action-file> parameter." log.error "The 'gtd done' command requires an <action-file> parameter."
return } return }
while (selectedFilePath) { while ((selectedFilePath = args.poll())) {
def item def item
def selectedFile = new File(selectedFilePath) def selectedFile = new File(selectedFilePath)
@ -429,7 +435,7 @@ public class GTDCLI {
if (inPath(gtdDirs.projects, oldFile)) { if (inPath(gtdDirs.projects, oldFile)) {
/// Delete any copies of this item from the next actions folder. /// 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 " + println "Deleting duplicate entry from the " +
"${file.parentFile.name} context." "${file.parentFile.name} context."
if (file.exists()) file.delete() } if (file.exists()) file.delete() }
@ -453,7 +459,6 @@ public class GTDCLI {
/// Delete the original /// Delete the original
oldFile.delete() oldFile.delete()
selectedFilePath = args.poll()
println "'$item' marked as done." } } println "'$item' marked as done." } }
/** #### `calendar` /** #### `calendar`
@ -614,14 +619,14 @@ 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.
def printItems = { dir -> def printItems = { dir ->
if (!dir.exists() || !dir.isDirectory()) return if (!dir.exists() || !dir.isDirectory()) return
println "-- ${getRelativePath(gtdDirs.root, dir)} --" println "-- ${getRelativePath(gtdDirs.root, dir)} --"
dir.eachFile { file -> dir.listFiles().sort { it.name }.each { file ->
if (!file.exists() || !file.isFile() || file.isHidden() || if (!file.exists() || !file.isFile() || file.isHidden() ||
file.name.startsWith('.')) file.name.startsWith('.'))
return return
@ -631,21 +636,20 @@ public class GTDCLI {
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` /** #### `debug`
* Print out debug information. Currently this prints out the internal * Print out debug information. Currently this prints out the internal
@ -682,6 +686,162 @@ public class GTDCLI {
else setLoggingThreshold(level) } else setLoggingThreshold(level) }
else log.error "Unrecognized debug command: '${command}'." } 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 print(String msg) { log.info(msg) }
private void println(String line) { log.info(line + EOL) } private void println(String line) { log.info(line + EOL) }
@ -704,20 +864,41 @@ options are:
top-level commands: top-level commands:
help <command> Print detailed help about a command. help <command> Print detailed help about a command.
process Process inbox items systematically. process Process inbox items systematically.
done <action-file> Mark an action as done. This will automatically done <action-file> Mark an action as done. This will automatically
take care of duplicates of the action in project take care of duplicates of the action in project
or next-actions sub-folders. or next-actions sub-folders.
calendar Show the tasks with specific days assigned to calendar Show the tasks with specific days assigned to
them, sorted by date. them, sorted by date.
list-copies <action-file> Given an action item, list all the other places list-copies <action-file> Given an action item, list all the other places
there the same item is filed (cross-reference there the same item is filed (cross-reference
with a project folder, for example). with a project folder, for example).
new Interactively create a new action item in the new Interactively create a new action item in the
current folder. current folder.
tickler Search the tickler file for items that need to be tickler Search the tickler file for items that need to be
delivered and move them to the *next-actions* 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 { } else {
def command = args.poll() def command = args.poll()
@ -821,15 +1002,35 @@ 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.""" and moves them out of the tickler file and into the next-actions file."""
break 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 /// @org gtd.jdb-labs.com/cli/GTDCLI/help/ls
case ~/ls|list-context/: println """\ case ~/ls|list/: println """\
usage gtd ls [<context> ...] usage gtd list [<context> ...]
or gtd ls [<context> ...]
This command lists all the tasks for a given context or project. The purpose is 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."""
/// 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."""
} }
} }
} }