Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec1198dcf0 | |||
| 2ed12e7487 | |||
| a273091dc7 | |||
| e35bd9f85a | |||
| 3d8fafd7b2 |
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
[tools]
|
[tools]
|
||||||
nim = "2.2.6"
|
nim = "2.2.8"
|
||||||
|
|||||||
+11
-14
@@ -1,6 +1,6 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "4.32.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'"
|
||||||
+29
-21
@@ -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"):
|
||||||
@@ -253,11 +254,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 +263,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 +280,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"]:
|
||||||
|
|||||||
@@ -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, {})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const PIT_VERSION* = "4.32.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.
|
||||||
|
|
||||||
@@ -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>'.
|
||||||
"""
|
"""
|
||||||
@@ -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
@@ -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*(
|
||||||
|
|||||||
+27
-20
@@ -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()
|
||||||
@@ -230,7 +235,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
|
||||||
|
|
||||||
@@ -342,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
|
||||||
@@ -449,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
@@ -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
@@ -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
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user