Compare commits

..

5 Commits
4.0.2 ... 4.0.7

Author SHA1 Message Date
6f247032a3 Add created property when creating issues. 2018-05-14 17:17:47 -05:00
efd5f6adff Add versbose flag, list specific issue. 2018-05-14 12:21:05 -05:00
49c5753ef1 Add rm as an alias for delete. 2018-05-14 10:09:33 -05:00
3bdb2ecb1f Fix padding issue in context listing. 2018-05-14 10:04:24 -05:00
28569a643e Added Dormant state, auto-create task dirs.
The Dormant state is for tasks that are still outstanding but not of
immediate importance. The main different between Dormant and Todo is
that dormant tasks are not listed by default. You must
`pit list dormant` to see them.
2018-05-14 09:53:15 -05:00
3 changed files with 76 additions and 40 deletions

View File

@ -1,6 +1,6 @@
# Package # Package
version = "4.0.2" version = "4.0.7"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Personal issue tracker." description = "Personal issue tracker."
license = "MIT" license = "MIT"

View File

@ -3,7 +3,7 @@
## ##
import cliutils, docopt, json, logging, options, os, ospaths, sequtils, import cliutils, docopt, json, logging, options, os, ospaths, sequtils,
tables, terminal, times, unicode, uuids tables, terminal, times, timeutils, unicode, uuids
import strutils except capitalize, toUpper, toLower import strutils except capitalize, toUpper, toLower
import pitpkg/private/libpit import pitpkg/private/libpit
@ -68,9 +68,9 @@ proc getIssueContextDisplayName(ctx: CliContext, context: string): string =
else: return context.capitalize() else: return context.capitalize()
return ctx.contexts[context] return ctx.contexts[context]
proc writeIssue(ctx: CliContext, issue: Issue, state: IssueState, proc writeIssue(ctx: CliContext, issue: Issue, width: int, indent = "",
width: int, indent: string, topPadded: bool) = verbose = false, topPadded = false) =
var showDetails = not issue.details.isNilOrWhitespace var showDetails = not issue.details.isNilOrWhitespace and verbose
if showDetails and not topPadded: stdout.writeLine("") if showDetails and not topPadded: stdout.writeLine("")
@ -92,7 +92,7 @@ proc writeIssue(ctx: CliContext, issue: Issue, state: IssueState,
else: stdout.writeLine("") else: stdout.writeLine("")
stdout.resetAttributes stdout.resetAttributes
if state == Pending and issue.hasProp("pending"): if issue.hasProp("pending"):
let startIdx = "Pending: ".len let startIdx = "Pending: ".len
var pendingText = issue["pending"].wordWrap(width - startIdx - 2) var pendingText = issue["pending"].wordWrap(width - startIdx - 2)
.indent(startIdx) .indent(startIdx)
@ -103,7 +103,7 @@ proc writeIssue(ctx: CliContext, issue: Issue, state: IssueState,
proc writeSection(ctx: CliContext, issues: seq[Issue], state: IssueState, proc writeSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
indent = "") = indent = "", verbose = false) =
let innerWidth = ctx.termWidth - (indent.len * 2) let innerWidth = ctx.termWidth - (indent.len * 2)
stdout.setForegroundColor(fgBlue, true) stdout.setForegroundColor(fgBlue, true)
@ -112,27 +112,28 @@ proc writeSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
stdout.writeLine("") stdout.writeLine("")
stdout.resetAttributes stdout.resetAttributes
var topPadded = true
let issuesByContext = issues.groupBy("context") let issuesByContext = issues.groupBy("context")
var topPadded = true
if issues.len > 5 and issuesByContext.len > 1: if issues.len > 5 and issuesByContext.len > 1:
for context, ctxIssues in issuesByContext: for context, ctxIssues in issuesByContext:
topPadded = true
stdout.setForegroundColor(fgYellow, false) stdout.setForegroundColor(fgYellow, false)
stdout.writeLine(indent & ctx.getIssueContextDisplayName(context) & ":") stdout.writeLine(indent & ctx.getIssueContextDisplayName(context) & ":")
stdout.writeLine("") stdout.writeLine("")
stdout.resetAttributes stdout.resetAttributes
for i in ctxIssues: for i in ctxIssues:
ctx.writeIssue(i, state, innerWidth - 2, indent & " ", topPadded) ctx.writeIssue(i, innerWidth - 2, indent & " ", verbose, topPadded)
topPadded = not i.details.isNilOrWhitespace topPadded = not i.details.isNilOrWhitespace and verbose
if not topPadded: stdout.writeLine("") if not topPadded: stdout.writeLine("")
else: else:
for i in issues: for i in issues:
ctx.writeIssue(i, state, innerWidth, indent, topPadded) ctx.writeIssue(i, innerWidth, indent, verbose, topPadded)
topPadded = not i.details.isNilOrWhitespace topPadded = not i.details.isNilOrWhitespace and verbose
stdout.writeLine("") stdout.writeLine("")
@ -184,12 +185,12 @@ proc edit(issue: Issue) =
getCurrentExceptionMsg() getCurrentExceptionMsg()
issue.store() 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: if state.isSome:
ctx.loadIssues(state.get) ctx.loadIssues(state.get)
if filter.isSome: ctx.filterIssues(filter.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 return
ctx.loadAllIssues() ctx.loadAllIssues()
@ -203,14 +204,14 @@ proc list(ctx: CliContext, filter: Option[IssueFilter], state: Option[IssueState
for s in [Current, TodoToday]: for s in [Current, TodoToday]:
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0: 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): if ctx.issues.hasKey(Done):
let doneIssues = ctx.issues[Done].filterIt( let doneIssues = ctx.issues[Done].filterIt(
it.hasProp("completed") and it.hasProp("completed") and
sameDay(getTime().local, it.getDateTime("completed"))) sameDay(getTime().local, it.getDateTime("completed")))
if doneIssues.len > 0: if doneIssues.len > 0:
ctx.writeSection(doneIssues, Done, indent) ctx.writeSection(doneIssues, Done, indent, verbose)
# Future items # Future items
if future: if future:
@ -218,7 +219,7 @@ proc list(ctx: CliContext, filter: Option[IssueFilter], state: Option[IssueState
for s in [Pending, Todo]: for s in [Pending, Todo]:
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0: 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: when isMainModule:
@ -226,26 +227,30 @@ when isMainModule:
let doc = """ let doc = """
Usage: Usage:
pit ( new | add) <summary> [<state>] [options] pit ( new | add) <summary> [<state>] [options]
pit list [<state>] [options] pit list [<listable>] [options]
pit ( start | done | pending | do-today | todo ) <id>... pit ( start | done | pending | do-today | todo | suspend ) <id>...
pit edit <id> pit edit <id>
pit delete <id>... pit ( delete | rm ) <id>...
Options: Options:
-h, --help Print this usage information. -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" -p, --properties <props> Specify properties. Formatted as "key:val;key:val"
When used with the list command this option applies When used with the list command this option applies
a filter to the issues listed, only allowing those a filter to the issues listed, only allowing those
which have all of the given properties. 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. -T, --today Limit to today's issues.
-F, --future Limit to future issues. -F, --future Limit to future issues.
-v, --verbose Show issue details when listing issues.
-y, --yes Automatically answer "yes" to any prompts. -y, --yes Automatically answer "yes" to any prompts.
-C, --config <cfgFile> Location of the config file (defaults to $HOME/.pitrc) -C, --config <cfgFile> Location of the config file (defaults to $HOME/.pitrc)
@ -261,7 +266,7 @@ Options:
logging.addHandler(newConsoleLogger()) logging.addHandler(newConsoleLogger())
# Parse arguments # Parse arguments
let args = docopt(doc, version = "pit 4.0.2") let args = docopt(doc, version = "pit 4.0.7")
if args["--echo-args"]: stderr.writeLine($args) if args["--echo-args"]: stderr.writeLine($args)
@ -271,18 +276,35 @@ Options:
let ctx = initContext(args) 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 ## Actual command runners
if args["new"] or args["add"]: if args["new"] or args["add"]:
let state = let state =
if args["<state>"]: parseEnum[IssueState]($args["<state>"]) if args["<state>"]: parseEnum[IssueState]($args["<state>"])
else: TodoToday else: TodoToday
var issueProps = propertiesOption.get(newTable[string,string]())
if not issueProps.hasKey("created"): issueProps["created"] = getTime().local.formatIso8601
var issue = Issue( var issue = Issue(
id: genUUID(), id: genUUID(),
summary: $args["<summary>"], summary: $args["<summary>"],
properties: properties: issueProps,
if args["--properties"]: parsePropertiesOption($args["--properties"])
else: newTable[string,string](),
tags: tags:
if args["--tags"]: ($args["tags"]).split(",").mapIt(it.strip) if args["--tags"]: ($args["tags"]).split(",").mapIt(it.strip)
else: newSeq[string]()) else: newSeq[string]())
@ -295,7 +317,7 @@ Options:
edit(ctx.tasksDir.loadIssueById(issueId)) edit(ctx.tasksDir.loadIssueById(issueId))
elif args["start"] or args["do-today"] or args["done"] or 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 var targetState: IssueState
if args["done"]: targetState = Done if args["done"]: targetState = Done
@ -303,6 +325,7 @@ Options:
elif args["pending"]: targetState = Todo elif args["pending"]: targetState = Todo
elif args["start"]: targetState = Current elif args["start"]: targetState = Current
elif args["todo"]: targetState = Todo elif args["todo"]: targetState = Todo
elif args["suspend"]: targetState = Dormant
for id in @(args["<id>"]): for id in @(args["<id>"]):
ctx.tasksDir.loadIssueById(id).changeState(ctx.tasksDir, targetState) ctx.tasksDir.loadIssueById(id).changeState(ctx.tasksDir, targetState)
@ -316,7 +339,7 @@ Options:
discard execShellCmd(cmd) discard execShellCmd(cmd)
elif targetState == Done: discard execShellCmd("ptk stop") elif targetState == Done: discard execShellCmd("ptk stop")
elif args["delete"]: elif args["delete"] or args["rm"]:
for id in @(args["<id>"]): for id in @(args["<id>"]):
let issue = ctx.tasksDir.loadIssueById(id) let issue = ctx.tasksDir.loadIssueById(id)
@ -332,21 +355,31 @@ Options:
let filter = initFilter() let filter = initFilter()
var filterOption = none(IssueFilter) var filterOption = none(IssueFilter)
if args["--properties"]: if propertiesOption.isSome:
filter.properties = parsePropertiesOption($args["--properties"]) filter.properties = propertiesOption.get
filterOption = some(filter) filterOption = some(filter)
let stateOption = var stateOption = none(IssueState)
if args["<state>"]: some(parseEnum[IssueState]($args["<state>"])) var issueIdOption = none(string)
else: none(IssueState) 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"] let showBoth = args["--today"] == args["--future"]
ctx.list(filterOption, stateOption, showBoth or args["--today"], 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"]: if ctx.autoList and not args["list"]:
ctx.loadAllIssues() ctx.loadAllIssues()
ctx.list(none(IssueFilter), none(IssueState), true, true) ctx.list(none(IssueFilter), none(IssueState), true, true, false)
except: except:
fatal "pit: " & getCurrentExceptionMsg() fatal "pit: " & getCurrentExceptionMsg()

View File

@ -15,6 +15,7 @@ type
Pending = "pending", Pending = "pending",
Done = "done", Done = "done",
Todo = "todo" Todo = "todo"
Dormant = "dormant"
IssueFilter* = ref object IssueFilter* = ref object
properties*: TableRef[string, string] properties*: TableRef[string, string]
@ -27,8 +28,9 @@ let ISSUE_FILE_PATTERN = re"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f
proc displayName*(s: IssueState): string = proc displayName*(s: IssueState): string =
case s case s
of Current: result = "Current" of Current: result = "Current"
of Pending: result = "Pending"
of Done: result = "Done" of Done: result = "Done"
of Dormant: result = "Dormant"
of Pending: result = "Pending"
of Todo: result = "Todo" of Todo: result = "Todo"
of TodoToday: result = "Todo" of TodoToday: result = "Todo"
@ -161,9 +163,10 @@ proc loadIssues*(path: string): seq[Issue] =
result.add(loadIssue(path)) result.add(loadIssue(path))
proc changeState*(issue: Issue, tasksDir: string, newState: IssueState) = proc changeState*(issue: Issue, tasksDir: string, newState: IssueState) =
removeFile(issue.filepath) let oldFilepath = issue.filepath
if newState == Done: issue.setDateTime("completed", getTime().local) if newState == Done: issue.setDateTime("completed", getTime().local)
tasksDir.store(issue, newState) tasksDir.store(issue, newState)
removeFile(oldFilepath)
proc delete*(issue: Issue) = removeFile(issue.filepath) proc delete*(issue: Issue) = removeFile(issue.filepath)