Compare commits

..

11 Commits
4.4.1 ... 4.7.0

5 changed files with 114 additions and 57 deletions

View File

@ -1,8 +1,6 @@
# Package
include "src/pitpkg/version.nim"
version = PIT_VERSION
version = "4.7.0"
author = "Jonathan Bernard"
description = "Personal issue tracker."
license = "MIT"
@ -11,5 +9,17 @@ bin = @["pit", "pit_api"]
# Dependencies
requires @[ "nim >= 0.18.0", "cliutils 0.4.1", "docopt 0.6.5", "jester 0.2.0",
"langutils >= 0.4.0", "timeutils 0.3.0", "uuids 0.1.9" ]
requires @[
"nim >= 0.19.0",
"docopt 0.6.8",
"jester 0.4.1",
"uuids 0.1.10",
"https://git.jdb-labs.com/jdb/nim-cli-utils.git >= 0.6.1",
"https://git.jdb-labs.com/jdb/nim-lang-utils.git >= 0.4.0",
"https://git.jdb-labs.com/jdb/nim-time-utils.git >= 0.4.0",
"https://git.jdb-labs.com/jdb/update-nim-package-version"
]
task updateVersion, "Update the version of this package.":
exec "update_nim_package_version pit 'src/pitpkg/version.nim'"

View File

@ -1,11 +1,11 @@
## Personal Issue Tracker CLI interface
## ====================================
import cliutils, docopt, json, logging, options, os, ospaths, sequtils,
import cliutils, docopt, json, logging, options, os, sequtils,
tables, terminal, times, timeutils, unicode, uuids
from nre import re
import strutils except capitalize, toUpper, toLower
import strutils except capitalize, strip, toUpper, toLower
import pitpkg/private/libpit
export libpit
@ -76,11 +76,21 @@ proc formatSectionIssue(ctx: CliContext, issue: Issue, width: int, indent = "",
var showDetails = not issue.details.isNilOrWhitespace and verbose
# Wrap and write the summary.
var wrappedSummary = (" ".repeat(5) & issue.summary).wordWrap(width - 2).indent(2 + indent.len)
wrappedSummary = wrappedSummary[(6 + indent.len)..^1]
var prefixLen = 0
var summaryIndentLen = indent.len + 7
if issue.hasProp("delegated-to"): prefixLen += issue["delegated-to"].len + 2 # space for the ':' and ' '
# Wrap and write the summary.
var wrappedSummary = ("+".repeat(prefixLen) & issue.summary).wordWrap(width - summaryIndentLen).indent(summaryIndentLen)
wrappedSummary = wrappedSummary[(prefixLen + summaryIndentLen)..^1]
result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true) & " "
if issue.hasProp("delegated-to"):
result &= (issue["delegated-to"] & ": ").withColor(fgGreen)
result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true)
result &= wrappedSummary.withColor(fgWhite)
if issue.tags.len > 0:
@ -92,9 +102,9 @@ proc formatSectionIssue(ctx: CliContext, issue: Issue, width: int, indent = "",
if issue.hasProp("pending"):
let startIdx = "Pending: ".len
var pendingText = issue["pending"].wordWrap(width - startIdx - 2)
var pendingText = issue["pending"].wordWrap(width - startIdx - summaryIndentLen)
.indent(startIdx)
pendingText = ("Pending: " & pendingText[startIdx..^1]).indent(indent.len + 2)
pendingText = ("Pending: " & pendingText[startIdx..^1]).indent(summaryIndentLen)
result &= "\n" & pendingText.withColor(fgCyan)
if showDetails:
@ -106,16 +116,9 @@ proc formatSectionIssueList(ctx: CliContext, issues: seq[Issue], width: int,
indent: string, verbose: bool): string =
result = ""
var topPadded = true
for i in issues:
var issueText = ctx.formatSectionIssue(i, width, indent, verbose)
if issueText.splitLines.len > 1:
if topPadded: result &= issueText & "\n\n"
else: result &= "\n" & issueText & "\n\n"
topPadded = true
else:
result &= issueText & "\n"
topPadded = false
proc formatSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
indent = "", verbose = false): string =
@ -241,7 +244,10 @@ Usage:
pit list [<listable>] [options]
pit ( start | done | pending | todo-today | todo | suspend ) <id>... [options]
pit edit <ref>...
pit tag <id>... [options]
pit untag <id>... [options]
pit reorder <state>
pit delegate <id> <delegated-to>
pit ( delete | rm ) <id>...
Options:
@ -353,6 +359,29 @@ Options:
else: edit(ctx.tasksDir.loadIssueById(editRef))
elif args["tag"]:
if not args["--tags"]: raise newException(Exception, "no tags given")
let newTags = ($args["--tags"]).split(",").mapIt(it.strip)
for id in @(args["<id>"]):
var issue = ctx.tasksDir.loadIssueById(id)
issue.tags = deduplicate(issue.tags & newTags)
issue.store()
elif args["untag"]:
let tagsToRemove: seq[string] =
if args["--tags"]: ($args["--tags"]).split(",").mapIt(it.strip)
else: @[]
for id in @(args["<id>"]):
var issue = ctx.tasksDir.loadIssueById(id)
if tagsToRemove.len > 0:
issue.tags = issue.tags.filter(
proc (tag: string): bool = not tagsToRemove.anyIt(it == tag))
else: issue.tags = @[]
issue.store()
elif args["start"] or args["todo-today"] or args["done"] or
args["pending"] or args["todo"] or args["suspend"]:
@ -376,12 +405,26 @@ Options:
if targetState == Current:
let issue = ctx.tasksDir.loadIssueById($(args["<id>"][0]))
var cmd = "ptk start"
if issue.tags.len > 0: cmd &= "-g \"" & issue.tags.join(",") & "\""
if issue.tags.len > 0 or issue.properties.hasKey("context"):
let tags = concat(
issue.tags,
if issue.properties.hasKey("context"): @[issue.properties["context"]]
else: @[]
)
cmd &= " -g \"" & tags.join(",") & "\""
cmd &= " -n \"pit-id: " & $issue.id & "\""
cmd &= " \"" & issue.summary & "\""
discard execShellCmd(cmd)
elif targetState == Done or targetState == Pending:
discard execShellCmd("ptk stop")
elif args["delegate"]:
let issue = ctx.tasksDir.loadIssueById($(args["<id>"]))
issue["delegated-to"] = $args["<delegated-to>"]
issue.store()
elif args["delete"] or args["rm"]:
for id in @(args["<id>"]):

View File

@ -1,7 +1,7 @@
## Personal Issue Tracker API Interface
## ====================================
import asyncdispatch, cliutils, docopt, jester, json, logging, sequtils, strutils
import asyncdispatch, cliutils, docopt, jester, json, logging, options, sequtils, strutils
import nre except toSeq
import pitpkg/private/libpit
@ -18,6 +18,20 @@ const TXT = "text/plain"
proc raiseEx(reason: string): void = raise newException(Exception, reason)
template halt(code: HttpCode,
headers: RawHeaders,
content: string): typed =
## Immediately replies with the specified request. This means any further
## code will not be executed after calling this template in the current
## route.
bind TCActionSend, newHttpHeaders
result[0] = CallbackAction.TCActionSend
result[1] = code
result[2] = some(headers)
result[3] = content
result.matched = true
break allRoutes
template checkAuth(cfg: PitApiCfg) =
## Check this request for authentication and authorization information.
## If the request is not authorized, this template sets up the 401 response
@ -40,30 +54,10 @@ template checkAuth(cfg: PitApiCfg) =
except:
stderr.writeLine "Auth failed: " & getCurrentExceptionMsg()
response.data[0] = CallbackAction.TCActionSend
response.data[1] = Http401
response.data[2]["WWW-Authenticate"] = "Bearer"
response.data[2]["Content-Type"] = TXT
response.data[3] = getCurrentExceptionMsg()
proc paramsToArgs(params: StringTableRef): tuple[stripAnsi: bool, args: seq[string]] =
result = (false, @[])
if params.hasKey("color"):
if params["color"] != "true":
result[0] = true
for k,v in params:
if k == "color": continue
elif k.startsWith("arg"): result[1].add(v) # support ?arg1=val1&arg2=val2 -> cmd val1 val2
else :
result[1].add("--" & k)
if v != "true": result[1].add(v) # support things like ?verbose=true -> cmd --verbose
let STRIP_ANSI_REGEX = re"\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]"
proc stripAnsi(str: string): string =
return str.replace(STRIP_ANSI_REGEX, "")
halt(
Http401,
@{"Content-Type": TXT},
getCurrentExceptionMsg())
proc start*(cfg: PitApiCfg) =
@ -79,20 +73,29 @@ proc start*(cfg: PitApiCfg) =
resp("pong", TXT)
get "/issues":
checkAuth(cfg); if not authed: return true
checkAuth(cfg)
var (stripAnsi, args) = paramsToArgs(request.params)
var args = queryParamsToCliArgs(request.params)
args = @["list"] & args
info "args: \n" & args.join(" ")
let execResult = execWithOutput("pit", ".", args)
if execResult[2] != 0: resp(Http500, stripAnsi($execResult[0] & "\n" & $execResult[1]), TXT)
else:
if stripAnsi: resp(stripAnsi(execResult[0]), TXT)
else: resp(execResult[0], TXT)
else: resp(stripAnsi(execResult[0]), TXT)
post "/issues":
checkAuth(cfg); if not authed: return true
checkAuth(cfg)
get "/issue/@issueId":
checkAuth(cfg)
var args = queryParamsToCliArgs(request.params)
args = @["list", @"issueId"] & args
info "args: \n" & args.join(" ")
let execResult = execWithOutput("pit", ".", args)
if execResult[2] != 0: resp(Http500, stripAnsi($execResult[0] & "\n" & $execResult[1]), TXT)
else: resp(stripAnsi(execResult[0]), TXT)
waitFor(stopFuture)

View File

@ -146,7 +146,8 @@ proc toStorageFormat*(issue: Issue, withComments = false): string =
if withComments: lines.add("# Summary (one line):")
lines.add(issue.summary)
if withComments: lines.add("# Properties (\"key:value\" per line):")
for key, val in issue.properties: lines.add(key & ": " & val)
for key, val in issue.properties:
if not val.isNilOrWhitespace: lines.add(key & ": " & val)
if issue.tags.len > 0: lines.add("tags: " & issue.tags.join(","))
if not isNilOrWhitespace(issue.details) or withComments:
if withComments: lines.add("# Details go below the \"--------\"")
@ -178,7 +179,7 @@ proc store*(tasksDir: string, issue: Issue, state: IssueState, withComments = fa
else:
issue.filepath = stateDir / filename
issue.store()
issue.store(withComments)
proc storeOrder*(issues: seq[Issue], path: string) =
var orderLines = newSeq[string]()

View File

@ -1 +1 @@
const PIT_VERSION = "4.4.1"
const PIT_VERSION* = "4.7.0"