Compare commits

..

3 Commits

7 changed files with 87 additions and 76 deletions

View File

@@ -1,2 +1,2 @@
[tools] [tools]
nim = "2.2.6" nim = "2.2.8"

View File

@@ -1,6 +1,6 @@
# Package # Package
version = "4.32.0" version = "4.33.1"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Personal issue tracker." description = "Personal issue tracker."
license = "MIT" license = "MIT"
@@ -19,7 +19,7 @@ requires @[
# Dependencies from git.jdb-software.com/jdb/nim-packages # Dependencies from git.jdb-software.com/jdb/nim-packages
requires @[ requires @[
"cliutils >= 0.10.2", "cliutils >= 0.11.1",
"langutils >= 0.4.0", "langutils >= 0.4.0",
"timeutils >= 0.5.4", "timeutils >= 0.5.4",
"data_uri > 1.0.0", "data_uri > 1.0.0",
@@ -27,4 +27,4 @@ requires @[
] ]
task updateVersion, "Update the version of this package.": task updateVersion, "Update the version of this package.":
exec "update_nim_package_version pit 'src/pit/cliconstants.nim'" exec "update_nim_package_version pit 'src/pit/cliconstants.nim'"

View File

@@ -92,8 +92,8 @@ proc addIssue(
"Do you want to set a value for '" & propName & "'? " & "Do you want to set a value for '" & propName & "'? " &
"You can use the numbers above to use an existing value, enter " & "You can use the numbers above to use an existing value, enter " &
"something new, or leave blank to indicate no value.\p" & "something new, or leave blank to indicate no value.\p" &
withColor(propName, fgMagenta) & ":" & color(propName, fg=cMagenta) & ":" &
withColor(" ", fgBlue, bright=true, skipReset=true)) ansiEscSeq(fg=cBrightBlue) & " ")
let resp = stdin.readLine.strip let resp = stdin.readLine.strip
let numberResp = resp.match(numberRegex) let numberResp = resp.match(numberRegex)
@@ -106,7 +106,7 @@ proc addIssue(
elif resp.len > 0: elif resp.len > 0:
issueProps[propName] = resp issueProps[propName] = resp
stdout.writeLine(termReset) stdout.writeLine(RESET_FORMATTING)
result = Issue( result = Issue(
id: genUUID(), id: genUUID(),
@@ -253,11 +253,6 @@ when isMainModule:
elif args["edit"]: elif args["edit"]:
for editRef in @(args["<ref>"]): for editRef in @(args["<ref>"]):
let propsOption =
if args["--properties"]:
some(parsePropertiesOption($args["--properties"]))
else: none(TableRef[string, string])
var stateOption = none(IssueState) var stateOption = none(IssueState)
try: stateOption = some(parseEnum[IssueState](editRef)) try: stateOption = some(parseEnum[IssueState](editRef))
@@ -267,10 +262,16 @@ when isMainModule:
let state = stateOption.get let state = stateOption.get
ctx.loadIssues(state) ctx.loadIssues(state)
for issue in ctx.issues[state]: for issue in ctx.issues[state]:
if propsOption.isSome: if propertiesOption.isSome:
for k,v in propsOption.get: for k,v in propertiesOption.get:
issue[k] = v issue[k] = v
edit(issue) if tagsOption.isSome:
issue.tags = deduplicate(issue.tags & tagsOption.get)
if exclTagsOption.isSome:
issue.tags = issue.tags.filter(
proc (tag: string): bool = not exclTagsOption.get.anyIt(it == tag))
if args["--non-interactive"]: issue.store()
else: edit(issue)
updatedIssues.add(issue) updatedIssues.add(issue)
else: else:
@@ -278,7 +279,13 @@ when isMainModule:
if propertiesOption.isSome: if propertiesOption.isSome:
for k,v in propertiesOption.get: for k,v in propertiesOption.get:
issue[k] = v issue[k] = v
edit(issue) if tagsOption.isSome:
issue.tags = deduplicate(issue.tags & tagsOption.get)
if exclTagsOption.isSome:
issue.tags = issue.tags.filter(
proc (tag: string): bool = not exclTagsOption.get.anyIt(it == tag))
if args["--non-interactive"]: issue.store()
else: edit(issue)
updatedIssues.add(issue) updatedIssues.add(issue)
elif args["tag"]: elif args["tag"]:

View File

@@ -1,4 +1,4 @@
const PIT_VERSION* = "4.32.0" const PIT_VERSION* = "4.33.1"
const USAGE* = """Usage: const USAGE* = """Usage:
pit ( new | add) <summary> [<state>] [options] pit ( new | add) <summary> [<state>] [options]
@@ -248,4 +248,4 @@ Issue Properties:
If present, expected to be a comma-delimited list of text tags. The -g If present, expected to be a comma-delimited list of text tags. The -g
option is a short-hand for '-p tags:<tags-value>'. option is a short-hand for '-p tags:<tags-value>'.
""" """

View File

@@ -6,36 +6,36 @@ import ./libpit
proc adjustedTerminalWidth(): int = min(terminalWidth(), 80) proc adjustedTerminalWidth(): int = min(terminalWidth(), 80)
proc formatIssue*(issue: Issue): string = proc formatIssue*(issue: Issue): string =
result = ($issue.id).withColor(fgBlack, true) & "\n"& result = ($issue.id).color(cBrightBlack) & "\n"&
issue.summary.withColor(fgWhite) & "\n" issue.summary.color(cWhite) & "\n"
if issue.tags.len > 0: if issue.tags.len > 0:
result &= "tags: ".withColor(fgMagenta) & result &= "tags: ".color(cMagenta) &
issue.tags.join(",").withColor(fgGreen, true) & "\n" issue.tags.join(",").color(cBrightGreen) & "\n"
if issue.properties.len > 0: if issue.properties.len > 0:
for k, v in issue.properties: for k, v in issue.properties:
if k == "project": if k == "project":
result &= "project: ".withColor(fgMagenta) & result &= "project: ".color(cMagenta) &
v.withColor(fgBlue, bright = true) & "\n" v.color(cBrightBlue) & "\n"
elif k == "milestone": elif k == "milestone":
result &= "milestone: ".withColor(fgMagenta) & result &= "milestone: ".color(cMagenta) &
v.withColor(fgBlue, bright = true) & "\n" v.color(cBrightBlue) & "\n"
elif k == "priority": elif k == "priority":
result &= "priority: ".withColor(fgMagenta) & result &= "priority: ".color(cMagenta) &
v.withColor(fgRed, bright = true) & "\n" v.color(cBrightRed) & "\n"
else: else:
result &= termColor(fgMagenta) & k & ": " & v & "\n" result &= ansiEscSeq(fg = cMagenta) & k & ": " & v & "\n"
result &= "--------".withColor(fgBlack, true) & "\n" result &= "--------".color(cBrightBlack) & "\n"
if not issue.details.isEmptyOrWhitespace: if not issue.details.isEmptyOrWhitespace:
result &= issue.details.strip.withColor(fgCyan) & "\n" result &= issue.details.strip.color(cCyan) & "\n"
result &= termReset result &= RESET_FORMATTING
proc formatPlainIssueSummary*(issue: Issue): string = proc formatPlainIssueSummary*(issue: Issue): string =
@@ -64,7 +64,7 @@ proc formatSectionIssue*(
verbose = false, verbose = false,
bold = false): string = bold = false): string =
result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true) & " " result = (indent & ($issue.id)[0..<6]).color(cBrightBlack) & " "
let showDetails = not issue.details.isEmptyOrWhitespace and verbose let showDetails = not issue.details.isEmptyOrWhitespace and verbose
@@ -75,7 +75,8 @@ proc formatSectionIssue*(
.wrapWords(summaryWidth) .wrapWords(summaryWidth)
.splitLines .splitLines
result &= summaryLines[0].termFmt(fgWhite, bold=bold, underline=bold) result &= summaryLines[0].termFmt(fg=cWhite, bg=cDefault,
style = if bold: { tsBold, tsUnderline } else: {})
for line in summaryLines[1..^1]: for line in summaryLines[1..^1]:
result &= "\p" & line.indent(summaryIndentLen) result &= "\p" & line.indent(summaryIndentLen)
@@ -84,11 +85,11 @@ proc formatSectionIssue*(
if issue.hasProp("delegated-to"): if issue.hasProp("delegated-to"):
if lastLineLen + issue["delegated-to"].len + 1 < summaryWidth: if lastLineLen + issue["delegated-to"].len + 1 < summaryWidth:
result &= " " & issue["delegated-to"].withColor(fgMagenta) result &= " " & issue["delegated-to"].color(cMagenta)
lastLineLen += issue["delegated-to"].len + 1 lastLineLen += issue["delegated-to"].len + 1
else: else:
result &= "\p" & issue["delegated-to"] result &= "\p" & issue["delegated-to"]
.withColor(fgMagenta) .color(cMagenta)
.indent(summaryIndentLen) .indent(summaryIndentLen)
lastLineLen = issue["delegated-to"].len lastLineLen = issue["delegated-to"].len
@@ -99,28 +100,28 @@ proc formatSectionIssue*(
if tagsStrLines.len == 1 and if tagsStrLines.len == 1 and
(lastLineLen + tagsStrLines[0].len + 1) < summaryWidth: (lastLineLen + tagsStrLines[0].len + 1) < summaryWidth:
result &= " " & tagsStrLines[0].withColor(fgGreen) result &= " " & tagsStrLines[0].color(cGreen)
lastLineLen += tagsStrLines[0].len + 1 lastLineLen += tagsStrLines[0].len + 1
else: else:
result &= "\p" & tagsStrLines result &= "\p" & tagsStrLines
.mapIt(it.indent(summaryIndentLen)) .mapIt(it.indent(summaryIndentLen))
.join("\p") .join("\p")
.withColor(fgGreen) .color(cGreen)
lastLineLen = tagsStrLines[^1].len lastLineLen = tagsStrLines[^1].len
if issue.hasProp("pending"): if issue.hasProp("pending"):
result &= "\p" & ("Pending: " & issue["pending"]) result &= "\p" & ("Pending: " & issue["pending"])
.wrapwords(summaryWidth) .wrapwords(summaryWidth)
.withColor(fgCyan) .color(cCyan)
.indent(summaryIndentLen) .indent(summaryIndentLen)
if showDetails: if showDetails:
result &= "\p" & issue.details result &= "\p" & issue.details
.strip .strip
.withColor(fgBlack, bright = true) .color(cBrightBlack)
.indent(summaryIndentLen) .indent(summaryIndentLen)
result &= termReset result &= RESET_FORMATTING
proc formatSectionIssueList*( proc formatSectionIssueList*(
@@ -139,19 +140,19 @@ proc formatSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
indent = "", verbose = false): string = indent = "", verbose = false): string =
let innerWidth = adjustedTerminalWidth() - (indent.len * 2) let innerWidth = adjustedTerminalWidth() - (indent.len * 2)
result = termColor(fgBlue) & result = ansiEscSeq(fg = cBlue) &
(indent & ".".repeat(innerWidth)) & "\n" & (indent & ".".repeat(innerWidth)) & "\n" &
state.displayName.center(adjustedTerminalWidth()) & "\n\n" & state.displayName.center(adjustedTerminalWidth()) & "\n\n" &
termReset RESET_FORMATTING
let issuesByContext = issues.groupBy("context") let issuesByContext = issues.groupBy("context")
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:
result &= termColor(fgYellow) & result &= ansiEscSeq(fg = cYellow) &
indent & ctx.getIssueContextDisplayName(context) & ":" & indent & ctx.getIssueContextDisplayName(context) & ":" &
termReset & "\n\n" RESET_FORMATTING & "\n\n"
result &= formatSectionIssueList(ctxIssues, innerWidth - 2, indent & " ", verbose) result &= formatSectionIssueList(ctxIssues, innerWidth - 2, indent & " ", verbose)
result &= "\n" result &= "\n"
@@ -160,11 +161,11 @@ proc formatSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
proc writeHeader*(ctx: CliContext, header: string) = proc writeHeader*(ctx: CliContext, header: string) =
stdout.setForegroundColor(fgRed, true) stdout.write(ansiEscSeq(fg = cBrightRed))
stdout.writeLine('_'.repeat(adjustedTerminalWidth())) stdout.writeLine('_'.repeat(adjustedTerminalWidth()))
stdout.writeLine(header.center(adjustedTerminalWidth())) stdout.writeLine(header.center(adjustedTerminalWidth()))
stdout.writeLine('~'.repeat(adjustedTerminalWidth())) stdout.writeLine('~'.repeat(adjustedTerminalWidth()))
stdout.resetAttributes stdout.write(RESET_FORMATTING)
proc list*( proc list*(

View File

@@ -230,7 +230,9 @@ proc fromStorageFormat*(id: string, issueTxt: string): Issue =
var detailLines: seq[string] = @[] var detailLines: seq[string] = @[]
for line in issueTxt.splitLines(): for line in issueTxt.splitLines():
if line.startsWith("#"): continue # ignore lines starting with '#' if line.startsWith("#") and parseState != ReadingDetails:
# ignore lines starting with '#', unless we're in the details section.
continue
case parseState case parseState

View File

@@ -152,9 +152,9 @@ proc listProjects*(ctx: CliContext, filter = none[IssueFilter]()) =
for (context, projects) in pairs(projectsByContext): for (context, projects) in pairs(projectsByContext):
linesToPrint.add(withColor( linesToPrint.add(termFmt(
ctx.getIssueContextDisplayName(context) & ":", ctx.getIssueContextDisplayName(context) & ":",
fgYellow) & termReset) fg=cYellow))
linesToPrint.add("") linesToPrint.add("")
var toList = toHashSet(toSeq(keys(projects))) var toList = toHashSet(toSeq(keys(projects)))
@@ -203,10 +203,10 @@ proc listMilestones*(ctx: CliContext, filter = none[IssueFilter]()) =
if values(project.milestones) --> all(it.len == 0): if values(project.milestones) --> all(it.len == 0):
continue continue
linesToPrint.add(withColor(project.name, fgBlue, bold = true, bright=true)) linesToPrint.add(termFmt(project.name, cBrightBlue, style = { tsBold }))
linesToPrint.add(withColor( linesToPrint.add(termFmt(
"".repeat(runeLen(stripAnsi(project.name))), "".repeat(runeLen(stripAnsi(project.name))),
fgBlue, bold = true)) cBrightBlue, style = { tsBold} ))
for milestone in project.milestoneOrder: for milestone in project.milestoneOrder:
if project.milestones.hasKey(milestone) and if project.milestones.hasKey(milestone) and
@@ -229,31 +229,31 @@ proc formatProjectIssue(
var firstLine = "" var firstLine = ""
if issue.state == IssueState.Done: if issue.state == IssueState.Done:
firstLine &= withColor("", fgBlack, bold=true, bright=true) firstLine &= termFmt("", fg = cBrightBlack, style = { tsBold })
else: else:
case issue.getPriority case issue.getPriority
of IssuePriority.essential: of IssuePriority.essential:
firstLine &= withColor("", fgRed, bold=true, bright=true) firstLine &= termFmt("", fg=cBrightRed, style={ tsBold })
of IssuePriority.vital: of IssuePriority.vital:
firstLine &= withColor("", fgYellow, bold=true, bright=true) firstLine &= termFmt("", fg=cBrightYellow, style={ tsBold })
of IssuePriority.important: of IssuePriority.important:
firstLine &= withColor("", fgBlue, bold=true, bright=true) firstLine &= termFmt("", fg=cBrightBlue, style={ tsBold })
of IssuePriority.optional: of IssuePriority.optional:
firstLine &= withColor("", fgBlack, bold=false, bright=true) firstLine &= termFmt("", fg=cBrightBlack)
let summaryText = formatSectionIssue(issue, width - 3, let summaryText = formatSectionIssue(issue, width - 3,
bold = [Current, TodoToday].contains(issue.state)).splitLines bold = [Current, TodoToday].contains(issue.state)).splitLines
firstLine &= summaryText[0] firstLine &= summaryText[0]
if issue.state == IssueState.Done: if issue.state == IssueState.Done:
firstLine = withColor(stripAnsi(firstLine), fgBlack, bright=true) firstLine = termFmt(stripAnsi(firstLine), fg=cBrightBlack)
result.add(firstLine) result.add(firstLine)
result.add(summaryText[1 .. ^1] --> map(" " & it)) result.add(summaryText[1 .. ^1] --> map(" " & it))
if issue.state == IssueState.Done: if issue.state == IssueState.Done:
let origLines = result let origLines = result
result = origLines --> map(withColor(stripAnsi(it), fgBlack, bright=true)) result = origLines --> map(termFmt(stripAnsi(it), fg = cBrightBlack))
proc formatParentIssue*( proc formatParentIssue*(
ctx: CliContext, ctx: CliContext,
@@ -265,7 +265,7 @@ proc formatParentIssue*(
for child in sorted(children, cmp): for child in sorted(children, cmp):
let childLines = ctx.formatProjectIssue(child, width - 3) let childLines = ctx.formatProjectIssue(child, width - 3)
result.add(childLines --> map(withColor("", fgBlack, bright=true) & it)) result.add(childLines --> map(termFmt("", fg=cBrightBlack) & it))
result.add("") result.add("")
@@ -277,8 +277,8 @@ proc formatMilestone*(
availWidth: int): seq[string] = availWidth: int): seq[string] =
result = @[""] result = @[""]
result.add(withColor(milestone, fgWhite, bold=true)) result.add(termFmt(milestone, fg=cWhite, style={tsBold}))
result.add(withColor("".repeat(availWidth), fgWhite)) result.add(termFmt("".repeat(availWidth), fg=cWhite))
var parentsToChildren = issues --> var parentsToChildren = issues -->
filter(it.hasProp("parent")).group(it["parent"]) filter(it.hasProp("parent")).group(it["parent"])
@@ -327,18 +327,19 @@ proc showProject*(ctx: CliContext, project: Project) =
let numColumns = (fullWidth - 4) div (columnWidth + 2) let numColumns = (fullWidth - 4) div (columnWidth + 2)
linesToPrint.add("") linesToPrint.add("")
linesToPrint.add(withColor( linesToPrint.add(termFmt(
"" & "".repeat(project.name.runeLen + 2) & "" & "".repeat(project.name.runeLen + 2) &
"" & "".repeat(fullWidth - project.name.runeLen - 4) & "", "" & "".repeat(fullWidth - project.name.runeLen - 4) & "",
fgBlue, bold=true)) fg=cBlue, style={tsBold}))
linesToPrint.add( linesToPrint.add(
withColor("", fgBlue, bold=true) & termFmt("", fg=cBlue, style={tsBold}) &
withColor(project.name, fgBlue, bold=true, bright=true) & termFmt(project.name, fg=cBrightBlue, style={tsBold}) &
withColor("" & " ".repeat(fullWidth - project.name.runeLen - 4) & "", fgBlue, bold=true)) termFmt("" & " ".repeat(fullWidth - project.name.runeLen - 4) & "",
linesToPrint.add(withColor( fg=cBlue, style={tsBold}))
linesToPrint.add(termFmt(
"" & "".repeat(project.name.runeLen + 2) & "" & "".repeat(project.name.runeLen + 2) &
"" & " ".repeat(fullWidth - project.name.runeLen - 4) & "", "" & " ".repeat(fullWidth - project.name.runeLen - 4) & "",
fgBlue, bold=true)) fg=cBlue, style={tsBold}))
let milestoneTexts: seq[seq[string]] = project.milestoneOrder --> let milestoneTexts: seq[seq[string]] = project.milestoneOrder -->
filter(project.milestones.hasKey(it) and project.milestones[it].len > 0). filter(project.milestones.hasKey(it) and project.milestones[it].len > 0).
@@ -355,14 +356,14 @@ proc showProject*(ctx: CliContext, project: Project) =
for line in joinedLines: for line in joinedLines:
let padLen = fullWidth - runeLen(stripAnsi(line)) - 3 let padLen = fullWidth - runeLen(stripAnsi(line)) - 3
linesToPrint.add( linesToPrint.add(
withColor("", fgBlue) & termFmt("", fg=cBlue) &
line & line &
" ".repeat(padLen) & " ".repeat(padLen) &
withColor("", fgBlue)) termFmt("", fg=cBlue))
linesToPrint.add(withColor( linesToPrint.add(termFmt(
"" & "".repeat(terminalWidth() - 2) & "", "" & "".repeat(terminalWidth() - 2) & "",
fgBlue, bold=true)) fg=cBlue, style={tsBold}))
if isatty(stdout): if isatty(stdout):
stdout.writeLine(linesToPrint.join("\p")) stdout.writeLine(linesToPrint.join("\p"))
@@ -389,9 +390,9 @@ proc showProjectBoard*(ctx: CliContext, filter = none[IssueFilter]()) =
for (context, projects) in contextsAndProjects: for (context, projects) in contextsAndProjects:
if contextsAndProjects.len > 1: if contextsAndProjects.len > 1:
stdout.writeLine("") stdout.writeLine("")
stdout.writeLine(withColor( stdout.writeLine(termFmt(
ctx.getIssueContextDisplayName(context) & ":", ctx.getIssueContextDisplayName(context) & ":",
fgYellow, bold=true)) fg=cYellow, style={tsBold}))
stdout.writeLine("") stdout.writeLine("")
for p in projects: for p in projects: