|
|
|
@ -5,8 +5,8 @@
|
|
|
|
|
import cliutils, docopt, json, logging, options, os, ospaths, sequtils,
|
|
|
|
|
tables, terminal, times, unicode, uuids
|
|
|
|
|
|
|
|
|
|
import strutils except capitalize
|
|
|
|
|
import pit/private/libpit
|
|
|
|
|
import strutils except capitalize, toUpper, toLower
|
|
|
|
|
import pitpkg/private/libpit
|
|
|
|
|
export libpit
|
|
|
|
|
|
|
|
|
|
type
|
|
|
|
@ -68,9 +68,9 @@ proc getIssueContextDisplayName(ctx: CliContext, context: string): string =
|
|
|
|
|
else: return context.capitalize()
|
|
|
|
|
return ctx.contexts[context]
|
|
|
|
|
|
|
|
|
|
proc writeIssue(ctx: CliContext, issue: Issue, state: IssueState,
|
|
|
|
|
width: int, indent: string, topPadded: bool) =
|
|
|
|
|
var showDetails = not issue.details.isNilOrWhitespace
|
|
|
|
|
proc writeIssue(ctx: CliContext, issue: Issue, width: int, indent = "",
|
|
|
|
|
verbose = false, topPadded = false) =
|
|
|
|
|
var showDetails = not issue.details.isNilOrWhitespace and verbose
|
|
|
|
|
|
|
|
|
|
if showDetails and not topPadded: stdout.writeLine("")
|
|
|
|
|
|
|
|
|
@ -92,7 +92,7 @@ proc writeIssue(ctx: CliContext, issue: Issue, state: IssueState,
|
|
|
|
|
else: stdout.writeLine("")
|
|
|
|
|
stdout.resetAttributes
|
|
|
|
|
|
|
|
|
|
if state == Pending and issue.hasProp("pending"):
|
|
|
|
|
if issue.hasProp("pending"):
|
|
|
|
|
let startIdx = "Pending: ".len
|
|
|
|
|
var pendingText = issue["pending"].wordWrap(width - startIdx - 2)
|
|
|
|
|
.indent(startIdx)
|
|
|
|
@ -103,7 +103,7 @@ proc writeIssue(ctx: CliContext, issue: Issue, state: IssueState,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc writeSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
|
|
|
|
|
indent = "") =
|
|
|
|
|
indent = "", verbose = false) =
|
|
|
|
|
let innerWidth = ctx.termWidth - (indent.len * 2)
|
|
|
|
|
|
|
|
|
|
stdout.setForegroundColor(fgBlue, true)
|
|
|
|
@ -112,27 +112,28 @@ proc writeSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
|
|
|
|
|
stdout.writeLine("")
|
|
|
|
|
stdout.resetAttributes
|
|
|
|
|
|
|
|
|
|
var topPadded = true
|
|
|
|
|
|
|
|
|
|
let issuesByContext = issues.groupBy("context")
|
|
|
|
|
|
|
|
|
|
var topPadded = true
|
|
|
|
|
|
|
|
|
|
if issues.len > 5 and issuesByContext.len > 1:
|
|
|
|
|
for context, ctxIssues in issuesByContext:
|
|
|
|
|
topPadded = true
|
|
|
|
|
stdout.setForegroundColor(fgYellow, false)
|
|
|
|
|
stdout.writeLine(indent & ctx.getIssueContextDisplayName(context) & ":")
|
|
|
|
|
stdout.writeLine("")
|
|
|
|
|
stdout.resetAttributes
|
|
|
|
|
|
|
|
|
|
for i in ctxIssues:
|
|
|
|
|
ctx.writeIssue(i, state, innerWidth - 2, indent & " ", topPadded)
|
|
|
|
|
topPadded = not i.details.isNilOrWhitespace
|
|
|
|
|
ctx.writeIssue(i, innerWidth - 2, indent & " ", verbose, topPadded)
|
|
|
|
|
topPadded = not i.details.isNilOrWhitespace and verbose
|
|
|
|
|
|
|
|
|
|
if not topPadded: stdout.writeLine("")
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
for i in issues:
|
|
|
|
|
ctx.writeIssue(i, state, innerWidth, indent, topPadded)
|
|
|
|
|
topPadded = not i.details.isNilOrWhitespace
|
|
|
|
|
ctx.writeIssue(i, innerWidth, indent, verbose, topPadded)
|
|
|
|
|
topPadded = not i.details.isNilOrWhitespace and verbose
|
|
|
|
|
|
|
|
|
|
stdout.writeLine("")
|
|
|
|
|
|
|
|
|
@ -184,12 +185,12 @@ proc edit(issue: Issue) =
|
|
|
|
|
getCurrentExceptionMsg()
|
|
|
|
|
issue.store()
|
|
|
|
|
|
|
|
|
|
proc list(ctx: CliContext, filter: Option[IssueFilter], state: Option[IssueState], today, future: bool) =
|
|
|
|
|
proc list(ctx: CliContext, filter: Option[IssueFilter], state: Option[IssueState], today, future, verbose: bool) =
|
|
|
|
|
|
|
|
|
|
if state.isSome:
|
|
|
|
|
ctx.loadIssues(state.get)
|
|
|
|
|
if filter.isSome: ctx.filterIssues(filter.get)
|
|
|
|
|
ctx.writeSection(ctx.issues[state.get], state.get)
|
|
|
|
|
ctx.writeSection(ctx.issues[state.get], state.get, "", verbose)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
ctx.loadAllIssues()
|
|
|
|
@ -203,14 +204,14 @@ proc list(ctx: CliContext, filter: Option[IssueFilter], state: Option[IssueState
|
|
|
|
|
|
|
|
|
|
for s in [Current, TodoToday]:
|
|
|
|
|
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
|
|
|
|
|
ctx.writeSection(ctx.issues[s], s, indent)
|
|
|
|
|
ctx.writeSection(ctx.issues[s], s, indent, verbose)
|
|
|
|
|
|
|
|
|
|
if ctx.issues.hasKey(Done):
|
|
|
|
|
let doneIssues = ctx.issues[Done].filterIt(
|
|
|
|
|
it.hasProp("completed") and
|
|
|
|
|
sameDay(getTime().local, it.getDateTime("completed")))
|
|
|
|
|
if doneIssues.len > 0:
|
|
|
|
|
ctx.writeSection(doneIssues, Done, indent)
|
|
|
|
|
ctx.writeSection(doneIssues, Done, indent, verbose)
|
|
|
|
|
|
|
|
|
|
# Future items
|
|
|
|
|
if future:
|
|
|
|
@ -218,33 +219,40 @@ proc list(ctx: CliContext, filter: Option[IssueFilter], state: Option[IssueState
|
|
|
|
|
|
|
|
|
|
for s in [Pending, Todo]:
|
|
|
|
|
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
|
|
|
|
|
ctx.writeSection(ctx.issues[s], s, indent)
|
|
|
|
|
ctx.writeSection(ctx.issues[s], s, indent, verbose)
|
|
|
|
|
|
|
|
|
|
when isMainModule:
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
let doc = """
|
|
|
|
|
Usage:
|
|
|
|
|
pit new <summary> [<state>] [options]
|
|
|
|
|
pit list [<state>] [options]
|
|
|
|
|
pit ( start | done | pending | do-today | todo ) <id>...
|
|
|
|
|
pit ( new | add) <summary> [<state>] [options]
|
|
|
|
|
pit list [<listable>] [options]
|
|
|
|
|
pit ( start | done | pending | do-today | todo | suspend ) <id>...
|
|
|
|
|
pit edit <id>
|
|
|
|
|
pit ( delete | rm ) <id>...
|
|
|
|
|
|
|
|
|
|
Options:
|
|
|
|
|
|
|
|
|
|
-h, --help Print this usage information.
|
|
|
|
|
|
|
|
|
|
-t, --tags <tags> Specify tags for an issue.
|
|
|
|
|
|
|
|
|
|
-p, --properties <props> Specify properties. Formatted as "key:val;key:val"
|
|
|
|
|
When used with the list command this option applies
|
|
|
|
|
a filter to the issues listed, only allowing those
|
|
|
|
|
which have all of the given properties.
|
|
|
|
|
|
|
|
|
|
-c, --context <ctxName> Shorthand for '-p context:<ctxName>'
|
|
|
|
|
|
|
|
|
|
-t, --tags <tags> Specify tags for an issue.
|
|
|
|
|
|
|
|
|
|
-T, --today Limit to today's issues.
|
|
|
|
|
|
|
|
|
|
-F, --future Limit to future issues.
|
|
|
|
|
|
|
|
|
|
-v, --verbose Show issue details when listing issues.
|
|
|
|
|
|
|
|
|
|
-y, --yes Automatically answer "yes" to any prompts.
|
|
|
|
|
|
|
|
|
|
-C, --config <cfgFile> Location of the config file (defaults to $HOME/.pitrc)
|
|
|
|
|
|
|
|
|
|
-E, --echo-args Echo arguments (for debug purposes).
|
|
|
|
@ -258,7 +266,7 @@ Options:
|
|
|
|
|
logging.addHandler(newConsoleLogger())
|
|
|
|
|
|
|
|
|
|
# Parse arguments
|
|
|
|
|
let args = docopt(doc, version = "pit 4.0.0")
|
|
|
|
|
let args = docopt(doc, version = "pit 4.0.6")
|
|
|
|
|
|
|
|
|
|
if args["--echo-args"]: stderr.writeLine($args)
|
|
|
|
|
|
|
|
|
@ -268,8 +276,24 @@ Options:
|
|
|
|
|
|
|
|
|
|
let ctx = initContext(args)
|
|
|
|
|
|
|
|
|
|
# Create our tasks directory structure if needed
|
|
|
|
|
for s in IssueState:
|
|
|
|
|
if not existsDir(ctx.tasksDir / $s):
|
|
|
|
|
(ctx.tasksDir / $s).createDir
|
|
|
|
|
|
|
|
|
|
var propertiesOption = none(TableRef[string,string])
|
|
|
|
|
|
|
|
|
|
if args["--properties"] or args["--context"]:
|
|
|
|
|
|
|
|
|
|
var props =
|
|
|
|
|
if args["--properties"]: parsePropertiesOption($args["--properties"])
|
|
|
|
|
else: newTable[string,string]()
|
|
|
|
|
|
|
|
|
|
if args["--context"]: props["context"] = $args["--context"]
|
|
|
|
|
propertiesOption = some(props)
|
|
|
|
|
|
|
|
|
|
## Actual command runners
|
|
|
|
|
if args["new"]:
|
|
|
|
|
if args["new"] or args["add"]:
|
|
|
|
|
let state =
|
|
|
|
|
if args["<state>"]: parseEnum[IssueState]($args["<state>"])
|
|
|
|
|
else: TodoToday
|
|
|
|
@ -277,9 +301,7 @@ Options:
|
|
|
|
|
var issue = Issue(
|
|
|
|
|
id: genUUID(),
|
|
|
|
|
summary: $args["<summary>"],
|
|
|
|
|
properties:
|
|
|
|
|
if args["--properties"]: parsePropertiesOption($args["--properties"])
|
|
|
|
|
else: newTable[string,string](),
|
|
|
|
|
properties: propertiesOption.get(newTable[string,string]()),
|
|
|
|
|
tags:
|
|
|
|
|
if args["--tags"]: ($args["tags"]).split(",").mapIt(it.strip)
|
|
|
|
|
else: newSeq[string]())
|
|
|
|
@ -292,7 +314,7 @@ Options:
|
|
|
|
|
edit(ctx.tasksDir.loadIssueById(issueId))
|
|
|
|
|
|
|
|
|
|
elif args["start"] or args["do-today"] or args["done"] or
|
|
|
|
|
args["pending"] or args["todo"]:
|
|
|
|
|
args["pending"] or args["todo"] or args["suspend"]:
|
|
|
|
|
|
|
|
|
|
var targetState: IssueState
|
|
|
|
|
if args["done"]: targetState = Done
|
|
|
|
@ -300,9 +322,10 @@ Options:
|
|
|
|
|
elif args["pending"]: targetState = Todo
|
|
|
|
|
elif args["start"]: targetState = Current
|
|
|
|
|
elif args["todo"]: targetState = Todo
|
|
|
|
|
elif args["suspend"]: targetState = Dormant
|
|
|
|
|
|
|
|
|
|
for id in @(args["<id>"]):
|
|
|
|
|
ctx.tasksDir.moveIssue(ctx.tasksDir.loadIssueById(id), targetState)
|
|
|
|
|
ctx.tasksDir.loadIssueById(id).changeState(ctx.tasksDir, targetState)
|
|
|
|
|
|
|
|
|
|
if ctx.triggerPtk:
|
|
|
|
|
if targetState == Current:
|
|
|
|
@ -313,25 +336,47 @@ Options:
|
|
|
|
|
discard execShellCmd(cmd)
|
|
|
|
|
elif targetState == Done: discard execShellCmd("ptk stop")
|
|
|
|
|
|
|
|
|
|
elif args["delete"] or args["rm"]:
|
|
|
|
|
for id in @(args["<id>"]):
|
|
|
|
|
|
|
|
|
|
let issue = ctx.tasksDir.loadIssueById(id)
|
|
|
|
|
|
|
|
|
|
if not args["--yes"]:
|
|
|
|
|
stderr.write("Delete '" & issue.summary & "' (y/n)? ")
|
|
|
|
|
if not "yes".startsWith(stdin.readLine.toLower):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
issue.delete
|
|
|
|
|
|
|
|
|
|
elif args["list"]:
|
|
|
|
|
|
|
|
|
|
let filter = initFilter()
|
|
|
|
|
var filterOption = none(IssueFilter)
|
|
|
|
|
if args["--properties"]:
|
|
|
|
|
filter.properties = parsePropertiesOption($args["--properties"])
|
|
|
|
|
if propertiesOption.isSome:
|
|
|
|
|
filter.properties = propertiesOption.get
|
|
|
|
|
filterOption = some(filter)
|
|
|
|
|
|
|
|
|
|
let stateOption =
|
|
|
|
|
if args["<state>"]: some(parseEnum[IssueState]($args["<state>"]))
|
|
|
|
|
else: none(IssueState)
|
|
|
|
|
var stateOption = none(IssueState)
|
|
|
|
|
var issueIdOption = none(string)
|
|
|
|
|
if args["<listable>"]:
|
|
|
|
|
try: stateOption = some(parseEnum[IssueState]($args["<listable>"]))
|
|
|
|
|
except: issueIdOption = some($args["<listable>"])
|
|
|
|
|
|
|
|
|
|
# List a specific issue
|
|
|
|
|
if issueIdOption.isSome:
|
|
|
|
|
let issue = ctx.tasksDir.loadIssueById(issueIdOption.get)
|
|
|
|
|
ctx.writeIssue(issue, ctx.termWidth, "", true, true)
|
|
|
|
|
|
|
|
|
|
# List all issues
|
|
|
|
|
else:
|
|
|
|
|
let showBoth = args["--today"] == args["--future"]
|
|
|
|
|
ctx.list(filterOption, stateOption, showBoth or args["--today"],
|
|
|
|
|
showBoth or args["--future"])
|
|
|
|
|
showBoth or args["--future"],
|
|
|
|
|
args["--verbose"])
|
|
|
|
|
|
|
|
|
|
if ctx.autoList and not args["list"]:
|
|
|
|
|
ctx.loadAllIssues()
|
|
|
|
|
ctx.list(none(IssueFilter), none(IssueState), true, true)
|
|
|
|
|
ctx.list(none(IssueFilter), none(IssueState), true, true, false)
|
|
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
fatal "pit: " & getCurrentExceptionMsg()
|
|
|
|
|