Add support for syncing to Probatem's Virtual Status Board.
This commit is contained in:
302
src/pit.nim
302
src/pit.nim
@@ -1,204 +1,19 @@
|
||||
## Personal Issue Tracker CLI interface
|
||||
## ====================================
|
||||
|
||||
import std/[algorithm, logging, options, os, sequtils, wordwrap, tables,
|
||||
terminal, times, unicode]
|
||||
import cliutils, data_uri, docopt, json, timeutils, uuids
|
||||
import std/[algorithm, logging, options, os, sequtils, tables, times, unicode]
|
||||
import data_uri, docopt, json, timeutils, uuids
|
||||
|
||||
from nre import re
|
||||
import strutils except alignLeft, capitalize, strip, toUpper, toLower
|
||||
import pit/[cliconstants, libpit]
|
||||
export libpit
|
||||
import pit/[cliconstants, formatting, libpit, sync_pbm_vsb]
|
||||
|
||||
type
|
||||
CliContext = ref object
|
||||
cfg*: PitConfig
|
||||
contexts*: TableRef[string, string]
|
||||
defaultContext*: Option[string]
|
||||
issues*: TableRef[IssueState, seq[Issue]]
|
||||
termWidth*: int
|
||||
triggerPtk*, verbose*: bool
|
||||
export formatting, libpit
|
||||
|
||||
let EDITOR =
|
||||
if existsEnv("EDITOR"): getEnv("EDITOR")
|
||||
else: "vi"
|
||||
|
||||
|
||||
proc initContext(args: Table[string, Value]): CliContext =
|
||||
let pitCfg = loadConfig(args)
|
||||
|
||||
let cliJson =
|
||||
if pitCfg.cfg.json.hasKey("cli"): pitCfg.cfg.json["cli"]
|
||||
else: newJObject()
|
||||
|
||||
let cliCfg = CombinedConfig(docopt: args, json: cliJson)
|
||||
|
||||
result = CliContext(
|
||||
cfg: pitCfg,
|
||||
contexts: pitCfg.contexts,
|
||||
defaultContext:
|
||||
if not cliJson.hasKey("defaultContext"): none(string)
|
||||
else: some(cliJson["defaultContext"].getStr()),
|
||||
verbose: parseBool(cliCfg.getVal("verbose", "false")) and not args["--quiet"],
|
||||
issues: newTable[IssueState, seq[Issue]](),
|
||||
termWidth: parseInt(cliCfg.getVal("termWidth", "80")),
|
||||
triggerPtk: cliJson.getOrDefault("triggerPtk").getBool(false))
|
||||
|
||||
proc getIssueContextDisplayName(ctx: CliContext, context: string): string =
|
||||
if not ctx.contexts.hasKey(context):
|
||||
if context.isEmptyOrWhitespace: return "<default>"
|
||||
else: return context.capitalize()
|
||||
return ctx.contexts[context]
|
||||
|
||||
proc formatIssue*(issue: Issue): string =
|
||||
result = ($issue.id).withColor(fgBlack, true) & "\n"&
|
||||
issue.summary.withColor(fgWhite) & "\n"
|
||||
|
||||
if issue.tags.len > 0:
|
||||
result &= "tags: ".withColor(fgMagenta) &
|
||||
issue.tags.join(",").withColor(fgGreen, true) & "\n"
|
||||
|
||||
if issue.properties.len > 0:
|
||||
result &= termColor(fgMagenta)
|
||||
for k, v in issue.properties: result &= k & ": " & v & "\n"
|
||||
|
||||
|
||||
result &= "--------".withColor(fgBlack, true) & "\n"
|
||||
if not issue.details.isEmptyOrWhitespace:
|
||||
result &= issue.details.strip.withColor(fgCyan) & "\n"
|
||||
|
||||
result &= termReset
|
||||
|
||||
proc formatPlainIssueSummary*(issue: Issue): string =
|
||||
|
||||
result = "$#: $# $#" % [
|
||||
$issue.state,
|
||||
($issue.id)[0..<6],
|
||||
issue.summary ]
|
||||
|
||||
if issue.hasProp("delegated-to") or issue.hasProp("pending"):
|
||||
var parts = newSeq[string]()
|
||||
|
||||
if issue.hasProp("delegated-to"):
|
||||
parts.add("delegated to " & issue["delegated-to"])
|
||||
|
||||
if issue.hasProp("pending"):
|
||||
parts.add("pendin: " & issue["pending"])
|
||||
|
||||
result &= "($#)" % [ parts.join("; ") ]
|
||||
|
||||
proc formatSectionIssue*(
|
||||
issue: Issue,
|
||||
width: int = 80,
|
||||
indent = "",
|
||||
verbose = false): string =
|
||||
|
||||
result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true) & " "
|
||||
|
||||
let showDetails = not issue.details.isEmptyOrWhitespace and verbose
|
||||
|
||||
let summaryIndentLen = indent.len + 7
|
||||
let summaryWidth = width - summaryIndentLen
|
||||
|
||||
let summaryLines = issue.summary
|
||||
.wrapWords(summaryWidth)
|
||||
.splitLines
|
||||
|
||||
result &= summaryLines[0].withColor(fgWhite)
|
||||
|
||||
for line in summaryLines[1..^1]:
|
||||
result &= "\p" & line.indent(summaryIndentLen)
|
||||
|
||||
var lastLineLen = summaryLines[^1].len
|
||||
|
||||
if issue.hasProp("delegated-to"):
|
||||
if lastLineLen + issue["delegated-to"].len + 1 < summaryWidth:
|
||||
result &= " " & issue["delegated-to"].withColor(fgMagenta)
|
||||
lastLineLen += issue["delegated-to"].len + 1
|
||||
else:
|
||||
result &= "\p" & issue["delegated-to"]
|
||||
.withColor(fgMagenta)
|
||||
.indent(summaryIndentLen)
|
||||
lastLineLen = issue["delegated-to"].len
|
||||
|
||||
if issue.tags.len > 0:
|
||||
let tagsStrLines = ("(" & issue.tags.join(", ") & ")")
|
||||
.wrapWords(summaryWidth)
|
||||
.splitLines
|
||||
|
||||
if tagsStrLines.len == 1 and
|
||||
(lastLineLen + tagsStrLines[0].len + 1) < summaryWidth:
|
||||
result &= " " & tagsStrLines[0].withColor(fgGreen)
|
||||
lastLineLen += tagsStrLines[0].len + 1
|
||||
else:
|
||||
result &= "\p" & tagsStrLines
|
||||
.mapIt(it.indent(summaryIndentLen))
|
||||
.join("\p")
|
||||
.withColor(fgGreen)
|
||||
lastLineLen = tagsStrLines[^1].len
|
||||
|
||||
if issue.hasProp("pending"):
|
||||
result &= "\p" & ("Pending: " & issue["pending"])
|
||||
.wrapwords(summaryWidth)
|
||||
.withColor(fgCyan)
|
||||
.indent(summaryIndentLen)
|
||||
|
||||
if showDetails:
|
||||
result &= "\p" & issue.details
|
||||
.strip
|
||||
.withColor(fgBlack, bright = true)
|
||||
.indent(summaryIndentLen)
|
||||
|
||||
result &= termReset
|
||||
|
||||
proc formatSectionIssueList*(
|
||||
issues: seq[Issue],
|
||||
width: int = 80,
|
||||
indent: string = "",
|
||||
verbose: bool = false): string =
|
||||
|
||||
result = ""
|
||||
for i in issues:
|
||||
var issueText = formatSectionIssue(i, width, indent, verbose)
|
||||
result &= issueText & "\n"
|
||||
|
||||
proc formatSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
|
||||
indent = "", verbose = false): string =
|
||||
let innerWidth = ctx.termWidth - (indent.len * 2)
|
||||
|
||||
result = termColor(fgBlue) &
|
||||
(indent & ".".repeat(innerWidth)) & "\n" &
|
||||
state.displayName.center(ctx.termWidth) & "\n\n" &
|
||||
termReset
|
||||
|
||||
let issuesByContext = issues.groupBy("context")
|
||||
|
||||
if issues.len > 5 and issuesByContext.len > 1:
|
||||
for context, ctxIssues in issuesByContext:
|
||||
|
||||
result &= termColor(fgYellow) &
|
||||
indent & ctx.getIssueContextDisplayName(context) & ":" &
|
||||
termReset & "\n\n"
|
||||
|
||||
result &= formatSectionIssueList(ctxIssues, innerWidth - 2, indent & " ", verbose)
|
||||
result &= "\n"
|
||||
|
||||
else: result &= formatSectionIssueList(issues, innerWidth, indent, verbose)
|
||||
|
||||
proc loadIssues(ctx: CliContext, state: IssueState) =
|
||||
ctx.issues[state] = loadIssues(ctx.cfg.tasksDir, state)
|
||||
|
||||
proc loadOpenIssues(ctx: CliContext) =
|
||||
ctx.issues = newTable[IssueState, seq[Issue]]()
|
||||
for state in [Current, TodoToday, Todo, Pending, Todo]: ctx.loadIssues(state)
|
||||
|
||||
proc loadAllIssues(ctx: CliContext) =
|
||||
ctx.issues = ctx.cfg.tasksDir.loadAllIssues()
|
||||
|
||||
proc filterIssues(ctx: CliContext, filter: IssueFilter) =
|
||||
for state, issueList in ctx.issues:
|
||||
ctx.issues[state] = issueList.filter(filter)
|
||||
|
||||
proc parsePropertiesOption(propsOpt: string): TableRef[string, string] =
|
||||
result = newTable[string, string]()
|
||||
for propText in propsOpt.split(";"):
|
||||
@@ -216,16 +31,6 @@ proc parseExclPropertiesOption(propsOpt: string): TableRef[string, seq[string]]
|
||||
if result.hasKey(pair[0]): result[pair[0]].add(val)
|
||||
else: result[pair[0]] = @[val]
|
||||
|
||||
proc sameDay(a, b: DateTime): bool =
|
||||
result = a.year == b.year and a.yearday == b.yearday
|
||||
|
||||
proc writeHeader(ctx: CliContext, header: string) =
|
||||
stdout.setForegroundColor(fgRed, true)
|
||||
stdout.writeLine('_'.repeat(ctx.termWidth))
|
||||
stdout.writeLine(header.center(ctx.termWidth))
|
||||
stdout.writeLine('~'.repeat(ctx.termWidth))
|
||||
stdout.resetAttributes
|
||||
|
||||
proc reorder(ctx: CliContext, state: IssueState) =
|
||||
|
||||
# load the issues to make sure the order file contains all issues in the state.
|
||||
@@ -248,96 +53,6 @@ proc edit(issue: Issue) =
|
||||
getCurrentExceptionMsg()
|
||||
issue.store()
|
||||
|
||||
proc list(
|
||||
ctx: CliContext,
|
||||
filter: Option[IssueFilter],
|
||||
states: Option[seq[IssueState]],
|
||||
showToday = false,
|
||||
showFuture = false,
|
||||
showHidden = false,
|
||||
verbose: bool) =
|
||||
|
||||
if states.isSome:
|
||||
trace "listing issues for " & $states.get
|
||||
for state in states.get:
|
||||
ctx.loadIssues(state)
|
||||
if filter.isSome: ctx.filterIssues(filter.get)
|
||||
|
||||
# Show Done for just today if requested
|
||||
if state == Done and showToday:
|
||||
ctx.issues[Done] = ctx.issues[Done].filterIt(
|
||||
it.hasProp("completed") and
|
||||
sameDay(getTime().local, it.getDateTime("completed")))
|
||||
|
||||
if isatty(stdout):
|
||||
stdout.write ctx.formatSection(ctx.issues[state], state, "", verbose)
|
||||
|
||||
else:
|
||||
stdout.writeLine ctx.issues[state]
|
||||
.mapIt(formatPlainIssueSummary(it))
|
||||
.join("\n")
|
||||
|
||||
|
||||
trace "listing complete"
|
||||
return
|
||||
|
||||
ctx.loadOpenIssues()
|
||||
if filter.isSome:
|
||||
ctx.filterIssues(filter.get)
|
||||
trace "filtered issues"
|
||||
|
||||
let today = showToday and [Current, TodoToday, Pending].anyIt(
|
||||
ctx.issues.hasKey(it) and ctx.issues[it].len > 0)
|
||||
|
||||
let future = showFuture and [Pending, Todo].anyIt(
|
||||
ctx.issues.hasKey(it) and ctx.issues[it].len > 0)
|
||||
|
||||
let indent = if today and future: " " else: ""
|
||||
|
||||
# Today's items
|
||||
if today:
|
||||
if future: ctx.writeHeader("Today")
|
||||
|
||||
for s in [Current, TodoToday, Pending]:
|
||||
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
|
||||
let visibleIssues = ctx.issues[s].filterIt(
|
||||
showHidden or
|
||||
not (it.hasProp("hide-until") and
|
||||
it.getDateTime("hide-until") > getTime().local))
|
||||
|
||||
if isatty(stdout):
|
||||
stdout.write ctx.formatSection(visibleIssues, s, indent, verbose)
|
||||
|
||||
else:
|
||||
stdout.writeLine visibleIssues
|
||||
.mapIt(formatPlainIssueSummary(it))
|
||||
.join("\n")
|
||||
|
||||
# Future items
|
||||
if future:
|
||||
if today: ctx.writeHeader("Future")
|
||||
|
||||
let futureCategories =
|
||||
if showToday: @[Todo]
|
||||
else: @[Pending, Todo]
|
||||
|
||||
for s in futureCategories:
|
||||
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
|
||||
let visibleIssues = ctx.issues[s].filterIt(
|
||||
showHidden or
|
||||
not (it.hasProp("hide-until") and
|
||||
it.getDateTime("hide-until") > getTime().local))
|
||||
|
||||
if isatty(stdout):
|
||||
stdout.write ctx.formatSection(visibleIssues, s, indent, verbose)
|
||||
|
||||
else:
|
||||
stdout.writeLine visibleIssues
|
||||
.mapIt(formatPlainIssueSummary(it))
|
||||
.join("\n")
|
||||
|
||||
trace "listing complete"
|
||||
|
||||
when isMainModule:
|
||||
try:
|
||||
|
||||
@@ -698,6 +413,15 @@ when isMainModule:
|
||||
if issuePaths.len < 2: continue
|
||||
stdout.writeLine(issueId & ":\p " & issuePaths.join("\p ") & "\p\p")
|
||||
|
||||
elif args["sync"]:
|
||||
if ctx.cfg.syncTargets.len == 0:
|
||||
info "No sync targets configured"
|
||||
|
||||
for syncTarget in ctx.cfg.syncTargets:
|
||||
let syncCtx = initSyncContext(ctx.cfg, syncTarget)
|
||||
|
||||
sync(syncCtx, args["--dry-run"])
|
||||
|
||||
except CatchableError:
|
||||
fatal getCurrentExceptionMsg()
|
||||
debug getCurrentException().getStackTrace()
|
||||
|
||||
Reference in New Issue
Block a user