222 lines
6.8 KiB
Nim
Raw Normal View History

2018-05-11 18:39:40 -05:00
## Personal Issue Tracker
## ======================
##
import cliutils, docopt, json, logging, os, ospaths, sequtils, strutils,
2018-05-11 21:35:03 -05:00
tables, times, unicode, uuids
2018-05-11 18:39:40 -05:00
import pit/private/libpit
export libpit
type
CliContext = ref object
tasksDir*: string
contexts*: TableRef[string, string]
issues*: TableRef[IssueState, seq[Issue]]
2018-05-12 11:40:15 -05:00
termWidth*: int
2018-05-11 18:39:40 -05:00
cfg*: CombinedConfig
proc initContext(args: Table[string, Value]): CliContext =
let pitrcLocations = @[
if args["--config"]: $args["--config"] else: "",
".pitrc", $getEnv("PITRC"), $getEnv("HOME") & "/.pitrc"]
var pitrcFilename: string =
foldl(pitrcLocations, if len(a) > 0: a elif existsFile(b): b else: "")
if not existsFile(pitrcFilename):
warn "pit: could not find .pitrc file: " & pitrcFilename
if isNilOrWhitespace(pitrcFilename):
pitrcFilename = $getEnv("HOME") & "/.pitrc"
var cfgFile: File
try:
cfgFile = open(pitrcFilename, fmWrite)
cfgFile.write("{\"tasksDir\": \"/path/to/tasks\"}")
except: warn "pit: could not write default .pitrc to " & pitrcFilename
finally: close(cfgFile)
var cfgJson: JsonNode
try: cfgJson = parseFile(pitrcFilename)
except: raise newException(IOError,
"unable to read config file: " & pitrcFilename &
"\x0D\x0A" & getCurrentExceptionMsg())
let cfg = CombinedConfig(docopt: args, json: cfgJson)
result = CliContext(
cfg: cfg,
tasksDir: cfg.getVal("tasks-dir", ""),
contexts: newTable[string,string](),
2018-05-12 11:40:15 -05:00
issues: newTable[IssueState, seq[Issue]](),
termWidth: parseInt(cfg.getVal("term-width", "80")))
2018-05-11 18:39:40 -05:00
if cfgJson.hasKey("contexts"):
for k, v in cfgJson["contexts"]:
result.contexts[k] = v.getStr()
if isNilOrWhitespace(result.tasksDir):
raise newException(Exception, "no tasks directory configured")
if not existsDir(result.tasksDir):
raise newException(Exception, "cannot find tasks dir: " & result.tasksDir)
proc getIssueContextDisplayName(ctx: CliContext, context: string): string =
if not ctx.contexts.hasKey(context): return context.capitalize()
return ctx.contexts[context]
proc formatIssue(ctx: CliContext, issue: Issue, state: IssueState,
width: int, indent: string, topPadded: bool): string =
var showDetails = not issue.details.isNilOrWhitespace
var lines: seq[string] = @[]
if showDetails and not topPadded: lines.add("")
var wrappedSummary = issue.summary.wordWrap(width - 2).indent(2)
wrappedSummary = "*" & wrappedSummary[1..^1]
lines.add(wrappedSummary.indent(indent.len))
if state == Pending and issue.properties.hasKey("pending"):
let startIdx = "Pending: ".len
2018-05-12 11:40:15 -05:00
var pendingText = issue["pending"].wordWrap(width - startIdx - 2)
.indent(startIdx)
2018-05-11 18:39:40 -05:00
pendingText = ("Pending: " & pendingText[startIdx..^1]).indent(indent.len + 2)
lines.add(pendingText)
if showDetails: lines.add(issue.details.indent(indent.len + 2))
return lines.join("\n")
proc formatSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
2018-05-12 11:40:15 -05:00
indent = ""): string =
let innerWidth = ctx.termWidth - (indent.len * 2)
2018-05-11 18:39:40 -05:00
var lines: seq[string] = @[]
lines.add(indent & ".".repeat(innerWidth))
2018-05-12 11:40:15 -05:00
lines.add(state.displayName.center(ctx.termWidth))
2018-05-11 18:39:40 -05:00
lines.add("")
var topPadded = true
let issuesByContext = issues.groupBy("context")
if issues.len > 5 and issuesByContext.len > 1:
for context, ctxIssues in issuesByContext:
lines.add(indent & ctx.getIssueContextDisplayName(context) & ":")
lines.add("")
for i in ctxIssues:
lines.add(ctx.formatIssue(i, state, innerWidth - 2, indent & " ", topPadded))
topPadded = not i.details.isNilOrWhitespace
if not topPadded: lines.add("")
else:
for i in issues:
lines.add(ctx.formatIssue(i, state, innerWidth, indent, topPadded))
topPadded = not i.details.isNilOrWhitespace
lines.add("")
return lines.join("\n")
2018-05-12 11:40:15 -05:00
proc loadIssues(ctx: CliContext, state: IssueState) =
ctx.issues[state] = loadIssues(joinPath(ctx.tasksDir, $state))
2018-05-11 18:39:40 -05:00
proc loadAllIssues(ctx: CliContext) =
2018-05-12 11:40:15 -05:00
for state in IssueState: ctx.loadIssues(state)
2018-05-11 18:39:40 -05:00
proc sameDay(a, b: DateTime): bool =
result = a.year == b.year and a.yearday == b.yearday
2018-05-12 11:40:15 -05:00
proc formatHeader(ctx: CliContext, header: string): string =
var lines: seq[string] = @[]
lines.add('_'.repeat(ctx.termWidth))
lines.add(header.center(ctx.termWidth))
lines.add('~'.repeat(ctx.termWidth))
lines.add("")
return lines.join("\n")
2018-05-11 18:39:40 -05:00
when isMainModule:
try:
let doc = """
Usage:
pit new <state> <summary> [options]
pit list [<state>] [options]
pit today
pit start
pit done
pit pending
pit edit
Options:
-t, --tags <tags> Specify tags for an issue.
-p, --properties <props> Specify properties for an issue. Formatted as "key:val;key:val"
-C, --config <cfgFile> Location of the config file (defaults to $HOME/.pitrc)
-h, --help Print this usage information.
-T, --today Limit to today's issues.
2018-05-12 11:40:15 -05:00
-F, --future Limit to future issues.
2018-05-11 18:39:40 -05:00
-E, --echo-args Echo arguments (for debug purposes).
--tasks-dir Path to the tasks directory (defaults to the value
configured in the .pitrc file)
2018-05-12 11:40:15 -05:00
--term-width Manually set the terminal width to use.
2018-05-11 18:39:40 -05:00
"""
logging.addHandler(newConsoleLogger())
# Parse arguments
let args = docopt(doc, version = "ptk 0.12.1")
if args["--echo-args"]: echo $args
if args["--help"]:
echo doc
quit()
let now = getTime().local
let ctx = initContext(args)
## Actual command runners
if args["list"]:
2018-05-12 11:40:15 -05:00
# Specific state request
if args["<state>"]:
let state = parseEnum[IssueState]($args["<state>"])
ctx.loadIssues(state)
echo ctx.formatSection(ctx.issues[state], state)
else:
2018-05-11 18:39:40 -05:00
2018-05-12 11:40:15 -05:00
let showBoth = args["--today"] == args["--future"]
let indent = if showBoth: " " else: ""
ctx.loadAllIssues()
2018-05-11 18:39:40 -05:00
2018-05-12 11:40:15 -05:00
# Today's items
if args["--today"] or showBoth:
if showBoth: echo ctx.formatHeader("Today")
2018-05-11 18:39:40 -05:00
2018-05-12 11:40:15 -05:00
for s in [Current, TodoToday]:
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
echo ctx.formatSection(ctx.issues[s], s, indent)
2018-05-11 18:39:40 -05:00
2018-05-12 11:40:15 -05:00
if ctx.issues.hasKey(Done):
let doneIssues = ctx.issues[Done].filterIt(
it.properties.hasKey("completed") and
sameDay(now, it.getDateTime("completed")))
if doneIssues.len > 0:
echo ctx.formatSection(doneIssues, Done, indent)
2018-05-11 18:39:40 -05:00
2018-05-12 11:40:15 -05:00
# Future items
if args["--future"] or showBoth:
if showBoth: echo ctx.formatHeader("Future")
2018-05-11 18:39:40 -05:00
2018-05-12 11:40:15 -05:00
for s in [Pending, Todo]:
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
echo ctx.formatSection(ctx.issues[s], s, indent)
2018-05-11 18:39:40 -05:00
except:
fatal "pit: " & getCurrentExceptionMsg()
#raise getCurrentException()
quit(QuitFailure)