Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec1198dcf0 |
+4
-3
@@ -1,6 +1,6 @@
|
||||
# Package
|
||||
|
||||
version = "4.33.2"
|
||||
version = "4.33.3"
|
||||
author = "Jonathan Bernard"
|
||||
description = "Personal issue tracker."
|
||||
license = "MIT"
|
||||
@@ -10,8 +10,9 @@ bin = @["pit"]
|
||||
|
||||
# Dependencies
|
||||
|
||||
requires "nim >= 1.4.0"
|
||||
requires "nim >= 1.6.0"
|
||||
requires "docopt >= 0.7.1"
|
||||
requires "regex >= 0.26.3"
|
||||
requires "uuids >= 0.1.10"
|
||||
requires "zero_functional"
|
||||
|
||||
@@ -19,7 +20,7 @@ requires "zero_functional"
|
||||
requires "cliutils >= 0.11.1"
|
||||
requires "langutils >= 0.4.0"
|
||||
requires "timeutils >= 0.5.4"
|
||||
requires "data_uri > 1.0.0"
|
||||
requires "filetype >= 0.9.0"
|
||||
requires "update_nim_package_version >= 0.2.0"
|
||||
|
||||
task updateVersion, "Update the version of this package.":
|
||||
|
||||
+10
-9
@@ -1,13 +1,14 @@
|
||||
## Personal Issue Tracker CLI interface
|
||||
## ====================================
|
||||
|
||||
import std/[algorithm, logging, options, os, sequtils, sets, tables, terminal,
|
||||
times, unicode]
|
||||
import cliutils, data_uri, docopt, json, timeutils, uuids, zero_functional
|
||||
import std/[algorithm, logging, options, os, sequtils, sets, tables, times,
|
||||
unicode]
|
||||
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 pit/[cliconstants, formatting, libpit, projects, sync_pbm_vsb]
|
||||
import pit/[ansiterm, cliconstants, data_uri, formatting, libpit, projects,
|
||||
sync_pbm_vsb]
|
||||
|
||||
export formatting, libpit
|
||||
|
||||
@@ -73,7 +74,7 @@ proc addIssue(
|
||||
if issueProps.hasKey("context"):
|
||||
ctx.filterIssues(propsFilter(newTable({"context": issueProps["context"]})))
|
||||
|
||||
let numberRegex = re("^[0-9]+$")
|
||||
let numberRegex = re2("^[0-9]+$")
|
||||
for propName in defaultProps:
|
||||
if not issueProps.hasKey(propName):
|
||||
let allIssues: seq[seq[Issue]] = toSeq(values(ctx.issues))
|
||||
@@ -98,7 +99,7 @@ proc addIssue(
|
||||
let resp = stdin.readLine.strip
|
||||
let numberResp = resp.match(numberRegex)
|
||||
|
||||
if numberResp.isSome:
|
||||
if numberResp:
|
||||
let idx = parseInt(resp)
|
||||
if idx >= 0 and idx < previousValues.len:
|
||||
issueProps[propName] = previousValues[idx]
|
||||
@@ -211,10 +212,10 @@ when isMainModule:
|
||||
|
||||
# If they supplied text matches, add that to the filter.
|
||||
if args["--match"]:
|
||||
filter.summaryMatch = some(re("(?i)" & $args["--match"]))
|
||||
filter.summaryMatch = some(re2("(?i)" & $args["--match"]))
|
||||
|
||||
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 ctx.defaultContext.isSome and not filter.properties.hasKey("context"):
|
||||
|
||||
@@ -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.33.2"
|
||||
const PIT_VERSION* = "4.33.3"
|
||||
|
||||
const USAGE* = """Usage:
|
||||
pit ( new | add) <summary> [<state>] [options]
|
||||
@@ -71,10 +71,10 @@ Options:
|
||||
properties set.
|
||||
|
||||
-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
|
||||
match the given pattern (PCRE regex supported).
|
||||
match the given pattern (regex syntax supported).
|
||||
|
||||
-v, --verbose Show issue details when listing issues.
|
||||
|
||||
|
||||
@@ -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])
|
||||
@@ -1,6 +1,7 @@
|
||||
import std/[options, sequtils, tables, terminal, times, unicode, wordwrap]
|
||||
import cliutils, uuids
|
||||
import uuids
|
||||
import std/strutils except alignLeft, capitalize, strip, toLower, toUpper
|
||||
import ./ansiterm
|
||||
import ./libpit
|
||||
|
||||
proc adjustedTerminalWidth(): int = min(terminalWidth(), 80)
|
||||
|
||||
+24
-19
@@ -1,8 +1,8 @@
|
||||
import std/[json, logging, options, os, strformat, strutils, tables, times,
|
||||
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 `>`
|
||||
from sequtils import deduplicate, toSeq
|
||||
|
||||
@@ -25,7 +25,7 @@ type
|
||||
|
||||
IssueFilter* = ref object
|
||||
completedRange*: Option[tuple[b, e: DateTime]]
|
||||
fullMatch*, summaryMatch*: Option[Regex]
|
||||
fullMatch*, summaryMatch*: Option[Regex2]
|
||||
inclStates*: seq[IssueState]
|
||||
exclStates*: seq[IssueState]
|
||||
hasTags*: seq[string]
|
||||
@@ -61,8 +61,8 @@ const MATCH_ANY* = "<match-any>"
|
||||
const DONE_FOLDER_FORMAT* = "yyyy-MM"
|
||||
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 RECURRENCE_PATTERN = re"(every|after) ((\d+) )?((hour|day|week|month|year)s?)(, ([0-9a-fA-F]+))?"
|
||||
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 = re2"(every|after) ((\d+) )?((hour|day|week|month|year)s?)(, ([0-9a-fA-F]+))?"
|
||||
|
||||
let traceStartTime = cpuTime()
|
||||
var lastTraceTime = traceStartTime
|
||||
@@ -116,26 +116,31 @@ proc setDateTime*(issue: Issue, key: string, dt: DateTime) =
|
||||
proc getRecurrence*(issue: Issue): Option[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"] & "'"
|
||||
return none[Recurrence]()
|
||||
|
||||
let c = nre.toSeq(m.get.captures)
|
||||
let timeVal = if c[2].isSome: c[2].get.parseInt
|
||||
proc captured(i: int): Option[string] =
|
||||
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
|
||||
return some(Recurrence(
|
||||
isFromCompletion: c[0].get == "after",
|
||||
isFromCompletion: captured(0).get == "after",
|
||||
interval:
|
||||
case c[4].get:
|
||||
case captured(4).get:
|
||||
of "hour": hours(timeVal)
|
||||
of "day": days(timeVal)
|
||||
of "week": weeks(timeVal)
|
||||
of "month": months(timeVal)
|
||||
of "year": years(timeVal)
|
||||
else: weeks(1),
|
||||
cloneId: c[6]))
|
||||
cloneId: captured(6)))
|
||||
|
||||
proc setPriority*(issue: Issue, priority: IssuePriority) =
|
||||
issue["priority"] = $priority
|
||||
@@ -149,8 +154,8 @@ proc getPriority*(issue: Issue): IssuePriority =
|
||||
proc initFilter*(): IssueFilter =
|
||||
result = IssueFilter(
|
||||
completedRange: none(tuple[b, e: DateTime]),
|
||||
fullMatch: none(Regex),
|
||||
summaryMatch: none(Regex),
|
||||
fullMatch: none(Regex2),
|
||||
summaryMatch: none(Regex2),
|
||||
inclStates: @[],
|
||||
exclStates: @[],
|
||||
exclHidden: true,
|
||||
@@ -173,11 +178,11 @@ proc dateFilter*(range: tuple[b, e: DateTime]): IssueFilter =
|
||||
|
||||
proc summaryMatchFilter*(pattern: string): IssueFilter =
|
||||
result = initFilter()
|
||||
result.summaryMatch = some(re("(?i)" & pattern))
|
||||
result.summaryMatch = some(re2("(?i)" & pattern))
|
||||
|
||||
proc fullMatchFilter*(pattern: string): IssueFilter =
|
||||
result = initFilter()
|
||||
result.fullMatch = some(re("(?i)" & pattern))
|
||||
result.fullMatch = some(re2("(?i)" & pattern))
|
||||
|
||||
proc hasTagsFilter*(tags: seq[string]): IssueFilter =
|
||||
result = initFilter()
|
||||
@@ -344,7 +349,7 @@ proc loadIssues*(path: string): seq[Issue] =
|
||||
var unorderedIssues: seq[TaggedIssue] = @[]
|
||||
|
||||
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))
|
||||
|
||||
trace "loaded " & $unorderedIssues.len & " issues", true
|
||||
@@ -451,12 +456,12 @@ proc filter*(issues: seq[Issue], filter: IssueFilter): seq[Issue] =
|
||||
|
||||
if filter.summaryMatch.isSome:
|
||||
let p = filter.summaryMatch.get
|
||||
f = f --> filter(it.summary.find(p).isSome)
|
||||
f = f --> filter(it.summary.contains(p))
|
||||
|
||||
if filter.fullMatch.isSome:
|
||||
let p = filter.fullMatch.get
|
||||
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:
|
||||
let tag = tagLent
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables,
|
||||
terminal, times, unicode]
|
||||
from std/sequtils import repeat, toSeq
|
||||
import cliutils, uuids, zero_functional
|
||||
import ./[formatting, libpit]
|
||||
import uuids, zero_functional
|
||||
import ./[ansiterm, formatting, libpit]
|
||||
|
||||
const NO_PROJECT* = "∅ No Project"
|
||||
const NO_MILESTONE* = "∅ No Milestone"
|
||||
|
||||
+13
-2
@@ -7,9 +7,10 @@
|
||||
# libpit directly rather than calling the pit cli executable. Unfortunately
|
||||
# this would require a non-trivial rewrite.
|
||||
|
||||
import asyncdispatch, cliutils, docopt, jester, json, logging, options, sequtils, strutils
|
||||
import nre except toSeq
|
||||
import asyncdispatch, docopt, jester, json, logging, options, sequtils, strutils
|
||||
import cliutils/procutil
|
||||
|
||||
import pit/ansiterm
|
||||
import pit/libpit
|
||||
import pit/cliconstants
|
||||
|
||||
@@ -23,6 +24,16 @@ const TXT = "text/plain"
|
||||
|
||||
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,
|
||||
headers: RawHeaders,
|
||||
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