Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
acaf58f456 | |||
5ac69157dc | |||
b4e01b6098 | |||
415c0e622f |
BIN
lib/compile/jar/logback-classic-0.9.26.jar
Normal file
BIN
lib/compile/jar/logback-classic-0.9.26.jar
Normal file
Binary file not shown.
BIN
lib/compile/jar/logback-core-0.9.26.jar
Normal file
BIN
lib/compile/jar/logback-core-0.9.26.jar
Normal file
Binary file not shown.
0
lib/compile/jar/logback.groovy
Normal file
0
lib/compile/jar/logback.groovy
Normal file
0
lib/runtime/jar/logback.groovy
Normal file
0
lib/runtime/jar/logback.groovy
Normal file
@ -1,8 +1,8 @@
|
|||||||
#Mon, 21 Oct 2013 14:08:15 +0000
|
#Tue, 05 Nov 2013 08:44:25 -0600
|
||||||
lib.local=true
|
lib.local=true
|
||||||
name=jdb-gtd
|
name=jdb-gtd
|
||||||
version=1.5
|
version=1.9
|
||||||
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=1
|
build.number=5
|
||||||
|
@ -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.5"
|
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
|
||||||
@ -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,159 @@ 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}'." }
|
||||||
|
|
||||||
|
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
|
||||||
@ -729,6 +955,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 +976,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() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user