Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
1064de3e1b | |||
8b0c751344 | |||
0f7e257f76 | |||
76225d1c50 | |||
0361d1b869 | |||
40cb602362 | |||
e2a306c1d6 | |||
e955cd5b24 | |||
4176dfea3a | |||
587e3c4509 | |||
f6a97c384a | |||
0c3d73dc2b | |||
9a0bf35882 | |||
be7c099b7b | |||
d04797460c | |||
8cf0bf5d98 | |||
ddad90ddef |
2
.mise.toml
Normal file
2
.mise.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[tools]
|
||||||
|
nim = "2.2.0"
|
174
nimble.lock
174
nimble.lock
@ -1,174 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 2,
|
|
||||||
"packages": {
|
|
||||||
"asynctools": {
|
|
||||||
"version": "0.1.1",
|
|
||||||
"vcsRevision": "0e6bdc3ed5bae8c7cc9e03cfbf66b7c882a908a7",
|
|
||||||
"url": "https://github.com/cheatfate/asynctools",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "54314dceabb06b20908ecb0f2c007e9ff3aaa054"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isaac": {
|
|
||||||
"version": "0.1.3",
|
|
||||||
"vcsRevision": "45a5cbbd54ff59ba3ed94242620c818b9aad1b5b",
|
|
||||||
"url": "https://github.com/pragmagic/isaac/",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "05c3583a954715d84b0bf1be97f9a503249e9cdf"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uuids": {
|
|
||||||
"version": "0.1.11",
|
|
||||||
"vcsRevision": "8cb8720b567c6bcb261bd1c0f7491bdb5209ad06",
|
|
||||||
"url": "https://github.com/pragmagic/uuids/",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [
|
|
||||||
"isaac"
|
|
||||||
],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "393f5fcefbc8ad3cf167e59760144208ff8f9f76"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"unicodedb": {
|
|
||||||
"version": "0.11.2",
|
|
||||||
"vcsRevision": "c70f8bc8c7373265670e0575bc5eda36fe3761b0",
|
|
||||||
"url": "https://github.com/nitely/nim-unicodedb",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "612c5955f91bd90a263ce914d1d5de74a33af3c6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"regex": {
|
|
||||||
"version": "0.20.1",
|
|
||||||
"vcsRevision": "66f144f935cc73977c61185fab15a3147bf117ff",
|
|
||||||
"url": "https://github.com/nitely/nim-regex",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [
|
|
||||||
"unicodedb"
|
|
||||||
],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "ea9b6600443e73b1ea89a477c7a5d1fce742c9da"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"docopt": {
|
|
||||||
"version": "0.7.0",
|
|
||||||
"vcsRevision": "17803d1205f9e752cce03a66b0a29b710520398e",
|
|
||||||
"url": "https://github.com/docopt/docopt.nim",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [
|
|
||||||
"regex"
|
|
||||||
],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "21150284640b882fa91147181c52da3e5bb44df8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"filetype": {
|
|
||||||
"version": "0.9.0",
|
|
||||||
"vcsRevision": "1fe1e7d988cd802abc26505efb5a91891bd6f53e",
|
|
||||||
"url": "https://github.com/jiro4989/filetype",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "d2242b94eeb0f6d3810a8c71af4664f28853e1be"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"zero_functional": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"vcsRevision": "edf3b7f59119f75706da435c2b7f080a0cf4960c",
|
|
||||||
"url": "https://github.com/zero-functional/zero-functional",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "2dc01ca0925ac1c2dcb46a0c6d9c93c57a9cddec"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"update_nim_package_version": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"vcsRevision": "5a78579fd7f88014263aed38c60327c85f6f8bcf",
|
|
||||||
"url": "https://git.jdb-software.com/jdb/update-nim-package-version",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "37052b7ce30d5493ef24253a82a6087350b4eabb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data_uri": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"vcsRevision": "f43ac66e44c37edd3cc7282d75d6fa2fa031b2ec",
|
|
||||||
"url": "",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [
|
|
||||||
"docopt",
|
|
||||||
"filetype",
|
|
||||||
"zero_functional",
|
|
||||||
"update_nim_package_version"
|
|
||||||
],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "949c11ffab4e85ff538b0bd3e5bb193f118b56d7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timeutils": {
|
|
||||||
"version": "0.5.4",
|
|
||||||
"vcsRevision": "a9308cbaf3c89496b5832ddd18404dc0debe66a2",
|
|
||||||
"url": "https://git.jdb-software.com/jdb/nim-time-utils.git",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "9ecd1020f5644bc59acb2f44ca9d30f4c7f066d3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cliutils": {
|
|
||||||
"version": "0.8.0",
|
|
||||||
"vcsRevision": "b1cc4fbe51d5e617789363efe716793ebe5bc5f1",
|
|
||||||
"url": "https://git.jdb-software.com/jdb/nim-cli-utils",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [
|
|
||||||
"docopt"
|
|
||||||
],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "5b114094c314007fa6f15e62852d62a58a3cbb62"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"langutils": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"vcsRevision": "8122660da3fc78132b823e76c9e990fd92802b0e",
|
|
||||||
"url": "https://git.jdb-software.com/jdb/nim-lang-utils.git",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "2da09deb0e0bfc186000f0d941d06dd974bf6e58"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"httpbeast": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"vcsRevision": "abc13d11c210b614960fe8760e581d44cfb2e3e9",
|
|
||||||
"url": "https://github.com/dom96/httpbeast",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [
|
|
||||||
"asynctools"
|
|
||||||
],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "b23e57a401057dcb9b7fae1fb8279a6a2ce1d0b8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"jester": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"vcsRevision": "a21b36a02b7745d6cdcda32d4ab3fba328cda17a",
|
|
||||||
"url": "https://github.com/dom96/jester/",
|
|
||||||
"downloadMethod": "git",
|
|
||||||
"dependencies": [
|
|
||||||
"httpbeast",
|
|
||||||
"asynctools"
|
|
||||||
],
|
|
||||||
"checksums": {
|
|
||||||
"sha1": "a192ca25bfc05d5de5c9a5fafca3b0cee47d82d2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tasks": {}
|
|
||||||
}
|
|
16
pit.nimble
16
pit.nimble
@ -1,31 +1,31 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "4.24.0"
|
version = "4.29.1"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Personal issue tracker."
|
description = "Personal issue tracker."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
srcDir = "src"
|
srcDir = "src"
|
||||||
installExt = @["nim"]
|
installExt = @["nim"]
|
||||||
bin = @["pit", "pit_api"]
|
bin = @["pit"]
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
||||||
requires @[
|
requires @[
|
||||||
"nim >= 1.4.0",
|
"nim >= 1.4.0",
|
||||||
"docopt >= 0.6.8",
|
"docopt >= 0.7.1",
|
||||||
"jester >= 0.5.0",
|
"jester >= 0.6.0",
|
||||||
"uuids >= 0.1.10",
|
"uuids >= 0.1.10",
|
||||||
"zero_functional"
|
"zero_functional"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Dependencies from git.jdb-software.com/nim-jdb/packages
|
# Dependencies from git.jdb-software.com/jdb/nim-packages
|
||||||
requires @[
|
requires @[
|
||||||
"cliutils >= 0.8.1",
|
"cliutils >= 0.9.1",
|
||||||
"langutils >= 0.4.0",
|
"langutils >= 0.4.0",
|
||||||
"timeutils >= 0.5.4",
|
"timeutils >= 0.5.4",
|
||||||
"data_uri > 1.0.0",
|
"data_uri > 1.0.0",
|
||||||
"https://git.jdb-software.com/jdb/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/pitpkg/cliconstants.nim'"
|
exec "update_nim_package_version pit 'src/pit/cliconstants.nim'"
|
1
src/config.nims
Normal file
1
src/config.nims
Normal file
@ -0,0 +1 @@
|
|||||||
|
switch("define", "ssl")
|
326
src/pit.nim
326
src/pit.nim
@ -1,187 +1,19 @@
|
|||||||
## Personal Issue Tracker CLI interface
|
## Personal Issue Tracker CLI interface
|
||||||
## ====================================
|
## ====================================
|
||||||
|
|
||||||
import std/[algorithm, logging, options, os, sequtils, wordwrap, tables,
|
import std/[algorithm, logging, options, os, sequtils, tables, times, unicode]
|
||||||
terminal, times, unicode]
|
import data_uri, docopt, json, timeutils, uuids
|
||||||
import cliutils, data_uri, docopt, json, timeutils, uuids
|
|
||||||
|
|
||||||
from nre import re
|
from nre import re
|
||||||
import strutils except alignLeft, capitalize, strip, toUpper, toLower
|
import strutils except alignLeft, capitalize, strip, toUpper, toLower
|
||||||
import pitpkg/private/libpit
|
import pit/[cliconstants, formatting, libpit, sync_pbm_vsb]
|
||||||
import pitpkg/cliconstants
|
|
||||||
export libpit
|
|
||||||
|
|
||||||
type
|
export formatting, libpit
|
||||||
CliContext = ref object
|
|
||||||
cfg*: PitConfig
|
|
||||||
contexts*: TableRef[string, string]
|
|
||||||
defaultContext*: Option[string]
|
|
||||||
issues*: TableRef[IssueState, seq[Issue]]
|
|
||||||
termWidth*: int
|
|
||||||
triggerPtk*, verbose*: bool
|
|
||||||
|
|
||||||
let EDITOR =
|
let EDITOR =
|
||||||
if existsEnv("EDITOR"): getEnv("EDITOR")
|
if existsEnv("EDITOR"): getEnv("EDITOR")
|
||||||
else: "vi"
|
else: "vi"
|
||||||
|
|
||||||
|
|
||||||
proc initContext(args: Table[string, Value]): CliContext =
|
|
||||||
let pitCfg = loadConfig(args)
|
|
||||||
|
|
||||||
let cliJson =
|
|
||||||
if pitCfg.cfg.json.hasKey("cli"): pitCfg.cfg.json["cli"]
|
|
||||||
else: newJObject()
|
|
||||||
|
|
||||||
let cliCfg = CombinedConfig(docopt: args, json: cliJson)
|
|
||||||
|
|
||||||
result = CliContext(
|
|
||||||
cfg: pitCfg,
|
|
||||||
contexts: pitCfg.contexts,
|
|
||||||
defaultContext:
|
|
||||||
if not cliJson.hasKey("defaultContext"): none(string)
|
|
||||||
else: some(cliJson["defaultContext"].getStr()),
|
|
||||||
verbose: parseBool(cliCfg.getVal("verbose", "false")) and not args["--quiet"],
|
|
||||||
issues: newTable[IssueState, seq[Issue]](),
|
|
||||||
termWidth: parseInt(cliCfg.getVal("termWidth", "80")),
|
|
||||||
triggerPtk: cliJson.getOrDefault("triggerPtk").getBool(false))
|
|
||||||
|
|
||||||
proc getIssueContextDisplayName(ctx: CliContext, context: string): string =
|
|
||||||
if not ctx.contexts.hasKey(context):
|
|
||||||
if context.isEmptyOrWhitespace: return "<default>"
|
|
||||||
else: return context.capitalize()
|
|
||||||
return ctx.contexts[context]
|
|
||||||
|
|
||||||
proc formatIssue*(issue: Issue): string =
|
|
||||||
result = ($issue.id).withColor(fgBlack, true) & "\n"&
|
|
||||||
issue.summary.withColor(fgWhite) & "\n"
|
|
||||||
|
|
||||||
if issue.tags.len > 0:
|
|
||||||
result &= "tags: ".withColor(fgMagenta) &
|
|
||||||
issue.tags.join(",").withColor(fgGreen, true) & "\n"
|
|
||||||
|
|
||||||
if issue.properties.len > 0:
|
|
||||||
result &= termColor(fgMagenta)
|
|
||||||
for k, v in issue.properties: result &= k & ": " & v & "\n"
|
|
||||||
|
|
||||||
|
|
||||||
result &= "--------".withColor(fgBlack, true) & "\n"
|
|
||||||
if not issue.details.isEmptyOrWhitespace:
|
|
||||||
result &= issue.details.strip.withColor(fgCyan) & "\n"
|
|
||||||
|
|
||||||
result &= termReset
|
|
||||||
|
|
||||||
proc formatSectionIssue*(
|
|
||||||
issue: Issue,
|
|
||||||
width: int = 80,
|
|
||||||
indent = "",
|
|
||||||
verbose = false): string =
|
|
||||||
|
|
||||||
result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true) & " "
|
|
||||||
|
|
||||||
let showDetails = not issue.details.isEmptyOrWhitespace and verbose
|
|
||||||
|
|
||||||
let summaryIndentLen = indent.len + 7
|
|
||||||
let summaryWidth = width - summaryIndentLen
|
|
||||||
|
|
||||||
let summaryLines = issue.summary
|
|
||||||
.wrapWords(summaryWidth)
|
|
||||||
.splitLines
|
|
||||||
|
|
||||||
result &= summaryLines[0].withColor(fgWhite)
|
|
||||||
|
|
||||||
for line in summaryLines[1..^1]:
|
|
||||||
result &= "\p" & line.indent(summaryIndentLen)
|
|
||||||
|
|
||||||
var lastLineLen = summaryLines[^1].len
|
|
||||||
|
|
||||||
if issue.hasProp("delegated-to"):
|
|
||||||
if lastLineLen + issue["delegated-to"].len + 1 < summaryWidth:
|
|
||||||
result &= " " & issue["delegated-to"].withColor(fgMagenta)
|
|
||||||
lastLineLen += issue["delegated-to"].len + 1
|
|
||||||
else:
|
|
||||||
result &= "\p" & issue["delegated-to"]
|
|
||||||
.withColor(fgMagenta)
|
|
||||||
.indent(summaryIndentLen)
|
|
||||||
lastLineLen = issue["delegated-to"].len
|
|
||||||
|
|
||||||
if issue.tags.len > 0:
|
|
||||||
let tagsStrLines = ("(" & issue.tags.join(", ") & ")")
|
|
||||||
.wrapWords(summaryWidth)
|
|
||||||
.splitLines
|
|
||||||
|
|
||||||
if tagsStrLines.len == 1 and
|
|
||||||
(lastLineLen + tagsStrLines[0].len + 1) < summaryWidth:
|
|
||||||
result &= " " & tagsStrLines[0].withColor(fgGreen)
|
|
||||||
lastLineLen += tagsStrLines[0].len + 1
|
|
||||||
else:
|
|
||||||
result &= "\p" & tagsStrLines
|
|
||||||
.mapIt(it.indent(summaryIndentLen))
|
|
||||||
.join("\p")
|
|
||||||
.withColor(fgGreen)
|
|
||||||
lastLineLen = tagsStrLines[^1].len
|
|
||||||
|
|
||||||
if issue.hasProp("pending"):
|
|
||||||
result &= "\p" & ("Pending: " & issue["pending"])
|
|
||||||
.wrapwords(summaryWidth)
|
|
||||||
.withColor(fgCyan)
|
|
||||||
.indent(summaryIndentLen)
|
|
||||||
|
|
||||||
if showDetails:
|
|
||||||
result &= "\p" & issue.details
|
|
||||||
.strip
|
|
||||||
.withColor(fgBlack, bright = true)
|
|
||||||
.indent(summaryIndentLen)
|
|
||||||
|
|
||||||
result &= termReset
|
|
||||||
|
|
||||||
proc formatSectionIssueList*(
|
|
||||||
issues: seq[Issue],
|
|
||||||
width: int = 80,
|
|
||||||
indent: string = "",
|
|
||||||
verbose: bool = false): string =
|
|
||||||
|
|
||||||
result = ""
|
|
||||||
for i in issues:
|
|
||||||
var issueText = formatSectionIssue(i, width, indent, verbose)
|
|
||||||
result &= issueText & "\n"
|
|
||||||
|
|
||||||
proc formatSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
|
|
||||||
indent = "", verbose = false): string =
|
|
||||||
let innerWidth = ctx.termWidth - (indent.len * 2)
|
|
||||||
|
|
||||||
result = termColor(fgBlue) &
|
|
||||||
(indent & ".".repeat(innerWidth)) & "\n" &
|
|
||||||
state.displayName.center(ctx.termWidth) & "\n\n" &
|
|
||||||
termReset
|
|
||||||
|
|
||||||
let issuesByContext = issues.groupBy("context")
|
|
||||||
|
|
||||||
if issues.len > 5 and issuesByContext.len > 1:
|
|
||||||
for context, ctxIssues in issuesByContext:
|
|
||||||
|
|
||||||
result &= termColor(fgYellow) &
|
|
||||||
indent & ctx.getIssueContextDisplayName(context) & ":" &
|
|
||||||
termReset & "\n\n"
|
|
||||||
|
|
||||||
result &= formatSectionIssueList(ctxIssues, innerWidth - 2, indent & " ", verbose)
|
|
||||||
result &= "\n"
|
|
||||||
|
|
||||||
else: result &= formatSectionIssueList(issues, innerWidth, indent, verbose)
|
|
||||||
|
|
||||||
proc loadIssues(ctx: CliContext, state: IssueState) =
|
|
||||||
ctx.issues[state] = loadIssues(ctx.cfg.tasksDir, state)
|
|
||||||
|
|
||||||
proc loadOpenIssues(ctx: CliContext) =
|
|
||||||
ctx.issues = newTable[IssueState, seq[Issue]]()
|
|
||||||
for state in [Current, TodoToday, Todo, Pending, Todo]: ctx.loadIssues(state)
|
|
||||||
|
|
||||||
proc loadAllIssues(ctx: CliContext) =
|
|
||||||
ctx.issues = ctx.cfg.tasksDir.loadAllIssues()
|
|
||||||
|
|
||||||
proc filterIssues(ctx: CliContext, filter: IssueFilter) =
|
|
||||||
for state, issueList in ctx.issues:
|
|
||||||
ctx.issues[state] = issueList.filter(filter)
|
|
||||||
|
|
||||||
proc parsePropertiesOption(propsOpt: string): TableRef[string, string] =
|
proc parsePropertiesOption(propsOpt: string): TableRef[string, string] =
|
||||||
result = newTable[string, string]()
|
result = newTable[string, string]()
|
||||||
for propText in propsOpt.split(";"):
|
for propText in propsOpt.split(";"):
|
||||||
@ -199,16 +31,6 @@ proc parseExclPropertiesOption(propsOpt: string): TableRef[string, seq[string]]
|
|||||||
if result.hasKey(pair[0]): result[pair[0]].add(val)
|
if result.hasKey(pair[0]): result[pair[0]].add(val)
|
||||||
else: result[pair[0]] = @[val]
|
else: result[pair[0]] = @[val]
|
||||||
|
|
||||||
proc sameDay(a, b: DateTime): bool =
|
|
||||||
result = a.year == b.year and a.yearday == b.yearday
|
|
||||||
|
|
||||||
proc writeHeader(ctx: CliContext, header: string) =
|
|
||||||
stdout.setForegroundColor(fgRed, true)
|
|
||||||
stdout.writeLine('_'.repeat(ctx.termWidth))
|
|
||||||
stdout.writeLine(header.center(ctx.termWidth))
|
|
||||||
stdout.writeLine('~'.repeat(ctx.termWidth))
|
|
||||||
stdout.resetAttributes
|
|
||||||
|
|
||||||
proc reorder(ctx: CliContext, state: IssueState) =
|
proc reorder(ctx: CliContext, state: IssueState) =
|
||||||
|
|
||||||
# load the issues to make sure the order file contains all issues in the state.
|
# load the issues to make sure the order file contains all issues in the state.
|
||||||
@ -231,75 +53,6 @@ proc edit(issue: Issue) =
|
|||||||
getCurrentExceptionMsg()
|
getCurrentExceptionMsg()
|
||||||
issue.store()
|
issue.store()
|
||||||
|
|
||||||
proc list(
|
|
||||||
ctx: CliContext,
|
|
||||||
filter: Option[IssueFilter],
|
|
||||||
states: Option[seq[IssueState]],
|
|
||||||
showToday, showFuture,
|
|
||||||
showHidden = false,
|
|
||||||
verbose: bool) =
|
|
||||||
|
|
||||||
if states.isSome:
|
|
||||||
trace "listing issues for " & $states.get
|
|
||||||
for state in states.get:
|
|
||||||
ctx.loadIssues(state)
|
|
||||||
if filter.isSome: ctx.filterIssues(filter.get)
|
|
||||||
|
|
||||||
# Show Done for just today if requested
|
|
||||||
if state == Done and showToday:
|
|
||||||
ctx.issues[Done] = ctx.issues[Done].filterIt(
|
|
||||||
it.hasProp("completed") and
|
|
||||||
sameDay(getTime().local, it.getDateTime("completed")))
|
|
||||||
|
|
||||||
stdout.write ctx.formatSection(ctx.issues[state], state, "", verbose)
|
|
||||||
trace "listing complete"
|
|
||||||
return
|
|
||||||
|
|
||||||
ctx.loadOpenIssues()
|
|
||||||
if filter.isSome:
|
|
||||||
ctx.filterIssues(filter.get)
|
|
||||||
trace "filtered issues"
|
|
||||||
|
|
||||||
let today = showToday and [Current, TodoToday, Pending].anyIt(
|
|
||||||
ctx.issues.hasKey(it) and ctx.issues[it].len > 0)
|
|
||||||
|
|
||||||
let future = showFuture and [Pending, Todo].anyIt(
|
|
||||||
ctx.issues.hasKey(it) and ctx.issues[it].len > 0)
|
|
||||||
|
|
||||||
let indent = if today and future: " " else: ""
|
|
||||||
|
|
||||||
# Today's items
|
|
||||||
if today:
|
|
||||||
if future: ctx.writeHeader("Today")
|
|
||||||
|
|
||||||
for s in [Current, TodoToday, Pending]:
|
|
||||||
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
|
|
||||||
let visibleIssues = ctx.issues[s].filterIt(
|
|
||||||
showHidden or
|
|
||||||
not (it.hasProp("hide-until") and
|
|
||||||
it.getDateTime("hide-until") > getTime().local))
|
|
||||||
|
|
||||||
stdout.write ctx.formatSection(visibleIssues, s, indent, verbose)
|
|
||||||
|
|
||||||
# Future items
|
|
||||||
if future:
|
|
||||||
if today: ctx.writeHeader("Future")
|
|
||||||
|
|
||||||
let futureCategories =
|
|
||||||
if showToday: @[Todo]
|
|
||||||
else: @[Pending, Todo]
|
|
||||||
|
|
||||||
for s in futureCategories:
|
|
||||||
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
|
|
||||||
let visibleIssues = ctx.issues[s].filterIt(
|
|
||||||
showHidden or
|
|
||||||
not (it.hasProp("hide-until") and
|
|
||||||
it.getDateTime("hide-until") > getTime().local))
|
|
||||||
|
|
||||||
stdout.write ctx.formatSection(visibleIssues, s, indent, verbose)
|
|
||||||
|
|
||||||
trace "listing complete"
|
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
@ -314,6 +67,9 @@ when isMainModule:
|
|||||||
if args["--debug"]:
|
if args["--debug"]:
|
||||||
consoleLogger.levelThreshold = lvlDebug
|
consoleLogger.levelThreshold = lvlDebug
|
||||||
|
|
||||||
|
if args["--silent"]:
|
||||||
|
consoleLogger.levelThreshold = lvlNone
|
||||||
|
|
||||||
if args["--echo-args"]: stderr.writeLine($args)
|
if args["--echo-args"]: stderr.writeLine($args)
|
||||||
|
|
||||||
if args["help"]:
|
if args["help"]:
|
||||||
@ -325,6 +81,7 @@ when isMainModule:
|
|||||||
|
|
||||||
trace "context initiated"
|
trace "context initiated"
|
||||||
|
|
||||||
|
var updatedIssues = newSeq[Issue]()
|
||||||
var propertiesOption = none(TableRef[string,string])
|
var propertiesOption = none(TableRef[string,string])
|
||||||
var exclPropsOption = none(TableRef[string,seq[string]])
|
var exclPropsOption = none(TableRef[string,seq[string]])
|
||||||
var tagsOption = none(seq[string])
|
var tagsOption = none(seq[string])
|
||||||
@ -380,7 +137,7 @@ when isMainModule:
|
|||||||
else: newSeq[string]())
|
else: newSeq[string]())
|
||||||
|
|
||||||
ctx.cfg.tasksDir.store(issue, state)
|
ctx.cfg.tasksDir.store(issue, state)
|
||||||
|
updatedIssues.add(issue)
|
||||||
stdout.writeLine formatIssue(issue)
|
stdout.writeLine formatIssue(issue)
|
||||||
|
|
||||||
elif args["reorder"]:
|
elif args["reorder"]:
|
||||||
@ -389,6 +146,11 @@ 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))
|
||||||
@ -397,9 +159,20 @@ when isMainModule:
|
|||||||
if stateOption.isSome:
|
if stateOption.isSome:
|
||||||
let state = stateOption.get
|
let state = stateOption.get
|
||||||
ctx.loadIssues(state)
|
ctx.loadIssues(state)
|
||||||
for issue in ctx.issues[state]: edit(issue)
|
for issue in ctx.issues[state]:
|
||||||
|
if propsOption.isSome:
|
||||||
|
for k,v in propsOption.get:
|
||||||
|
issue[k] = v
|
||||||
|
edit(issue)
|
||||||
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
else: edit(ctx.cfg.tasksDir.loadIssueById(editRef))
|
else:
|
||||||
|
let issue = ctx.cfg.tasksDir.loadIssueById(editRef)
|
||||||
|
if propertiesOption.isSome:
|
||||||
|
for k,v in propertiesOption.get:
|
||||||
|
issue[k] = v
|
||||||
|
edit(issue)
|
||||||
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
elif args["tag"]:
|
elif args["tag"]:
|
||||||
if tagsOption.isNone: raise newException(Exception, "no tags given")
|
if tagsOption.isNone: raise newException(Exception, "no tags given")
|
||||||
@ -410,6 +183,7 @@ when isMainModule:
|
|||||||
var issue = ctx.cfg.tasksDir.loadIssueById(id)
|
var issue = ctx.cfg.tasksDir.loadIssueById(id)
|
||||||
issue.tags = deduplicate(issue.tags & newTags)
|
issue.tags = deduplicate(issue.tags & newTags)
|
||||||
issue.store()
|
issue.store()
|
||||||
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
elif args["untag"]:
|
elif args["untag"]:
|
||||||
let tagsToRemove: seq[string] =
|
let tagsToRemove: seq[string] =
|
||||||
@ -423,6 +197,7 @@ when isMainModule:
|
|||||||
proc (tag: string): bool = not tagsToRemove.anyIt(it == tag))
|
proc (tag: string): bool = not tagsToRemove.anyIt(it == tag))
|
||||||
else: issue.tags = @[]
|
else: issue.tags = @[]
|
||||||
issue.store()
|
issue.store()
|
||||||
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
elif args["start"] or args["todo-today"] or args["done"] or
|
elif args["start"] or args["todo-today"] or args["done"] or
|
||||||
args["pending"] or args["todo"] or args["suspend"]:
|
args["pending"] or args["todo"] or args["suspend"]:
|
||||||
@ -446,10 +221,12 @@ when isMainModule:
|
|||||||
let nextIssue = ctx.cfg.tasksDir.nextRecurrence(issue.getRecurrence.get, issue)
|
let nextIssue = ctx.cfg.tasksDir.nextRecurrence(issue.getRecurrence.get, issue)
|
||||||
ctx.cfg.tasksDir.store(nextIssue, TodoToday)
|
ctx.cfg.tasksDir.store(nextIssue, TodoToday)
|
||||||
info "created the next recurrence:"
|
info "created the next recurrence:"
|
||||||
|
updatedIssues.add(nextIssue)
|
||||||
stdout.writeLine formatIssue(nextIssue)
|
stdout.writeLine formatIssue(nextIssue)
|
||||||
|
|
||||||
|
|
||||||
issue.changeState(ctx.cfg.tasksDir, targetState)
|
issue.changeState(ctx.cfg.tasksDir, targetState)
|
||||||
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
if ctx.triggerPtk or args["--ptk"]:
|
if ctx.triggerPtk or args["--ptk"]:
|
||||||
if targetState == Current:
|
if targetState == Current:
|
||||||
@ -463,7 +240,7 @@ when isMainModule:
|
|||||||
)
|
)
|
||||||
cmd &= " -g \"" & tags.join(",") & "\""
|
cmd &= " -g \"" & tags.join(",") & "\""
|
||||||
cmd &= " -n \"pit-id: " & $issue.id & "\""
|
cmd &= " -n \"pit-id: " & $issue.id & "\""
|
||||||
cmd &= " \"" & issue.summary & "\""
|
cmd &= " \"[" & ($issue.id)[0..<6] & "] " & issue.summary & "\""
|
||||||
discard execShellCmd(cmd)
|
discard execShellCmd(cmd)
|
||||||
elif targetState == Done or targetState == Pending:
|
elif targetState == Done or targetState == Pending:
|
||||||
discard execShellCmd("ptk stop")
|
discard execShellCmd("ptk stop")
|
||||||
@ -474,6 +251,7 @@ when isMainModule:
|
|||||||
issue.setDateTime("hide-until", parseDate($args["<date>"]))
|
issue.setDateTime("hide-until", parseDate($args["<date>"]))
|
||||||
|
|
||||||
issue.store()
|
issue.store()
|
||||||
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
elif args["delegate"]:
|
elif args["delegate"]:
|
||||||
|
|
||||||
@ -481,6 +259,7 @@ when isMainModule:
|
|||||||
issue["delegated-to"] = $args["<delegated-to>"]
|
issue["delegated-to"] = $args["<delegated-to>"]
|
||||||
|
|
||||||
issue.store()
|
issue.store()
|
||||||
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
elif args["delete"] or args["rm"]:
|
elif args["delete"] or args["rm"]:
|
||||||
for id in @(args["<id>"]):
|
for id in @(args["<id>"]):
|
||||||
@ -493,6 +272,7 @@ when isMainModule:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
issue.delete
|
issue.delete
|
||||||
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
elif args["list"]:
|
elif args["list"]:
|
||||||
|
|
||||||
@ -540,10 +320,12 @@ when isMainModule:
|
|||||||
filter.exclProperties.del("context")
|
filter.exclProperties.del("context")
|
||||||
|
|
||||||
var listContexts = false
|
var listContexts = false
|
||||||
|
var listTags = false
|
||||||
var statesOption = none(seq[IssueState])
|
var statesOption = none(seq[IssueState])
|
||||||
var issueIdsOption = none(seq[string])
|
var issueIdsOption = none(seq[string])
|
||||||
|
|
||||||
if args["contexts"]: listContexts = true
|
if args["contexts"]: listContexts = true
|
||||||
|
elif args["tags"]: listTags = true
|
||||||
elif args["<stateOrId>"]:
|
elif args["<stateOrId>"]:
|
||||||
try:
|
try:
|
||||||
statesOption =
|
statesOption =
|
||||||
@ -569,6 +351,21 @@ when isMainModule:
|
|||||||
for c in uniqContexts.sorted:
|
for c in uniqContexts.sorted:
|
||||||
stdout.writeLine(c.alignLeft(maxLen+2) & ctx.getIssueContextDisplayName(c))
|
stdout.writeLine(c.alignLeft(maxLen+2) & ctx.getIssueContextDisplayName(c))
|
||||||
|
|
||||||
|
elif listTags:
|
||||||
|
var uniqTags = newseq[string]()
|
||||||
|
if statesOption.isSome:
|
||||||
|
for state in statesOption.get: ctx.loadIssues(state)
|
||||||
|
else: ctx.loadAllIssues()
|
||||||
|
|
||||||
|
if filterOption.isSome: ctx.filterIssues(filterOption.get)
|
||||||
|
|
||||||
|
for state, issueList in ctx.issues:
|
||||||
|
for issue in issueList:
|
||||||
|
for tag in issue.tags:
|
||||||
|
if not uniqTags.contains(tag): uniqTags.add(tag)
|
||||||
|
|
||||||
|
stdout.writeLine(uniqTags.sorted.join("\n"))
|
||||||
|
|
||||||
# List a specific issue
|
# List a specific issue
|
||||||
elif issueIdsOption.isSome:
|
elif issueIdsOption.isSome:
|
||||||
for issueId in issueIdsOption.get:
|
for issueId in issueIdsOption.get:
|
||||||
@ -598,6 +395,7 @@ when isMainModule:
|
|||||||
finally: close(propIn)
|
finally: close(propIn)
|
||||||
|
|
||||||
issue.store()
|
issue.store()
|
||||||
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
elif args["get-binary-property"]:
|
elif args["get-binary-property"]:
|
||||||
let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"]))
|
let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"]))
|
||||||
@ -629,6 +427,24 @@ when isMainModule:
|
|||||||
if issuePaths.len < 2: continue
|
if issuePaths.len < 2: continue
|
||||||
stdout.writeLine(issueId & ":\p " & issuePaths.join("\p ") & "\p\p")
|
stdout.writeLine(issueId & ":\p " & issuePaths.join("\p ") & "\p\p")
|
||||||
|
|
||||||
|
elif args["sync"]:
|
||||||
|
if ctx.cfg.syncTargets.len == 0:
|
||||||
|
info "No sync targets configured"
|
||||||
|
|
||||||
|
for syncTarget in ctx.cfg.syncTargets:
|
||||||
|
let syncCtx = initSyncContext(ctx.cfg, syncTarget)
|
||||||
|
|
||||||
|
sync(syncCtx, args["--dry-run"])
|
||||||
|
|
||||||
|
# after doing stuff, sync if auto-sync is requested
|
||||||
|
if ctx.cfg.autoSync:
|
||||||
|
for syncTarget in ctx.cfg.syncTargets:
|
||||||
|
let syncCtx = initSyncContext(ctx.cfg, syncTarget)
|
||||||
|
if anyIt(
|
||||||
|
updatedIssues,
|
||||||
|
it.hasProp("context") and it["context"] == syncCtx.issueContext):
|
||||||
|
sync(syncCtx, false)
|
||||||
|
|
||||||
except CatchableError:
|
except CatchableError:
|
||||||
fatal getCurrentExceptionMsg()
|
fatal getCurrentExceptionMsg()
|
||||||
debug getCurrentException().getStackTrace()
|
debug getCurrentException().getStackTrace()
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
const PIT_VERSION* = "4.24.0"
|
const PIT_VERSION* = "4.29.1"
|
||||||
|
|
||||||
const USAGE* = """Usage:
|
const USAGE* = """Usage:
|
||||||
pit ( new | add) <summary> [<state>] [options]
|
pit ( new | add) <summary> [<state>] [options]
|
||||||
pit list contexts [options]
|
pit list contexts [options]
|
||||||
|
pit list tags [options]
|
||||||
pit list [<stateOrId>...] [options]
|
pit list [<stateOrId>...] [options]
|
||||||
pit ( start | done | pending | todo-today | todo | suspend ) <id>... [options]
|
pit ( start | done | pending | todo-today | todo | suspend ) <id>... [options]
|
||||||
pit edit <ref>... [options]
|
pit edit <ref>... [options]
|
||||||
@ -15,6 +16,7 @@ const USAGE* = """Usage:
|
|||||||
pit add-binary-property <id> <propName> <propSource> [options]
|
pit add-binary-property <id> <propName> <propSource> [options]
|
||||||
pit get-binary-property <id> <propName> <propDest> [options]
|
pit get-binary-property <id> <propName> <propDest> [options]
|
||||||
pit show-dupes
|
pit show-dupes
|
||||||
|
pit sync [<syncTarget>...] [options]
|
||||||
pit help [options]
|
pit help [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@ -73,11 +75,15 @@ Options:
|
|||||||
-d, --tasks-dir Path to the tasks directory (defaults to the value
|
-d, --tasks-dir Path to the tasks directory (defaults to the value
|
||||||
configured in the .pitrc file)
|
configured in the .pitrc file)
|
||||||
|
|
||||||
--term-width <width> Manually set the terminal width to use.
|
|
||||||
|
|
||||||
--ptk Enable PTK integration for this command.
|
--ptk Enable PTK integration for this command.
|
||||||
|
|
||||||
--debug Enable debug-level log output.
|
--debug Enable debug-level log output.
|
||||||
|
|
||||||
|
--dry-run Currently only supported by the `sync` command:
|
||||||
|
only print the changes that would be made, but do
|
||||||
|
not actually make them.
|
||||||
|
|
||||||
|
-s, --silent Suppress all logging and status output.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
const ONLINE_HELP* = """Issue States:
|
const ONLINE_HELP* = """Issue States:
|
||||||
@ -101,7 +107,9 @@ Issue Properties:
|
|||||||
created
|
created
|
||||||
|
|
||||||
If present, expected to be an ISO 8601-formatted date that represents the
|
If present, expected to be an ISO 8601-formatted date that represents the
|
||||||
time when the issue was created.
|
time when the issue was created. E.g.:
|
||||||
|
|
||||||
|
created: 2023-07-13T13:28:41-05:00
|
||||||
|
|
||||||
completed
|
completed
|
||||||
|
|
||||||
@ -110,12 +118,16 @@ Issue Properties:
|
|||||||
property automatically when you use the "done" command, and can filter on
|
property automatically when you use the "done" command, and can filter on
|
||||||
this value.
|
this value.
|
||||||
|
|
||||||
|
completed: 2023-04-27T11:52:28-05:00
|
||||||
|
|
||||||
context
|
context
|
||||||
|
|
||||||
Allows issues to be organized into contexts. The -c option is short-hand
|
Allows issues to be organized into contexts. The -c option is short-hand
|
||||||
for '-p context:<context-name>' and the 'list contexts' command will show
|
for '-p context:<context-name>' and the 'list contexts' command will show
|
||||||
all values of 'context' set in existing issues.
|
all values of 'context' set in existing issues.
|
||||||
|
|
||||||
|
context: family
|
||||||
|
|
||||||
delegated-to
|
delegated-to
|
||||||
|
|
||||||
When an issue now belongs to someone else, but needs to be monitored for
|
When an issue now belongs to someone else, but needs to be monitored for
|
||||||
@ -123,17 +135,23 @@ Issue Properties:
|
|||||||
note how it has been delegated. When present PIT will prepend this value
|
note how it has been delegated. When present PIT will prepend this value
|
||||||
to the issue summary with an accent color.
|
to the issue summary with an accent color.
|
||||||
|
|
||||||
|
delegated-to: Bob Ross
|
||||||
|
|
||||||
hide-until
|
hide-until
|
||||||
|
|
||||||
When present, expected to be an ISO 8601-formatted date and used to
|
When present, expected to be an ISO 8601-formatted date and used to
|
||||||
supress the display of the issue until on or after the given date.
|
supress the display of the issue until on or after the given date.
|
||||||
|
|
||||||
|
hide-until: 2024-01-01T13:45:00-05:00
|
||||||
|
|
||||||
pending
|
pending
|
||||||
|
|
||||||
When an issue is blocked by a third party, this property can be used to
|
When an issue is blocked by a third party, this property can be used to
|
||||||
capture details about the dependency When present PIT will display this
|
capture details about the dependency When present PIT will display this
|
||||||
value after the issue summary.
|
value after the issue summary.
|
||||||
|
|
||||||
|
pending: Results of WCAG analysis.
|
||||||
|
|
||||||
recurrence
|
recurrence
|
||||||
|
|
||||||
When an issue is moved to the "done" state, if the issue has a valid
|
When an issue is moved to the "done" state, if the issue has a valid
|
||||||
@ -144,7 +162,7 @@ Issue Properties:
|
|||||||
A valid recurrence value has a time value and optionally has an source
|
A valid recurrence value has a time value and optionally has an source
|
||||||
issue ID. For example:
|
issue ID. For example:
|
||||||
|
|
||||||
every 5 days, 10a544
|
recurrence: every 5 days, 10a544
|
||||||
|
|
||||||
The first word, "every", is expected to be either "every" or "after".
|
The first word, "every", is expected to be either "every" or "after".
|
||||||
|
|
||||||
@ -162,12 +180,12 @@ Issue Properties:
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
every day
|
every day
|
||||||
every 2 days
|
every 2 days
|
||||||
after 2 days
|
after 2 days
|
||||||
every week
|
every week
|
||||||
after 12 hours
|
after 12 hours
|
||||||
every 2 weeks, 10a544
|
every 2 weeks, 10a544
|
||||||
|
|
||||||
tags
|
tags
|
||||||
|
|
251
src/pit/formatting.nim
Normal file
251
src/pit/formatting.nim
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import std/[options, sequtils, wordwrap, tables, terminal, times, unicode, wordwrap]
|
||||||
|
import cliutils, uuids
|
||||||
|
import std/strutils except alignLeft, capitalize, strip, toLower, toUpper
|
||||||
|
import ./libpit
|
||||||
|
|
||||||
|
proc adjustedTerminalWidth(): int = min(terminalWidth(), 80)
|
||||||
|
|
||||||
|
proc getIssueContextDisplayName*(ctx: CliContext, context: string): string =
|
||||||
|
if not ctx.contexts.hasKey(context):
|
||||||
|
if context.isEmptyOrWhitespace: return "<default>"
|
||||||
|
else: return context.capitalize()
|
||||||
|
return ctx.contexts[context]
|
||||||
|
|
||||||
|
|
||||||
|
proc formatIssue*(issue: Issue): string =
|
||||||
|
result = ($issue.id).withColor(fgBlack, true) & "\n"&
|
||||||
|
issue.summary.withColor(fgWhite) & "\n"
|
||||||
|
|
||||||
|
if issue.tags.len > 0:
|
||||||
|
result &= "tags: ".withColor(fgMagenta) &
|
||||||
|
issue.tags.join(",").withColor(fgGreen, true) & "\n"
|
||||||
|
|
||||||
|
if issue.properties.len > 0:
|
||||||
|
result &= termColor(fgMagenta)
|
||||||
|
for k, v in issue.properties: result &= k & ": " & v & "\n"
|
||||||
|
|
||||||
|
|
||||||
|
result &= "--------".withColor(fgBlack, true) & "\n"
|
||||||
|
if not issue.details.isEmptyOrWhitespace:
|
||||||
|
result &= issue.details.strip.withColor(fgCyan) & "\n"
|
||||||
|
|
||||||
|
result &= termReset
|
||||||
|
|
||||||
|
|
||||||
|
proc formatPlainIssueSummary*(issue: Issue): string =
|
||||||
|
|
||||||
|
result = "$#: $# $#" % [
|
||||||
|
$issue.state,
|
||||||
|
($issue.id)[0..<6],
|
||||||
|
issue.summary ]
|
||||||
|
|
||||||
|
if issue.hasProp("delegated-to") or issue.hasProp("pending"):
|
||||||
|
var parts = newSeq[string]()
|
||||||
|
|
||||||
|
if issue.hasProp("delegated-to"):
|
||||||
|
parts.add("delegated to " & issue["delegated-to"])
|
||||||
|
|
||||||
|
if issue.hasProp("pending"):
|
||||||
|
parts.add("pendin: " & issue["pending"])
|
||||||
|
|
||||||
|
result &= "($#)" % [ parts.join("; ") ]
|
||||||
|
|
||||||
|
|
||||||
|
proc formatSectionIssue*(
|
||||||
|
issue: Issue,
|
||||||
|
width: int = 80,
|
||||||
|
indent = "",
|
||||||
|
verbose = false): string =
|
||||||
|
|
||||||
|
result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true) & " "
|
||||||
|
|
||||||
|
let showDetails = not issue.details.isEmptyOrWhitespace and verbose
|
||||||
|
|
||||||
|
let summaryIndentLen = indent.len + 7
|
||||||
|
let summaryWidth = width - summaryIndentLen
|
||||||
|
|
||||||
|
let summaryLines = issue.summary
|
||||||
|
.wrapWords(summaryWidth)
|
||||||
|
.splitLines
|
||||||
|
|
||||||
|
result &= summaryLines[0].withColor(fgWhite)
|
||||||
|
|
||||||
|
for line in summaryLines[1..^1]:
|
||||||
|
result &= "\p" & line.indent(summaryIndentLen)
|
||||||
|
|
||||||
|
var lastLineLen = summaryLines[^1].len
|
||||||
|
|
||||||
|
if issue.hasProp("delegated-to"):
|
||||||
|
if lastLineLen + issue["delegated-to"].len + 1 < summaryWidth:
|
||||||
|
result &= " " & issue["delegated-to"].withColor(fgMagenta)
|
||||||
|
lastLineLen += issue["delegated-to"].len + 1
|
||||||
|
else:
|
||||||
|
result &= "\p" & issue["delegated-to"]
|
||||||
|
.withColor(fgMagenta)
|
||||||
|
.indent(summaryIndentLen)
|
||||||
|
lastLineLen = issue["delegated-to"].len
|
||||||
|
|
||||||
|
if issue.tags.len > 0:
|
||||||
|
let tagsStrLines = ("(" & issue.tags.join(", ") & ")")
|
||||||
|
.wrapWords(summaryWidth)
|
||||||
|
.splitLines
|
||||||
|
|
||||||
|
if tagsStrLines.len == 1 and
|
||||||
|
(lastLineLen + tagsStrLines[0].len + 1) < summaryWidth:
|
||||||
|
result &= " " & tagsStrLines[0].withColor(fgGreen)
|
||||||
|
lastLineLen += tagsStrLines[0].len + 1
|
||||||
|
else:
|
||||||
|
result &= "\p" & tagsStrLines
|
||||||
|
.mapIt(it.indent(summaryIndentLen))
|
||||||
|
.join("\p")
|
||||||
|
.withColor(fgGreen)
|
||||||
|
lastLineLen = tagsStrLines[^1].len
|
||||||
|
|
||||||
|
if issue.hasProp("pending"):
|
||||||
|
result &= "\p" & ("Pending: " & issue["pending"])
|
||||||
|
.wrapwords(summaryWidth)
|
||||||
|
.withColor(fgCyan)
|
||||||
|
.indent(summaryIndentLen)
|
||||||
|
|
||||||
|
if showDetails:
|
||||||
|
result &= "\p" & issue.details
|
||||||
|
.strip
|
||||||
|
.withColor(fgBlack, bright = true)
|
||||||
|
.indent(summaryIndentLen)
|
||||||
|
|
||||||
|
result &= termReset
|
||||||
|
|
||||||
|
|
||||||
|
proc formatSectionIssueList*(
|
||||||
|
issues: seq[Issue],
|
||||||
|
width: int = 80,
|
||||||
|
indent: string = "",
|
||||||
|
verbose: bool = false): string =
|
||||||
|
|
||||||
|
result = ""
|
||||||
|
for i in issues:
|
||||||
|
var issueText = formatSectionIssue(i, width, indent, verbose)
|
||||||
|
result &= issueText & "\n"
|
||||||
|
|
||||||
|
|
||||||
|
proc formatSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
|
||||||
|
indent = "", verbose = false): string =
|
||||||
|
let innerWidth = adjustedTerminalWidth() - (indent.len * 2)
|
||||||
|
|
||||||
|
result = termColor(fgBlue) &
|
||||||
|
(indent & ".".repeat(innerWidth)) & "\n" &
|
||||||
|
state.displayName.center(adjustedTerminalWidth()) & "\n\n" &
|
||||||
|
termReset
|
||||||
|
|
||||||
|
let issuesByContext = issues.groupBy("context")
|
||||||
|
|
||||||
|
if issues.len > 5 and issuesByContext.len > 1:
|
||||||
|
for context, ctxIssues in issuesByContext:
|
||||||
|
|
||||||
|
result &= termColor(fgYellow) &
|
||||||
|
indent & ctx.getIssueContextDisplayName(context) & ":" &
|
||||||
|
termReset & "\n\n"
|
||||||
|
|
||||||
|
result &= formatSectionIssueList(ctxIssues, innerWidth - 2, indent & " ", verbose)
|
||||||
|
result &= "\n"
|
||||||
|
|
||||||
|
else: result &= formatSectionIssueList(issues, innerWidth, indent, verbose)
|
||||||
|
|
||||||
|
|
||||||
|
proc writeHeader*(ctx: CliContext, header: string) =
|
||||||
|
stdout.setForegroundColor(fgRed, true)
|
||||||
|
stdout.writeLine('_'.repeat(adjustedTerminalWidth()))
|
||||||
|
stdout.writeLine(header.center(adjustedTerminalWidth()))
|
||||||
|
stdout.writeLine('~'.repeat(adjustedTerminalWidth()))
|
||||||
|
stdout.resetAttributes
|
||||||
|
|
||||||
|
|
||||||
|
proc list*(
|
||||||
|
ctx: CliContext,
|
||||||
|
filter: Option[IssueFilter],
|
||||||
|
states: Option[seq[IssueState]],
|
||||||
|
showToday = false,
|
||||||
|
showFuture = false,
|
||||||
|
showHidden = false,
|
||||||
|
verbose: bool) =
|
||||||
|
|
||||||
|
if states.isSome:
|
||||||
|
trace "listing issues for " & $states.get
|
||||||
|
for state in states.get:
|
||||||
|
ctx.loadIssues(state)
|
||||||
|
if filter.isSome: ctx.filterIssues(filter.get)
|
||||||
|
|
||||||
|
# Show Done for just today if requested
|
||||||
|
if state == Done and showToday:
|
||||||
|
ctx.issues[Done] = ctx.issues[Done].filterIt(
|
||||||
|
it.hasProp("completed") and
|
||||||
|
sameDay(getTime().local, it.getDateTime("completed")))
|
||||||
|
|
||||||
|
if isatty(stdout):
|
||||||
|
stdout.write ctx.formatSection(ctx.issues[state], state, "", verbose)
|
||||||
|
|
||||||
|
else:
|
||||||
|
stdout.writeLine ctx.issues[state]
|
||||||
|
.mapIt(formatPlainIssueSummary(it))
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
|
||||||
|
trace "listing complete"
|
||||||
|
return
|
||||||
|
|
||||||
|
ctx.loadOpenIssues()
|
||||||
|
if filter.isSome:
|
||||||
|
ctx.filterIssues(filter.get)
|
||||||
|
trace "filtered issues"
|
||||||
|
|
||||||
|
let today = showToday and [Current, TodoToday, Pending].anyIt(
|
||||||
|
ctx.issues.hasKey(it) and ctx.issues[it].len > 0)
|
||||||
|
|
||||||
|
let future = showFuture and [Pending, Todo].anyIt(
|
||||||
|
ctx.issues.hasKey(it) and ctx.issues[it].len > 0)
|
||||||
|
|
||||||
|
let indent = if today and future: " " else: ""
|
||||||
|
|
||||||
|
# Today's items
|
||||||
|
if today:
|
||||||
|
if future: ctx.writeHeader("Today")
|
||||||
|
|
||||||
|
for s in [Current, TodoToday, Pending]:
|
||||||
|
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
|
||||||
|
let visibleIssues = ctx.issues[s].filterIt(
|
||||||
|
showHidden or
|
||||||
|
not (it.hasProp("hide-until") and
|
||||||
|
it.getDateTime("hide-until") > getTime().local))
|
||||||
|
|
||||||
|
if isatty(stdout):
|
||||||
|
stdout.write ctx.formatSection(visibleIssues, s, indent, verbose)
|
||||||
|
|
||||||
|
else:
|
||||||
|
stdout.writeLine visibleIssues
|
||||||
|
.mapIt(formatPlainIssueSummary(it))
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
# Future items
|
||||||
|
if future:
|
||||||
|
if today: ctx.writeHeader("Future")
|
||||||
|
|
||||||
|
let futureCategories =
|
||||||
|
if showToday: @[Todo]
|
||||||
|
else: @[Pending, Todo]
|
||||||
|
|
||||||
|
for s in futureCategories:
|
||||||
|
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
|
||||||
|
let visibleIssues = ctx.issues[s].filterIt(
|
||||||
|
showHidden or
|
||||||
|
not (it.hasProp("hide-until") and
|
||||||
|
it.getDateTime("hide-until") > getTime().local))
|
||||||
|
|
||||||
|
if isatty(stdout):
|
||||||
|
stdout.write ctx.formatSection(visibleIssues, s, indent, verbose)
|
||||||
|
|
||||||
|
else:
|
||||||
|
stdout.writeLine visibleIssues
|
||||||
|
.mapIt(formatPlainIssueSummary(it))
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
trace "listing complete"
|
@ -33,13 +33,23 @@ type
|
|||||||
PitConfig* = ref object
|
PitConfig* = ref object
|
||||||
tasksDir*: string
|
tasksDir*: string
|
||||||
contexts*: TableRef[string, string]
|
contexts*: TableRef[string, string]
|
||||||
|
autoSync*: bool
|
||||||
|
syncTargets*: seq[JsonNode]
|
||||||
cfg*: CombinedConfig
|
cfg*: CombinedConfig
|
||||||
|
|
||||||
|
CliContext* = ref object
|
||||||
|
cfg*: PitConfig
|
||||||
|
contexts*: TableRef[string, string]
|
||||||
|
defaultContext*: Option[string]
|
||||||
|
issues*: TableRef[IssueState, seq[Issue]]
|
||||||
|
triggerPtk*, verbose*: bool
|
||||||
|
|
||||||
Recurrence* = object
|
Recurrence* = object
|
||||||
cloneId*: Option[string]
|
cloneId*: Option[string]
|
||||||
interval*: TimeInterval
|
interval*: TimeInterval
|
||||||
isFromCompletion*: bool
|
isFromCompletion*: bool
|
||||||
|
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
@ -68,6 +78,11 @@ proc displayName*(s: IssueState): string =
|
|||||||
of Todo: result = "Todo"
|
of Todo: result = "Todo"
|
||||||
of TodoToday: result = "Todo"
|
of TodoToday: result = "Todo"
|
||||||
|
|
||||||
|
|
||||||
|
proc sameDay*(a, b: DateTime): bool =
|
||||||
|
result = a.year == b.year and a.yearday == b.yearday
|
||||||
|
|
||||||
|
|
||||||
## Allow issue properties to be accessed as if the issue was a table
|
## Allow issue properties to be accessed as if the issue was a table
|
||||||
proc `[]`*(issue: Issue, key: string): string =
|
proc `[]`*(issue: Issue, key: string): string =
|
||||||
return issue.properties[key]
|
return issue.properties[key]
|
||||||
@ -75,6 +90,7 @@ proc `[]`*(issue: Issue, key: string): string =
|
|||||||
proc `[]=`*(issue: Issue, key: string, value: string) =
|
proc `[]=`*(issue: Issue, key: string, value: string) =
|
||||||
issue.properties[key] = value
|
issue.properties[key] = value
|
||||||
|
|
||||||
|
|
||||||
## Issue property accessors
|
## Issue property accessors
|
||||||
proc hasProp*(issue: Issue, key: string): bool =
|
proc hasProp*(issue: Issue, key: string): bool =
|
||||||
return issue.properties.hasKey(key)
|
return issue.properties.hasKey(key)
|
||||||
@ -113,6 +129,7 @@ proc getRecurrence*(issue: Issue): Option[Recurrence] =
|
|||||||
else: weeks(1),
|
else: weeks(1),
|
||||||
cloneId: c[6]))
|
cloneId: c[6]))
|
||||||
|
|
||||||
|
|
||||||
## Issue filtering
|
## Issue filtering
|
||||||
proc initFilter*(): IssueFilter =
|
proc initFilter*(): IssueFilter =
|
||||||
result = IssueFilter(
|
result = IssueFilter(
|
||||||
@ -173,6 +190,7 @@ proc parseDate*(d: string): DateTime =
|
|||||||
continue
|
continue
|
||||||
raise newException(ValueError, "Unable to parse input as a date: " & d & errMsg)
|
raise newException(ValueError, "Unable to parse input as a date: " & d & errMsg)
|
||||||
|
|
||||||
|
|
||||||
## Parse and format issues
|
## Parse and format issues
|
||||||
proc fromStorageFormat*(id: string, issueTxt: string): Issue =
|
proc fromStorageFormat*(id: string, issueTxt: string): Issue =
|
||||||
type ParseState = enum ReadingSummary, ReadingProps, ReadingDetails
|
type ParseState = enum ReadingSummary, ReadingProps, ReadingDetails
|
||||||
@ -238,6 +256,7 @@ proc toStorageFormat*(issue: Issue, withComments = false): string =
|
|||||||
|
|
||||||
result = lines.join("\n")
|
result = lines.join("\n")
|
||||||
|
|
||||||
|
|
||||||
## Load and store from filesystem
|
## Load and store from filesystem
|
||||||
proc loadIssue*(filePath: string): Issue =
|
proc loadIssue*(filePath: string): Issue =
|
||||||
result = fromStorageFormat(splitFile(filePath).name, readFile(filePath))
|
result = fromStorageFormat(splitFile(filePath).name, readFile(filePath))
|
||||||
@ -328,11 +347,16 @@ proc loadAllIssues*(tasksDir: string): TableRef[IssueState, seq[Issue]] =
|
|||||||
for state in IssueState: result[state] = tasksDir.loadIssues(state)
|
for state in IssueState: result[state] = tasksDir.loadIssues(state)
|
||||||
|
|
||||||
proc changeState*(issue: Issue, tasksDir: string, newState: IssueState) =
|
proc changeState*(issue: Issue, tasksDir: string, newState: IssueState) =
|
||||||
|
var dbgInfo = "[$#] changing state: $# → $#" %
|
||||||
|
[ ($issue.id)[0..<6], $issue.state, $newState ]
|
||||||
|
|
||||||
let oldFilepath = issue.filepath
|
let oldFilepath = issue.filepath
|
||||||
if newState == Done: issue.setDateTime("completed", getTime().local)
|
if newState == Done: issue.setDateTime("completed", getTime().local)
|
||||||
tasksDir.store(issue, newState)
|
tasksDir.store(issue, newState)
|
||||||
if oldFilePath != issue.filepath: removeFile(oldFilepath)
|
if oldFilePath != issue.filepath: removeFile(oldFilepath)
|
||||||
|
dbgInfo &= "\n\told path: $#\n\tnew path: $#" % [oldFilePath, issue.filepath]
|
||||||
issue.state = newState
|
issue.state = newState
|
||||||
|
debug dbgInfo
|
||||||
|
|
||||||
proc delete*(issue: Issue) = removeFile(issue.filepath)
|
proc delete*(issue: Issue) = removeFile(issue.filepath)
|
||||||
|
|
||||||
@ -368,6 +392,7 @@ proc nextRecurrence*(tasksDir: string, rec: Recurrence, defaultIssue: Issue): Is
|
|||||||
|
|
||||||
result.setDateTime("hide-until", nextTime)
|
result.setDateTime("hide-until", nextTime)
|
||||||
|
|
||||||
|
|
||||||
## Utilities for working with issue collections.
|
## Utilities for working with issue collections.
|
||||||
proc filter*(issues: seq[Issue], filter: IssueFilter): seq[Issue] =
|
proc filter*(issues: seq[Issue], filter: IssueFilter): seq[Issue] =
|
||||||
var f: seq[Issue] = issues
|
var f: seq[Issue] = issues
|
||||||
@ -411,6 +436,7 @@ proc find*(
|
|||||||
result = @[]
|
result = @[]
|
||||||
for stateIssues in issues.values: result &= stateIssues.filter(filter)
|
for stateIssues in issues.values: result &= stateIssues.filter(filter)
|
||||||
|
|
||||||
|
|
||||||
### Configuration utilities
|
### Configuration utilities
|
||||||
proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitConfig =
|
proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitConfig =
|
||||||
var pitrcFilename: string
|
var pitrcFilename: string
|
||||||
@ -434,8 +460,10 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitCo
|
|||||||
|
|
||||||
result = PitConfig(
|
result = PitConfig(
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
autoSync: parseBool(cfg.getVal("auto-sync", "false")),
|
||||||
contexts: newTable[string,string](),
|
contexts: newTable[string,string](),
|
||||||
tasksDir: cfg.getVal("tasks-dir", ""))
|
tasksDir: cfg.getVal("tasks-dir", ""),
|
||||||
|
syncTargets: cfg.getJson("sync-targets", newJArray()).getElems)
|
||||||
|
|
||||||
for k, v in cfg.getJson("contexts", newJObject()):
|
for k, v in cfg.getJson("contexts", newJObject()):
|
||||||
result.contexts[k] = v.getStr()
|
result.contexts[k] = v.getStr()
|
||||||
@ -450,3 +478,38 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitCo
|
|||||||
for s in IssueState:
|
for s in IssueState:
|
||||||
if not dirExists(result.tasksDir / $s):
|
if not dirExists(result.tasksDir / $s):
|
||||||
(result.tasksDir / $s).createDir
|
(result.tasksDir / $s).createDir
|
||||||
|
|
||||||
|
|
||||||
|
## CliContext functionality
|
||||||
|
proc initContext*(args: Table[string, Value]): CliContext =
|
||||||
|
let pitCfg = loadConfig(args)
|
||||||
|
|
||||||
|
let cliJson =
|
||||||
|
if pitCfg.cfg.json.hasKey("cli"): pitCfg.cfg.json["cli"]
|
||||||
|
else: newJObject()
|
||||||
|
|
||||||
|
let cliCfg = CombinedConfig(docopt: args, json: cliJson)
|
||||||
|
|
||||||
|
result = CliContext(
|
||||||
|
cfg: pitCfg,
|
||||||
|
contexts: pitCfg.contexts,
|
||||||
|
defaultContext:
|
||||||
|
if not cliJson.hasKey("defaultContext"): none(string)
|
||||||
|
else: some(cliJson["defaultContext"].getStr()),
|
||||||
|
verbose: parseBool(cliCfg.getVal("verbose", "false")) and not args["--quiet"],
|
||||||
|
issues: newTable[IssueState, seq[Issue]](),
|
||||||
|
triggerPtk: cliJson.getOrDefault("triggerPtk").getBool(false))
|
||||||
|
|
||||||
|
proc loadIssues*(ctx: CliContext, state: IssueState) =
|
||||||
|
ctx.issues[state] = loadIssues(ctx.cfg.tasksDir, state)
|
||||||
|
|
||||||
|
proc loadOpenIssues*(ctx: CliContext) =
|
||||||
|
ctx.issues = newTable[IssueState, seq[Issue]]()
|
||||||
|
for state in [Current, TodoToday, Todo, Pending, Todo]: ctx.loadIssues(state)
|
||||||
|
|
||||||
|
proc loadAllIssues*(ctx: CliContext) =
|
||||||
|
ctx.issues = ctx.cfg.tasksDir.loadAllIssues()
|
||||||
|
|
||||||
|
proc filterIssues*(ctx: CliContext, filter: IssueFilter) =
|
||||||
|
for state, issueList in ctx.issues:
|
||||||
|
ctx.issues[state] = issueList.filter(filter)
|
179
src/pit/sync_pbm_vsb.nim
Normal file
179
src/pit/sync_pbm_vsb.nim
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import std/[httpclient, json, jsonutils, logging, options, sets, strutils,
|
||||||
|
terminal, times, tables]
|
||||||
|
import timeutils, uuids, zero_functional
|
||||||
|
import ./formatting, ./libpit
|
||||||
|
|
||||||
|
type
|
||||||
|
PbmVsbSyncContext* = object
|
||||||
|
apiBaseUrl*: string
|
||||||
|
apiToken*: string
|
||||||
|
issueContext*: string
|
||||||
|
pit: PitConfig
|
||||||
|
http: HttpClient
|
||||||
|
|
||||||
|
ServerTask* = object
|
||||||
|
id*: string
|
||||||
|
summary*: string
|
||||||
|
details*: string
|
||||||
|
state*: string
|
||||||
|
lastUpdatedAt*: DateTime
|
||||||
|
archivedAt*: Option[DateTime]
|
||||||
|
tags*: seq[string]
|
||||||
|
parent*: Option[string]
|
||||||
|
priority*: Option[string]
|
||||||
|
project*: Option[string]
|
||||||
|
milestone*: Option[string]
|
||||||
|
hideUntil*: Option[DateTime]
|
||||||
|
|
||||||
|
|
||||||
|
proc `%`*(dt: DateTime): JsonNode = %(dt.formatIso8601)
|
||||||
|
func toJsonHook(dt: DateTime): JsonNode = %(dt.formatIso8601)
|
||||||
|
proc fromJsonHook(dt: var DateTime, n: JsonNode): void =
|
||||||
|
dt = n.getStr.parseIso8601
|
||||||
|
|
||||||
|
|
||||||
|
#func `%`*(p: Project): JsonNode = toJson(p)
|
||||||
|
func `%`*(t: ServerTask): JsonNode = toJson(t)
|
||||||
|
|
||||||
|
|
||||||
|
proc toServerTask(i: Issue): ServerTask =
|
||||||
|
return ServerTask(
|
||||||
|
id: $i.id,
|
||||||
|
summary: i.summary,
|
||||||
|
details: i.details,
|
||||||
|
state: $i.state,
|
||||||
|
tags: i.tags,
|
||||||
|
lastUpdatedAt:
|
||||||
|
if i.hasProp("last-updated"): parseIso8601(i["last-updated"])
|
||||||
|
else: now(),
|
||||||
|
parent:
|
||||||
|
if i.hasProp("parent"): some(i["parent"])
|
||||||
|
else: none[string](),
|
||||||
|
priority:
|
||||||
|
if i.hasProp("priority"): some(i["priority"])
|
||||||
|
else: none[string](),
|
||||||
|
project:
|
||||||
|
if i.hasProp("project"): some(i["project"])
|
||||||
|
else: none[string](),
|
||||||
|
milestone:
|
||||||
|
if i.hasProp("milestone"): some(i["milestone"])
|
||||||
|
else: none[string](),
|
||||||
|
hideUntil:
|
||||||
|
if i.hasProp("hide-until"): some(parseIso8601(i["hide-until"]))
|
||||||
|
else: none[DateTime]())
|
||||||
|
|
||||||
|
|
||||||
|
func getOrFail(n: JsonNode, k: string): JsonNode =
|
||||||
|
if not n.hasKey(k):
|
||||||
|
raise newException(ValueError, "missing key '" & k & "'")
|
||||||
|
return n[k]
|
||||||
|
|
||||||
|
|
||||||
|
proc initSyncContext*(pit: PitConfig, syncConfig: JsonNode): PbmVsbSyncContext =
|
||||||
|
result.pit = pit
|
||||||
|
result.apiBaseUrl = syncConfig.getOrFail("apiBaseUrl").getStr
|
||||||
|
result.apiToken = syncConfig.getOrFail("apiToken").getStr
|
||||||
|
result.issueContext = syncConfig.getOrFail("context").getStr
|
||||||
|
result.http = newHttpClient()
|
||||||
|
result.http.headers = newHttpHeaders({ "Authorization": "Bearer " & result.apiToken })
|
||||||
|
|
||||||
|
|
||||||
|
proc fetchServerTasks*(ctx: PbmVsbSyncContext): seq[ServerTask] =
|
||||||
|
result = newSeq[ServerTask]()
|
||||||
|
let url = ctx.apiBaseUrl & "/tasks"
|
||||||
|
|
||||||
|
debug "Fetching tasks from server:\n\t" & url
|
||||||
|
let resp = ctx.http.get(url)
|
||||||
|
if resp.status != "200":
|
||||||
|
debug("Received " & resp.status & ": " & resp.body)
|
||||||
|
raise newException(Exception, "Failed to fetch tasks from server")
|
||||||
|
fromJson(result, parseJson(resp.body)["data"])
|
||||||
|
|
||||||
|
|
||||||
|
proc updateTask*(ctx: PbmVsbSyncContext, task: ServerTask): void =
|
||||||
|
let url = ctx.apiBaseUrl & "/task" & task.id
|
||||||
|
let body = %task
|
||||||
|
let resp = ctx.http.post(url, $body)
|
||||||
|
if resp.status != "200":
|
||||||
|
debug("Received " & resp.status & ": " & resp.body)
|
||||||
|
raise newException(Exception, "Failed to update task " & task.id & "on server")
|
||||||
|
|
||||||
|
|
||||||
|
proc updateTasks*(ctx: PbmVsbSyncContext, tasks: seq[ServerTask]): void =
|
||||||
|
let url = ctx.apiBaseUrl & "/tasks"
|
||||||
|
let body = %tasks
|
||||||
|
let resp = ctx.http.put(url, $body)
|
||||||
|
if resp.status != "200":
|
||||||
|
debug("Received " & resp.status & ": " & resp.body)
|
||||||
|
raise newException(Exception, "Failed to update tasks on server")
|
||||||
|
|
||||||
|
|
||||||
|
proc deleteTask*(ctx: PbmVsbSyncContext, taskId: string): void =
|
||||||
|
let url = ctx.apiBaseUrl & "/tasks/" & taskId
|
||||||
|
let resp = ctx.http.delete(url)
|
||||||
|
if resp.status != "200":
|
||||||
|
debug("Received " & resp.status & ": " & resp.body)
|
||||||
|
raise newException(Exception, "Failed to delete task " & taskId & " on server")
|
||||||
|
|
||||||
|
|
||||||
|
proc sync*(
|
||||||
|
ctx: PbmVsbSyncContext,
|
||||||
|
dryRun = true,
|
||||||
|
batchSize = 100): void =
|
||||||
|
|
||||||
|
# We're going to do a uni-directional sync, pushing local issues to the
|
||||||
|
# server. However, we only want to update issues that have changed since
|
||||||
|
# the last sync based on the *last-updated* property.
|
||||||
|
|
||||||
|
# Note that all the logic assumes the server only has one context of tasks.
|
||||||
|
# If this ever changes, this logic will need to change.
|
||||||
|
let filter = propsFilter(newTable({ "context": ctx.issueContext }))
|
||||||
|
let allIssues = ctx.pit.tasksDir.loadAllIssues()
|
||||||
|
|
||||||
|
var issues = newSeq[Issue]()
|
||||||
|
for state in allIssues.keys:
|
||||||
|
issues.add(allIssues[state].filter(filter))
|
||||||
|
|
||||||
|
let serverTasks = fetchServerTasks(ctx)
|
||||||
|
debug "Loaded $# server tasks" % [$serverTasks.len]
|
||||||
|
var issuesToPush = newSeq[ServerTask]()
|
||||||
|
|
||||||
|
var unmatchedServerTaskIds = toHashSet(serverTasks --> map(it.id))
|
||||||
|
|
||||||
|
# Process all local issues
|
||||||
|
info "Updating the following tasks for context " & ctx.issueContext
|
||||||
|
for lentIssue in issues:
|
||||||
|
let i = lentIssue
|
||||||
|
let foundTask = serverTasks --> find(it.id == $i.id)
|
||||||
|
|
||||||
|
if foundTask.isSome:
|
||||||
|
# There is a server task for this issue
|
||||||
|
unmatchedServerTaskIds.excl(foundTask.get.id)
|
||||||
|
|
||||||
|
if i.hasProp("last-updated"):
|
||||||
|
var localUpdate = parseIso8601(i["last-updated"])
|
||||||
|
localUpdate.nanosecond = 0
|
||||||
|
if foundTask.get.lastUpdatedAt >= localUpdate:
|
||||||
|
# but we don't have any update
|
||||||
|
continue
|
||||||
|
|
||||||
|
# We fell through the conditional block above, so either there isn't a
|
||||||
|
# server task, or we *do* have an update,
|
||||||
|
issuesToPush.add(toServerTask(i))
|
||||||
|
info " " & formatSectionIssue(i, width = terminalWidth() - 8)
|
||||||
|
|
||||||
|
# Now archive the issues on the server that we didn't see locally.
|
||||||
|
info "Archiving the following tasks for context " & ctx.issueContext
|
||||||
|
for task in serverTasks:
|
||||||
|
var toArchive = task
|
||||||
|
if unmatchedServerTaskIds.contains(task.id) and
|
||||||
|
toArchive.archivedAt.isNone:
|
||||||
|
toArchive.archivedAt = some(now())
|
||||||
|
issuesToPush.add(toArchive)
|
||||||
|
|
||||||
|
if not dryRun:
|
||||||
|
var offset = 0
|
||||||
|
while (offset < issuesToPush.len):
|
||||||
|
let batchSize = min(issuesToPush.len - offset, batchSize)
|
||||||
|
updateTasks(ctx, issuesToPush[offset ..< offset + batchSize])
|
||||||
|
offset += batchSize
|
@ -1,11 +1,17 @@
|
|||||||
## Personal Issue Tracker API Interface
|
## Personal Issue Tracker API Interface
|
||||||
## ====================================
|
## ====================================
|
||||||
|
#
|
||||||
|
# **NOTE** This is currently not being built as it no longer works under Nim
|
||||||
|
# 2.x due to the inability to call system calls (invoke pit via cli) in a
|
||||||
|
# gc-safe manner. It should be rewritten to use the functionality exposed by
|
||||||
|
# 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 asyncdispatch, cliutils, docopt, jester, json, logging, options, sequtils, strutils
|
||||||
import nre except toSeq
|
import nre except toSeq
|
||||||
|
|
||||||
import pitpkg/private/libpit
|
import pit/libpit
|
||||||
import pitpkg/cliconstants
|
import pit/cliconstants
|
||||||
|
|
||||||
type
|
type
|
||||||
PitApiCfg* = object
|
PitApiCfg* = object
|
||||||
|
Loading…
x
Reference in New Issue
Block a user