Compare commits

..

3 Commits

11 changed files with 252 additions and 112 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
[tools] [tools]
nim = "2.2.6" nim = "2.2.8"
+11 -14
View File
@@ -1,6 +1,6 @@
# Package # Package
version = "4.33.0" version = "4.33.3"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Personal issue tracker." description = "Personal issue tracker."
license = "MIT" license = "MIT"
@@ -10,21 +10,18 @@ bin = @["pit"]
# Dependencies # Dependencies
requires @[ requires "nim >= 1.6.0"
"nim >= 1.4.0", requires "docopt >= 0.7.1"
"docopt >= 0.7.1", requires "regex >= 0.26.3"
"uuids >= 0.1.10", requires "uuids >= 0.1.10"
"zero_functional" requires "zero_functional"
]
# Dependencies from git.jdb-software.com/jdb/nim-packages # Dependencies from git.jdb-software.com/jdb/nim-packages
requires @[ requires "cliutils >= 0.11.1"
"cliutils >= 0.10.2", requires "langutils >= 0.4.0"
"langutils >= 0.4.0", requires "timeutils >= 0.5.4"
"timeutils >= 0.5.4", requires "filetype >= 0.9.0"
"data_uri > 1.0.0", requires "update_nim_package_version >= 0.2.0"
"update_nim_package_version >= 0.2.0"
]
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'"
+13 -12
View File
@@ -1,13 +1,14 @@
## Personal Issue Tracker CLI interface ## Personal Issue Tracker CLI interface
## ==================================== ## ====================================
import std/[algorithm, logging, options, os, sequtils, sets, tables, terminal, import std/[algorithm, logging, options, os, sequtils, sets, tables, times,
times, unicode] unicode]
import cliutils, data_uri, docopt, json, timeutils, uuids, zero_functional import docopt, json, timeutils, uuids, zero_functional
from nre import match, re from regex import match, re2
import strutils except alignLeft, capitalize, strip, toUpper, toLower import strutils except alignLeft, capitalize, strip, toUpper, toLower
import pit/[cliconstants, formatting, libpit, projects, sync_pbm_vsb] import pit/[ansiterm, cliconstants, data_uri, formatting, libpit, projects,
sync_pbm_vsb]
export formatting, libpit export formatting, libpit
@@ -73,7 +74,7 @@ proc addIssue(
if issueProps.hasKey("context"): if issueProps.hasKey("context"):
ctx.filterIssues(propsFilter(newTable({"context": issueProps["context"]}))) ctx.filterIssues(propsFilter(newTable({"context": issueProps["context"]})))
let numberRegex = re("^[0-9]+$") let numberRegex = re2("^[0-9]+$")
for propName in defaultProps: for propName in defaultProps:
if not issueProps.hasKey(propName): if not issueProps.hasKey(propName):
let allIssues: seq[seq[Issue]] = toSeq(values(ctx.issues)) let allIssues: seq[seq[Issue]] = toSeq(values(ctx.issues))
@@ -92,13 +93,13 @@ 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)
if numberResp.isSome: if numberResp:
let idx = parseInt(resp) let idx = parseInt(resp)
if idx >= 0 and idx < previousValues.len: if idx >= 0 and idx < previousValues.len:
issueProps[propName] = previousValues[idx] issueProps[propName] = previousValues[idx]
@@ -106,7 +107,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(),
@@ -211,10 +212,10 @@ when isMainModule:
# If they supplied text matches, add that to the filter. # If they supplied text matches, add that to the filter.
if args["--match"]: if args["--match"]:
filter.summaryMatch = some(re("(?i)" & $args["--match"])) filter.summaryMatch = some(re2("(?i)" & $args["--match"]))
if args["--match-all"]: if args["--match-all"]:
filter.fullMatch = some(re("(?i)" & $args["--match-all"])) filter.fullMatch = some(re2("(?i)" & $args["--match-all"]))
# If no "context" property is given, use the default (if we have one) # If no "context" property is given, use the default (if we have one)
if ctx.defaultContext.isSome and not filter.properties.hasKey("context"): if ctx.defaultContext.isSome and not filter.properties.hasKey("context"):
+75
View File
@@ -0,0 +1,75 @@
import std/[sequtils, strutils]
const CSI = "\x1b["
const RESET_FORMATTING* = "\x1b[0m"
type
TrueColor* = tuple[r, g, b: int]
TerminalColors* = enum
cBlack = 0, cRed, cGreen, cYellow, cBlue, cMagenta, cCyan, cWhite,
cDefault = 9
cBrightBlack = 60, cBrightRed, cBrightGreen, cBrightYellow, cBrightBlue,
cBrightMagenta, cBrightCyan, cBrightWhite
TerminalStyle* = enum
tsBold = 1, tsDim, tsItalic, tsUnderline, tsBlink, tsInverse, tsHidden,
tsStrikethrough
func isAnsiTerminator(c: char): bool =
c in {'A'..'Z'} or c in {'a'..'z'}
proc stripAnsi*(text: string): string =
var i = 0
while i < text.len:
if i + 1 < text.len and text[i] == '\x1b' and text[i + 1] == '[':
i += 2
while i < text.len and not text[i].isAnsiTerminator:
inc i
if i < text.len:
inc i
else:
result.add(text[i])
inc i
func ansiEscSeq*(style: set[TerminalStyle]): string =
CSI & toSeq(style).mapIt($int(it)).join(";") & "m"
func ansiEscSeq*(fg: int): string = CSI & "38;5;" & $fg & "m"
func ansiEscSeq*(bg: int): string = CSI & "48;5;" & $bg & "m"
func ansiEscSeq*(fg: TrueColor): string =
"$#38;2;$#;$#;$#m" % [CSI, $fg.r, $fg.g, $fg.b]
func ansiEscSeq*(bg: TrueColor): string =
"$#48;2;$#;$#;$#m" % [CSI, $bg.r, $bg.g, $bg.b]
func ansiEscSeq*(fg: TerminalColors): string = CSI & $(int(fg) + 30) & "m"
func ansiEscSeq*(bg: TerminalColors): string = CSI & $(int(bg) + 40) & "m"
func ansiEscSeq*(
fg: TerminalColors,
bg: TerminalColors,
style: set[TerminalStyle]): string =
result = CSI
if style != {}:
result &= toSeq(style).mapIt($int(it)).join(";") & ";"
result &= $(int(fg) + 30) & ";" & $(int(bg) + 40) & "m"
func termFmt*(
text: string,
fg = cDefault,
bg = cDefault,
style: set[TerminalStyle] = {}): string =
ansiEscSeq(fg, bg, style) & text & RESET_FORMATTING
func termFmt*[C: int | TrueColor](
text: string,
fg, bg: C,
style: set[TerminalStyle] = {}): string =
ansiEscSeq(style) & ansiEscSeq(fg = fg) & ansiEscSeq(bg = bg) &
text & RESET_FORMATTING
func color*(text: string, fg = cDefault, bg = cDefault): string =
termFmt(text, fg, bg, {})
+3 -3
View File
@@ -1,4 +1,4 @@
const PIT_VERSION* = "4.33.0" const PIT_VERSION* = "4.33.3"
const USAGE* = """Usage: const USAGE* = """Usage:
pit ( new | add) <summary> [<state>] [options] pit ( new | add) <summary> [<state>] [options]
@@ -71,10 +71,10 @@ Options:
properties set. properties set.
-m, --match <pattern> Limit to issues whose summaries match the given -m, --match <pattern> Limit to issues whose summaries match the given
pattern (PCRE regex supported). pattern (regex syntax supported).
-M, --match-all <pat> Limit to the issues whose summaries or details -M, --match-all <pat> Limit to the issues whose summaries or details
match the given pattern (PCRE regex supported). match the given pattern (regex syntax supported).
-v, --verbose Show issue details when listing issues. -v, --verbose Show issue details when listing issues.
+20
View File
@@ -0,0 +1,20 @@
import std/[base64, strutils]
import filetype
proc encodeAsDataUri*(value: string, mimeType: string = ""): string =
let actualMimeType =
if mimeType.isEmptyOrWhitespace: filetype.match(cast[seq[byte]](value)).mime.value
else: mimeType
"data:" & actualMimeType & ";base64," & encode(value)
proc decodeDataUri*(dataUri: string): string =
if not dataUri.startsWith("data:"):
raise newException(Exception, "input does not look like a valid data URI")
let commaIdx = dataUri.find(',')
if commaIdx < 0:
raise newException(Exception, "input does not look like a valid data URI")
decode(dataUri[commaIdx + 1..^1])
+32 -30
View File
@@ -1,41 +1,42 @@
import std/[options, sequtils, tables, terminal, times, unicode, wordwrap] import std/[options, sequtils, tables, terminal, times, unicode, wordwrap]
import cliutils, uuids import uuids
import std/strutils except alignLeft, capitalize, strip, toLower, toUpper import std/strutils except alignLeft, capitalize, strip, toLower, toUpper
import ./ansiterm
import ./libpit 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 +65,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 +76,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 +86,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 +101,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 +141,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 +162,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*(
+24 -19
View File
@@ -1,8 +1,8 @@
import std/[json, logging, options, os, strformat, strutils, tables, times, import std/[json, logging, options, os, strformat, strutils, tables, times,
unicode] unicode]
import cliutils, docopt, langutils, uuids, zero_functional import cliutils/config, docopt, langutils, uuids, zero_functional
import nre except toSeq import regex
import timeutils except `>` import timeutils except `>`
from sequtils import deduplicate, toSeq from sequtils import deduplicate, toSeq
@@ -25,7 +25,7 @@ type
IssueFilter* = ref object IssueFilter* = ref object
completedRange*: Option[tuple[b, e: DateTime]] completedRange*: Option[tuple[b, e: DateTime]]
fullMatch*, summaryMatch*: Option[Regex] fullMatch*, summaryMatch*: Option[Regex2]
inclStates*: seq[IssueState] inclStates*: seq[IssueState]
exclStates*: seq[IssueState] exclStates*: seq[IssueState]
hasTags*: seq[string] hasTags*: seq[string]
@@ -61,8 +61,8 @@ const MATCH_ANY* = "<match-any>"
const DONE_FOLDER_FORMAT* = "yyyy-MM" const DONE_FOLDER_FORMAT* = "yyyy-MM"
const ISO8601_MS = "yyyy-MM-dd'T'HH:mm:ss'.'fffzzz" const ISO8601_MS = "yyyy-MM-dd'T'HH:mm:ss'.'fffzzz"
let ISSUE_FILE_PATTERN = re"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}\.txt" let ISSUE_FILE_PATTERN = re2"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}\.txt"
let RECURRENCE_PATTERN = re"(every|after) ((\d+) )?((hour|day|week|month|year)s?)(, ([0-9a-fA-F]+))?" let RECURRENCE_PATTERN = re2"(every|after) ((\d+) )?((hour|day|week|month|year)s?)(, ([0-9a-fA-F]+))?"
let traceStartTime = cpuTime() let traceStartTime = cpuTime()
var lastTraceTime = traceStartTime var lastTraceTime = traceStartTime
@@ -116,26 +116,31 @@ proc setDateTime*(issue: Issue, key: string, dt: DateTime) =
proc getRecurrence*(issue: Issue): Option[Recurrence] = proc getRecurrence*(issue: Issue): Option[Recurrence] =
if not issue.hasProp("recurrence"): return none[Recurrence]() if not issue.hasProp("recurrence"): return none[Recurrence]()
let m = issue["recurrence"].match(RECURRENCE_PATTERN) let recurrence = issue["recurrence"]
var m = RegexMatch2()
if not m.isSome: if not recurrence.match(RECURRENCE_PATTERN, m):
warn "not a valid recurrence value: '" & issue["recurrence"] & "'" warn "not a valid recurrence value: '" & issue["recurrence"] & "'"
return none[Recurrence]() return none[Recurrence]()
let c = nre.toSeq(m.get.captures) proc captured(i: int): Option[string] =
let timeVal = if c[2].isSome: c[2].get.parseInt let bounds = m.group(i)
if bounds == reNonCapture: none(string)
else: some(recurrence[bounds])
let timeVal = if captured(2).isSome: captured(2).get.parseInt
else: 1 else: 1
return some(Recurrence( return some(Recurrence(
isFromCompletion: c[0].get == "after", isFromCompletion: captured(0).get == "after",
interval: interval:
case c[4].get: case captured(4).get:
of "hour": hours(timeVal) of "hour": hours(timeVal)
of "day": days(timeVal) of "day": days(timeVal)
of "week": weeks(timeVal) of "week": weeks(timeVal)
of "month": months(timeVal) of "month": months(timeVal)
of "year": years(timeVal) of "year": years(timeVal)
else: weeks(1), else: weeks(1),
cloneId: c[6])) cloneId: captured(6)))
proc setPriority*(issue: Issue, priority: IssuePriority) = proc setPriority*(issue: Issue, priority: IssuePriority) =
issue["priority"] = $priority issue["priority"] = $priority
@@ -149,8 +154,8 @@ proc getPriority*(issue: Issue): IssuePriority =
proc initFilter*(): IssueFilter = proc initFilter*(): IssueFilter =
result = IssueFilter( result = IssueFilter(
completedRange: none(tuple[b, e: DateTime]), completedRange: none(tuple[b, e: DateTime]),
fullMatch: none(Regex), fullMatch: none(Regex2),
summaryMatch: none(Regex), summaryMatch: none(Regex2),
inclStates: @[], inclStates: @[],
exclStates: @[], exclStates: @[],
exclHidden: true, exclHidden: true,
@@ -173,11 +178,11 @@ proc dateFilter*(range: tuple[b, e: DateTime]): IssueFilter =
proc summaryMatchFilter*(pattern: string): IssueFilter = proc summaryMatchFilter*(pattern: string): IssueFilter =
result = initFilter() result = initFilter()
result.summaryMatch = some(re("(?i)" & pattern)) result.summaryMatch = some(re2("(?i)" & pattern))
proc fullMatchFilter*(pattern: string): IssueFilter = proc fullMatchFilter*(pattern: string): IssueFilter =
result = initFilter() result = initFilter()
result.fullMatch = some(re("(?i)" & pattern)) result.fullMatch = some(re2("(?i)" & pattern))
proc hasTagsFilter*(tags: seq[string]): IssueFilter = proc hasTagsFilter*(tags: seq[string]): IssueFilter =
result = initFilter() result = initFilter()
@@ -344,7 +349,7 @@ proc loadIssues*(path: string): seq[Issue] =
var unorderedIssues: seq[TaggedIssue] = @[] var unorderedIssues: seq[TaggedIssue] = @[]
for path in walkDirRec(path): for path in walkDirRec(path):
if extractFilename(path).match(ISSUE_FILE_PATTERN).isSome(): if extractFilename(path).match(ISSUE_FILE_PATTERN):
unorderedIssues.add((loadIssue(path), false)) unorderedIssues.add((loadIssue(path), false))
trace "loaded " & $unorderedIssues.len & " issues", true trace "loaded " & $unorderedIssues.len & " issues", true
@@ -451,12 +456,12 @@ proc filter*(issues: seq[Issue], filter: IssueFilter): seq[Issue] =
if filter.summaryMatch.isSome: if filter.summaryMatch.isSome:
let p = filter.summaryMatch.get let p = filter.summaryMatch.get
f = f --> filter(it.summary.find(p).isSome) f = f --> filter(it.summary.contains(p))
if filter.fullMatch.isSome: if filter.fullMatch.isSome:
let p = filter.fullMatch.get let p = filter.fullMatch.get
f = f --> f = f -->
filter(it.summary.find(p).isSome or it.details.find(p).isSome) filter(it.summary.contains(p) or it.details.contains(p))
for tagLent in filter.hasTags: for tagLent in filter.hasTags:
let tag = tagLent let tag = tagLent
+31 -30
View File
@@ -1,8 +1,8 @@
import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables, import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables,
terminal, times, unicode] terminal, times, unicode]
from std/sequtils import repeat, toSeq from std/sequtils import repeat, toSeq
import cliutils, uuids, zero_functional import uuids, zero_functional
import ./[formatting, libpit] import ./[ansiterm, formatting, libpit]
const NO_PROJECT* = "∅ No Project" const NO_PROJECT* = "∅ No Project"
const NO_MILESTONE* = "∅ No Milestone" const NO_MILESTONE* = "∅ No Milestone"
@@ -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:
+13 -2
View File
@@ -7,9 +7,10 @@
# libpit directly rather than calling the pit cli executable. Unfortunately # libpit directly rather than calling the pit cli executable. Unfortunately
# this would require a non-trivial rewrite. # this would require a non-trivial rewrite.
import asyncdispatch, cliutils, docopt, jester, json, logging, options, sequtils, strutils import asyncdispatch, docopt, jester, json, logging, options, sequtils, strutils
import nre except toSeq import cliutils/procutil
import pit/ansiterm
import pit/libpit import pit/libpit
import pit/cliconstants import pit/cliconstants
@@ -23,6 +24,16 @@ const TXT = "text/plain"
proc raiseEx(reason: string): void = raise newException(Exception, reason) proc raiseEx(reason: string): void = raise newException(Exception, reason)
proc queryParamsToCliArgs[T](queryParams: T): seq[string] =
result = @[]
for k, v in queryParams:
if k.startsWith("arg"):
result.add(v)
else:
result.add("--" & k)
if v != "true":
result.add(v)
template halt(code: HttpCode, template halt(code: HttpCode,
headers: RawHeaders, headers: RawHeaders,
content: string): void = content: string): void =
+29 -1
View File
@@ -1 +1,29 @@
doAssert(1 + 1 == 2) import std/[options, tables, times, unittest]
import pit/libpit
suite "regex-backed issue behavior":
test "summary and full-text filters are case-insensitive":
let issue = Issue(
summary: "Fix the Widget",
details: "Failure happens in the renderer",
properties: newTable[string, string](),
tags: @[])
check @[issue].filter(summaryMatchFilter("widget")).len == 1
check @[issue].filter(summaryMatchFilter("missing")).len == 0
check @[issue].filter(fullMatchFilter("RENDERER")).len == 1
test "recurrence values parse captures":
let issue = Issue(
summary: "repeating",
properties: newTable[string, string](),
tags: @[])
issue["recurrence"] = "after 2 weeks, abc123"
let recurrence = issue.getRecurrence()
check recurrence.isSome
check recurrence.get.isFromCompletion
check recurrence.get.interval == weeks(2)
check recurrence.get.cloneId == some("abc123")