Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a273091dc7 | |||
| e35bd9f85a | |||
| 3d8fafd7b2 | |||
| 7d5d55d24a | |||
| 89c924bb72 | |||
| 3d1dc7512a | |||
| 1d18be9d1b | |||
| 6ceca9b009 |
@@ -1,2 +1,2 @@
|
|||||||
[tools]
|
[tools]
|
||||||
nim = "2.2.6"
|
nim = "2.2.8"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "4.30.1"
|
version = "4.33.1"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Personal issue tracker."
|
description = "Personal issue tracker."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -19,7 +19,7 @@ requires @[
|
|||||||
|
|
||||||
# Dependencies from git.jdb-software.com/jdb/nim-packages
|
# Dependencies from git.jdb-software.com/jdb/nim-packages
|
||||||
requires @[
|
requires @[
|
||||||
"cliutils >= 0.10.2",
|
"cliutils >= 0.11.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",
|
||||||
|
|||||||
186
src/pit.nim
186
src/pit.nim
@@ -1,10 +1,11 @@
|
|||||||
## Personal Issue Tracker CLI interface
|
## Personal Issue Tracker CLI interface
|
||||||
## ====================================
|
## ====================================
|
||||||
|
|
||||||
import std/[algorithm, logging, options, os, sequtils, tables, times, unicode]
|
import std/[algorithm, logging, options, os, sequtils, sets, tables, terminal,
|
||||||
import data_uri, docopt, json, timeutils, uuids
|
times, unicode]
|
||||||
|
import cliutils, data_uri, docopt, json, timeutils, uuids, zero_functional
|
||||||
|
|
||||||
from nre import re
|
from nre import match, re
|
||||||
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/[cliconstants, formatting, libpit, projects, sync_pbm_vsb]
|
||||||
|
|
||||||
@@ -18,18 +19,16 @@ 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(";"):
|
||||||
let pair = propText.split(":", 1)
|
let pair = propText.split(":", 1)
|
||||||
if pair.len == 1: result[pair[0]] = "true"
|
if pair.len == 1: result[pair[0]] = MATCH_ANY
|
||||||
else: result[pair[0]] = pair[1]
|
else: result[pair[0]] = pair[1]
|
||||||
|
|
||||||
|
|
||||||
proc parseExclPropertiesOption(propsOpt: string): TableRef[string, seq[string]] =
|
proc parseExclPropertiesOption(propsOpt: string): TableRef[string, seq[string]] =
|
||||||
result = newTable[string, seq[string]]()
|
result = newTable[string, seq[string]]()
|
||||||
for propText in propsOpt.split(";"):
|
for propText in propsOpt.split(";"):
|
||||||
let pair = propText.split(":", 1)
|
let pair = propText.split(":", 1)
|
||||||
let val =
|
if not result.hasKey(pair[0]): result[pair[0]] = @[]
|
||||||
if pair.len == 1: "true"
|
if pair.len == 2: result[pair[0]].add(pair[1])
|
||||||
else: pair[1]
|
|
||||||
if result.hasKey(pair[0]): result[pair[0]].add(val)
|
|
||||||
else: result[pair[0]] = @[val]
|
|
||||||
|
|
||||||
|
|
||||||
proc reorder(ctx: CliContext, state: IssueState) =
|
proc reorder(ctx: CliContext, state: IssueState) =
|
||||||
@@ -38,6 +37,89 @@ proc reorder(ctx: CliContext, state: IssueState) =
|
|||||||
ctx.loadIssues(state)
|
ctx.loadIssues(state)
|
||||||
discard os.execShellCmd(EDITOR & " '" & (ctx.cfg.tasksDir / $state / "order.txt") & "' </dev/tty >/dev/tty")
|
discard os.execShellCmd(EDITOR & " '" & (ctx.cfg.tasksDir / $state / "order.txt") & "' </dev/tty >/dev/tty")
|
||||||
|
|
||||||
|
|
||||||
|
proc addIssue(
|
||||||
|
ctx: CliContext,
|
||||||
|
args: Table[string, Value],
|
||||||
|
propertiesOption = none[TableRef[string, string]](),
|
||||||
|
tagsOption = none[seq[string]]()): Issue =
|
||||||
|
|
||||||
|
let state =
|
||||||
|
if args["<state>"]: parseEnum[IssueState]($args["<state>"])
|
||||||
|
else: TodoToday
|
||||||
|
|
||||||
|
var issueProps = propertiesOption.get(newTable[string,string]())
|
||||||
|
if not issueProps.hasKey("created"): issueProps["created"] = getTime().local.formatIso8601
|
||||||
|
if not issueProps.hasKey("context") and ctx.defaultContext.isSome():
|
||||||
|
stderr.writeLine("Using default context: " & ctx.defaultContext.get)
|
||||||
|
issueProps["context"] = ctx.defaultContext.get
|
||||||
|
|
||||||
|
if not args["--non-interactive"]:
|
||||||
|
# look for default properties for this context
|
||||||
|
let globalDefaultProps =
|
||||||
|
if ctx.cfg.defaultPropertiesByContext.hasKey("<all>"):
|
||||||
|
ctx.cfg.defaultPropertiesByContext["<all>"]
|
||||||
|
else: newSeq[string]()
|
||||||
|
|
||||||
|
let contextDefaultProps =
|
||||||
|
if issueProps.hasKey("context") and
|
||||||
|
ctx.cfg.defaultPropertiesByContext.hasKey(issueProps["context"]):
|
||||||
|
ctx.cfg.defaultPropertiesByContext[issueProps["context"]]
|
||||||
|
else: newSeq[string]()
|
||||||
|
|
||||||
|
let defaultProps = toOrderedSet(globalDefaultProps & contextDefaultProps)
|
||||||
|
if defaultProps.len > 0:
|
||||||
|
ctx.loadAllIssues()
|
||||||
|
if issueProps.hasKey("context"):
|
||||||
|
ctx.filterIssues(propsFilter(newTable({"context": issueProps["context"]})))
|
||||||
|
|
||||||
|
let numberRegex = re("^[0-9]+$")
|
||||||
|
for propName in defaultProps:
|
||||||
|
if not issueProps.hasKey(propName):
|
||||||
|
let allIssues: seq[seq[Issue]] = toSeq(values(ctx.issues))
|
||||||
|
let previousValues = toSeq(toHashSet(allIssues -->
|
||||||
|
flatten()
|
||||||
|
.filter(it.hasProp(propName))
|
||||||
|
.map(it[propName])))
|
||||||
|
|
||||||
|
let idxValPairs: seq[tuple[key: int, val: string]] = toSeq(pairs(previousValues))
|
||||||
|
let previousValuesDisplay: seq[string] = idxValPairs -->
|
||||||
|
map(" " & $it[0] & " - " & it[1])
|
||||||
|
|
||||||
|
stdout.write(
|
||||||
|
"Previous values for property '" & propName & "':\p" &
|
||||||
|
previousValuesDisplay.join("\p") & "\p" &
|
||||||
|
"Do you want to set a value for '" & propName & "'? " &
|
||||||
|
"You can use the numbers above to use an existing value, enter " &
|
||||||
|
"something new, or leave blank to indicate no value.\p" &
|
||||||
|
color(propName, fg=cMagenta) & ":" &
|
||||||
|
ansiEscSeq(fg=cBrightBlue) & " ")
|
||||||
|
|
||||||
|
let resp = stdin.readLine.strip
|
||||||
|
let numberResp = resp.match(numberRegex)
|
||||||
|
|
||||||
|
if numberResp.isSome:
|
||||||
|
let idx = parseInt(resp)
|
||||||
|
if idx >= 0 and idx < previousValues.len:
|
||||||
|
issueProps[propName] = previousValues[idx]
|
||||||
|
|
||||||
|
elif resp.len > 0:
|
||||||
|
issueProps[propName] = resp
|
||||||
|
|
||||||
|
stdout.writeLine(RESET_FORMATTING)
|
||||||
|
|
||||||
|
result = Issue(
|
||||||
|
id: genUUID(),
|
||||||
|
summary: $args["<summary>"],
|
||||||
|
properties: issueProps,
|
||||||
|
tags:
|
||||||
|
if tagsOption.isSome: tagsOption.get
|
||||||
|
else: newSeq[string]())
|
||||||
|
|
||||||
|
ctx.cfg.tasksDir.store(result, state)
|
||||||
|
stdout.writeLine "\p" & formatIssue(result)
|
||||||
|
|
||||||
|
|
||||||
proc edit(issue: Issue) =
|
proc edit(issue: Issue) =
|
||||||
|
|
||||||
# Write format comments (to help when editing)
|
# Write format comments (to help when editing)
|
||||||
@@ -89,8 +171,6 @@ when isMainModule:
|
|||||||
var exclTagsOption = none(seq[string])
|
var exclTagsOption = none(seq[string])
|
||||||
|
|
||||||
let filter = initFilter()
|
let filter = initFilter()
|
||||||
var filterOption = none(IssueFilter)
|
|
||||||
|
|
||||||
|
|
||||||
if args["--properties"] or args["--context"]:
|
if args["--properties"] or args["--context"]:
|
||||||
|
|
||||||
@@ -124,43 +204,37 @@ when isMainModule:
|
|||||||
# Initialize filter with properties (if given)
|
# Initialize filter with properties (if given)
|
||||||
if propertiesOption.isSome:
|
if propertiesOption.isSome:
|
||||||
filter.properties = propertiesOption.get
|
filter.properties = propertiesOption.get
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
# Add property exclusions (if given)
|
# Add property exclusions (if given)
|
||||||
if exclPropsOption.isSome:
|
if exclPropsOption.isSome:
|
||||||
filter.exclProperties = exclPropsOption.get
|
filter.exclProperties = exclPropsOption.get
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
# 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(re("(?i)" & $args["--match"]))
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
if args["--match-all"]:
|
if args["--match-all"]:
|
||||||
filter.fullMatch = some(re("(?i)" & $args["--match-all"]))
|
filter.fullMatch = some(re("(?i)" & $args["--match-all"]))
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
# 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"):
|
||||||
stderr.writeLine("Limiting to default context: " & ctx.defaultContext.get)
|
stderr.writeLine("Limiting to default context: " & ctx.defaultContext.get)
|
||||||
filter.properties["context"] = ctx.defaultContext.get
|
filter.properties["context"] = ctx.defaultContext.get
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
if tagsOption.isSome:
|
if tagsOption.isSome:
|
||||||
filter.hasTags = tagsOption.get
|
filter.hasTags = tagsOption.get
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
if exclTagsOption.isSome:
|
if exclTagsOption.isSome:
|
||||||
filter.exclTags = exclTagsOption.get
|
filter.exclTags = exclTagsOption.get
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
if args["--today"]:
|
if args["--today"]:
|
||||||
filter.inclStates.add(@[Current, TodoToday, Pending])
|
filter.inclStates.add(@[Current, TodoToday, Pending])
|
||||||
filterOption = some(filter)
|
|
||||||
|
|
||||||
if args["--future"]:
|
if args["--future"]:
|
||||||
filter.inclStates.add(@[Pending, Todo])
|
filter.inclStates.add(@[Pending, Todo])
|
||||||
filterOption = some(filter)
|
|
||||||
|
if args["--show-hidden"]:
|
||||||
|
filter.exclHidden = false
|
||||||
|
|
||||||
# Finally, if the "context" is "all", don't filter on context
|
# Finally, if the "context" is "all", don't filter on context
|
||||||
if filter.properties.hasKey("context") and
|
if filter.properties.hasKey("context") and
|
||||||
@@ -171,27 +245,7 @@ when isMainModule:
|
|||||||
|
|
||||||
## Actual command runners
|
## Actual command runners
|
||||||
if args["new"] or args["add"]:
|
if args["new"] or args["add"]:
|
||||||
let state =
|
updatedIssues.add(ctx.addIssue(args, propertiesOption, tagsOption))
|
||||||
if args["<state>"]: parseEnum[IssueState]($args["<state>"])
|
|
||||||
else: TodoToday
|
|
||||||
|
|
||||||
var issueProps = propertiesOption.get(newTable[string,string]())
|
|
||||||
if not issueProps.hasKey("created"): issueProps["created"] = getTime().local.formatIso8601
|
|
||||||
if not issueProps.hasKey("context") and ctx.defaultContext.isSome():
|
|
||||||
stderr.writeLine("Using default context: " & ctx.defaultContext.get)
|
|
||||||
issueProps["context"] = ctx.defaultContext.get
|
|
||||||
|
|
||||||
var issue = Issue(
|
|
||||||
id: genUUID(),
|
|
||||||
summary: $args["<summary>"],
|
|
||||||
properties: issueProps,
|
|
||||||
tags:
|
|
||||||
if tagsOption.isSome: tagsOption.get
|
|
||||||
else: newSeq[string]())
|
|
||||||
|
|
||||||
ctx.cfg.tasksDir.store(issue, state)
|
|
||||||
updatedIssues.add(issue)
|
|
||||||
stdout.writeLine formatIssue(issue)
|
|
||||||
|
|
||||||
elif args["reorder"]:
|
elif args["reorder"]:
|
||||||
ctx.reorder(parseEnum[IssueState]($args["<state>"]))
|
ctx.reorder(parseEnum[IssueState]($args["<state>"]))
|
||||||
@@ -199,11 +253,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))
|
||||||
@@ -213,10 +262,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:
|
||||||
@@ -224,7 +279,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"]:
|
||||||
@@ -252,6 +313,17 @@ when isMainModule:
|
|||||||
issue.store()
|
issue.store()
|
||||||
updatedIssues.add(issue)
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
|
elif args["update-details"]:
|
||||||
|
let details =
|
||||||
|
if not args["--file"] or $args["--file"] == "-": readAll(stdin)
|
||||||
|
else: readFile($args["--file"])
|
||||||
|
|
||||||
|
for id in @(args["<id>"]):
|
||||||
|
var issue = ctx.cfg.tasksDir.loadIssueById(id)
|
||||||
|
issue.details = details
|
||||||
|
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"]:
|
||||||
|
|
||||||
@@ -318,7 +390,7 @@ when isMainModule:
|
|||||||
|
|
||||||
let issue = ctx.cfg.tasksDir.loadIssueById(id)
|
let issue = ctx.cfg.tasksDir.loadIssueById(id)
|
||||||
|
|
||||||
if not args["--yes"]:
|
if not args["--non-interactive"]:
|
||||||
stderr.write("Delete '" & issue.summary & "' (y/n)? ")
|
stderr.write("Delete '" & issue.summary & "' (y/n)? ")
|
||||||
if not "yes".startsWith(stdin.readLine.toLower):
|
if not "yes".startsWith(stdin.readLine.toLower):
|
||||||
continue
|
continue
|
||||||
@@ -370,7 +442,7 @@ when isMainModule:
|
|||||||
for state in statesOption.get: ctx.loadIssues(state)
|
for state in statesOption.get: ctx.loadIssues(state)
|
||||||
else: ctx.loadAllIssues()
|
else: ctx.loadAllIssues()
|
||||||
|
|
||||||
if filterOption.isSome: ctx.filterIssues(filterOption.get)
|
ctx.filterIssues(filter)
|
||||||
|
|
||||||
for state, issueList in ctx.issues:
|
for state, issueList in ctx.issues:
|
||||||
for issue in issueList:
|
for issue in issueList:
|
||||||
@@ -386,27 +458,27 @@ when isMainModule:
|
|||||||
stdout.writeLine formatIssue(issue)
|
stdout.writeLine formatIssue(issue)
|
||||||
|
|
||||||
# List projects
|
# List projects
|
||||||
elif listProjects: ctx.listProjects(filterOption)
|
elif listProjects: ctx.listProjects(some(filter))
|
||||||
|
|
||||||
# List milestones
|
# List milestones
|
||||||
elif listMilestones: ctx.listMilestones(filterOption)
|
elif listMilestones: ctx.listMilestones(some(filter))
|
||||||
|
|
||||||
# List all issues
|
# List all issues
|
||||||
else:
|
else:
|
||||||
trace "listing all issues"
|
trace "listing all issues"
|
||||||
let showBoth = args["--today"] == args["--future"]
|
let showBoth = args["--today"] == args["--future"]
|
||||||
ctx.list(
|
ctx.list(
|
||||||
filter = filterOption,
|
filter = some(filter),
|
||||||
states = statesOption,
|
states = statesOption,
|
||||||
showToday = showBoth or args["--today"],
|
showToday = showBoth or args["--today"],
|
||||||
showFuture = showBoth or args["--future"],
|
showFuture = showBoth or args["--future"],
|
||||||
showHidden = args["--show-hidden"],
|
|
||||||
verbose = ctx.verbose)
|
verbose = ctx.verbose)
|
||||||
|
|
||||||
elif args["show"]:
|
elif args["show"]:
|
||||||
|
|
||||||
if args["project-board"]:
|
if args["project-board"]:
|
||||||
ctx.showProjectBoard(filterOption)
|
if not args["--show-done"]: filter.exclStates.add(Done)
|
||||||
|
ctx.showProjectBoard(some(filter))
|
||||||
discard
|
discard
|
||||||
|
|
||||||
elif args["dupes"]:
|
elif args["dupes"]:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const PIT_VERSION* = "4.30.1"
|
const PIT_VERSION* = "4.33.1"
|
||||||
|
|
||||||
const USAGE* = """Usage:
|
const USAGE* = """Usage:
|
||||||
pit ( new | add) <summary> [<state>] [options]
|
pit ( new | add) <summary> [<state>] [options]
|
||||||
@@ -10,6 +10,7 @@ const USAGE* = """Usage:
|
|||||||
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]
|
||||||
pit tag <id>... [options]
|
pit tag <id>... [options]
|
||||||
|
pit update-details <id> [--file=<path>] [options]
|
||||||
pit untag <id>... [options]
|
pit untag <id>... [options]
|
||||||
pit reorder <state> [options]
|
pit reorder <state> [options]
|
||||||
pit delegate <id> <delegated-to> [options]
|
pit delegate <id> <delegated-to> [options]
|
||||||
@@ -18,7 +19,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 [options]
|
pit show dupes [options]
|
||||||
pit show project-board [options]
|
pit show project-board [--show-done] [options]
|
||||||
pit show <id> [options]
|
pit show <id> [options]
|
||||||
pit sync [<syncTarget>...] [options]
|
pit sync [<syncTarget>...] [options]
|
||||||
pit help [options]
|
pit help [options]
|
||||||
@@ -33,12 +34,20 @@ Options:
|
|||||||
a filter to the issues listed, only allowing those
|
a filter to the issues listed, only allowing those
|
||||||
which have all of the given properties.
|
which have all of the given properties.
|
||||||
|
|
||||||
|
If a propert name is provided without a value, this
|
||||||
|
will allow all issues which have any value defined
|
||||||
|
for the named property.
|
||||||
|
|
||||||
-P, --excl-properties <props>
|
-P, --excl-properties <props>
|
||||||
When used with the list command, exclude issues
|
When used with the list command, exclude issues
|
||||||
that contain properties with the given value. This
|
that contain properties with the given value. This
|
||||||
parameter is formatted the same as the --properties
|
parameter is formatted the same as the --properties
|
||||||
parameter: "key:val;key:val"
|
parameter: "key:val;key:val"
|
||||||
|
|
||||||
|
If no value is provided for a property, this will
|
||||||
|
filter out all issues with *any* value for that
|
||||||
|
property.
|
||||||
|
|
||||||
-c, --context <ctx> Shorthand for '-p context:<ctx>'
|
-c, --context <ctx> Shorthand for '-p context:<ctx>'
|
||||||
|
|
||||||
-C, --excl-context <ctx> Don't show issues from the given context(s).
|
-C, --excl-context <ctx> Don't show issues from the given context(s).
|
||||||
@@ -71,8 +80,6 @@ Options:
|
|||||||
|
|
||||||
-q, --quiet Suppress verbose output.
|
-q, --quiet Suppress verbose output.
|
||||||
|
|
||||||
-y, --yes Automatically answer "yes" to any prompts.
|
|
||||||
|
|
||||||
--config <cfgFile> Location of the config file (defaults to $HOME/.pitrc)
|
--config <cfgFile> Location of the config file (defaults to $HOME/.pitrc)
|
||||||
|
|
||||||
-E, --echo-args Echo arguments (for debug purposes).
|
-E, --echo-args Echo arguments (for debug purposes).
|
||||||
@@ -88,7 +95,13 @@ Options:
|
|||||||
only print the changes that would be made, but do
|
only print the changes that would be made, but do
|
||||||
not actually make them.
|
not actually make them.
|
||||||
|
|
||||||
-s, --silent Suppress all logging and status output.
|
-I, --non-interactive Run in non-interactive mode. Commands that would
|
||||||
|
normally prompt for user input will instead use
|
||||||
|
default values or fail if required input is not
|
||||||
|
provided via command-line options.
|
||||||
|
|
||||||
|
-s, --silent Suppress all logging and status output and run in
|
||||||
|
non-interactive mode (implies --non-interactive).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
const ONLINE_HELP* = """Issue States:
|
const ONLINE_HELP* = """Issue States:
|
||||||
|
|||||||
@@ -6,36 +6,36 @@ 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 +64,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 +75,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 +85,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 +100,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 +140,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 +161,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*(
|
||||||
@@ -173,7 +174,6 @@ proc list*(
|
|||||||
states: Option[seq[IssueState]],
|
states: Option[seq[IssueState]],
|
||||||
showToday = false,
|
showToday = false,
|
||||||
showFuture = false,
|
showFuture = false,
|
||||||
showHidden = false,
|
|
||||||
verbose: bool) =
|
verbose: bool) =
|
||||||
|
|
||||||
if states.isSome:
|
if states.isSome:
|
||||||
@@ -219,10 +219,7 @@ proc list*(
|
|||||||
|
|
||||||
for s in [Current, TodoToday, Pending]:
|
for s in [Current, TodoToday, Pending]:
|
||||||
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
|
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
|
||||||
let visibleIssues = ctx.issues[s].filterIt(
|
let visibleIssues = ctx.issues[s]
|
||||||
showHidden or
|
|
||||||
not (it.hasProp("hide-until") and
|
|
||||||
it.getDateTime("hide-until") > getTime().local))
|
|
||||||
|
|
||||||
if isatty(stdout):
|
if isatty(stdout):
|
||||||
stdout.write ctx.formatSection(visibleIssues, s, indent, verbose)
|
stdout.write ctx.formatSection(visibleIssues, s, indent, verbose)
|
||||||
@@ -242,10 +239,7 @@ proc list*(
|
|||||||
|
|
||||||
for s in futureCategories:
|
for s in futureCategories:
|
||||||
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
|
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
|
||||||
let visibleIssues = ctx.issues[s].filterIt(
|
let visibleIssues = ctx.issues[s]
|
||||||
showHidden or
|
|
||||||
not (it.hasProp("hide-until") and
|
|
||||||
it.getDateTime("hide-until") > getTime().local))
|
|
||||||
|
|
||||||
if isatty(stdout):
|
if isatty(stdout):
|
||||||
stdout.write ctx.formatSection(visibleIssues, s, indent, verbose)
|
stdout.write ctx.formatSection(visibleIssues, s, indent, verbose)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type
|
|||||||
exclStates*: seq[IssueState]
|
exclStates*: seq[IssueState]
|
||||||
hasTags*: seq[string]
|
hasTags*: seq[string]
|
||||||
exclTags*: seq[string]
|
exclTags*: seq[string]
|
||||||
|
exclHidden*: bool
|
||||||
properties*: TableRef[string, string]
|
properties*: TableRef[string, string]
|
||||||
exclProperties*: TableRef[string, seq[string]]
|
exclProperties*: TableRef[string, seq[string]]
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ type
|
|||||||
PitConfig* = ref object
|
PitConfig* = ref object
|
||||||
tasksDir*: string
|
tasksDir*: string
|
||||||
contexts*: TableRef[string, string]
|
contexts*: TableRef[string, string]
|
||||||
|
defaultPropertiesByContext*: TableRef[string, seq[string]]
|
||||||
autoSync*: bool
|
autoSync*: bool
|
||||||
syncTargets*: seq[JsonNode]
|
syncTargets*: seq[JsonNode]
|
||||||
cfg*: CombinedConfig
|
cfg*: CombinedConfig
|
||||||
@@ -55,6 +57,7 @@ type
|
|||||||
isFromCompletion*: bool
|
isFromCompletion*: bool
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
@@ -150,6 +153,7 @@ proc initFilter*(): IssueFilter =
|
|||||||
summaryMatch: none(Regex),
|
summaryMatch: none(Regex),
|
||||||
inclStates: @[],
|
inclStates: @[],
|
||||||
exclStates: @[],
|
exclStates: @[],
|
||||||
|
exclHidden: true,
|
||||||
hasTags: @[],
|
hasTags: @[],
|
||||||
exclTags: @[],
|
exclTags: @[],
|
||||||
properties: newTable[string, string](),
|
properties: newTable[string, string](),
|
||||||
@@ -183,6 +187,10 @@ proc stateFilter*(states: seq[IssueState]): IssueFilter =
|
|||||||
result = initFilter()
|
result = initFilter()
|
||||||
result.inclStates = states
|
result.inclStates = states
|
||||||
|
|
||||||
|
proc showHiddenFilter*(): IssueFilter =
|
||||||
|
result = initFilter()
|
||||||
|
result.exclHidden = false
|
||||||
|
|
||||||
proc groupBy*(issues: seq[Issue], propertyKey: string): TableRef[string, seq[Issue]] =
|
proc groupBy*(issues: seq[Issue], propertyKey: string): TableRef[string, seq[Issue]] =
|
||||||
result = newTable[string, seq[Issue]]()
|
result = newTable[string, seq[Issue]]()
|
||||||
for i in issues:
|
for i in issues:
|
||||||
@@ -222,7 +230,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
|
||||||
|
|
||||||
@@ -415,11 +425,23 @@ proc nextRecurrence*(tasksDir: string, rec: Recurrence, defaultIssue: Issue): Is
|
|||||||
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
|
||||||
|
|
||||||
|
if filter.exclHidden:
|
||||||
|
let now = getTime().local
|
||||||
|
f = f --> filter(
|
||||||
|
not it.hasProp("hide-until") or
|
||||||
|
it.getDateTime("hide-until") <= now)
|
||||||
|
|
||||||
for k,v in filter.properties:
|
for k,v in filter.properties:
|
||||||
f = f --> filter(it.hasProp(k) and it[k] == v)
|
if v == MATCH_ANY:
|
||||||
|
f = f --> filter(it.hasProp(k))
|
||||||
|
else:
|
||||||
|
f = f --> filter(it.hasProp(k) and it[k] == v)
|
||||||
|
|
||||||
for k,v in filter.exclProperties:
|
for k,v in filter.exclProperties:
|
||||||
f = f --> filter(not (it.hasProp(k) and v.contains(it[k])))
|
if v.len == 0:
|
||||||
|
f = f --> filter(not (it.hasProp(k)))
|
||||||
|
else:
|
||||||
|
f = f --> filter(not (it.hasProp(k) and v.contains(it[k])))
|
||||||
|
|
||||||
if filter.completedRange.isSome:
|
if filter.completedRange.isSome:
|
||||||
let range = filter.completedRange.get
|
let range = filter.completedRange.get
|
||||||
@@ -486,12 +508,18 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitCo
|
|||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
autoSync: parseBool(cfg.getVal("auto-sync", "false")),
|
autoSync: parseBool(cfg.getVal("auto-sync", "false")),
|
||||||
contexts: newTable[string,string](),
|
contexts: newTable[string,string](),
|
||||||
|
defaultPropertiesByContext: newTable[string, seq[string]](),
|
||||||
tasksDir: cfg.getVal("tasks-dir", ""),
|
tasksDir: cfg.getVal("tasks-dir", ""),
|
||||||
syncTargets: cfg.getJson("sync-targets", newJArray()).getElems)
|
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()
|
||||||
|
|
||||||
|
for k, v in cfg.getJson("defaultPropertiesByContext", newJObject()):
|
||||||
|
result.defaultPropertiesByContext[k] = v.getElems() -->
|
||||||
|
map(it.getStr("").strip())
|
||||||
|
.filter(not it.isEmptyOrWhitespace)
|
||||||
|
|
||||||
if isEmptyOrWhitespace(result.tasksDir):
|
if isEmptyOrWhitespace(result.tasksDir):
|
||||||
raise newException(Exception, "no tasks directory configured")
|
raise newException(Exception, "no tasks directory configured")
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables,
|
import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables,
|
||||||
terminal, times, unicode, wordwrap]
|
terminal, times, unicode]
|
||||||
from std/sequtils import repeat, toSeq
|
from std/sequtils import repeat, toSeq
|
||||||
import cliutils, uuids, zero_functional
|
import cliutils, uuids, zero_functional
|
||||||
import ./[formatting, libpit]
|
import ./[formatting, libpit]
|
||||||
|
|
||||||
|
const NO_PROJECT* = "∅ No Project"
|
||||||
|
const NO_MILESTONE* = "∅ No Milestone"
|
||||||
|
const NO_CONTEXT* = "<no-context>"
|
||||||
|
|
||||||
type
|
type
|
||||||
ProjectCfg* = ref object of RootObj
|
ProjectCfg* = ref object of RootObj
|
||||||
name: string
|
name: string
|
||||||
@@ -62,15 +66,18 @@ proc buildDb*(ctx: CliContext, cfg: ProjectsConfiguration): ProjectsDatabase =
|
|||||||
# Now populate the database with issues
|
# Now populate the database with issues
|
||||||
for (state, issues) in pairs(ctx.issues):
|
for (state, issues) in pairs(ctx.issues):
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
if not issue.hasProp("project") or
|
|
||||||
not issue.hasProp("milestone"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
let projectName = issue["project"]
|
let projectName =
|
||||||
let milestone = issue["milestone"]
|
if issue.hasProp("project"): issue["project"]
|
||||||
|
else: NO_PROJECT
|
||||||
|
|
||||||
|
let milestone =
|
||||||
|
if issue.hasProp("milestone"): issue["milestone"]
|
||||||
|
else: NO_MILESTONE
|
||||||
|
|
||||||
let context =
|
let context =
|
||||||
if issue.hasProp("context"): issue["context"]
|
if issue.hasProp("context"): issue["context"]
|
||||||
else: "<no-context>"
|
else: NO_CONTEXT
|
||||||
|
|
||||||
# Make sure we have entries for this context and project
|
# Make sure we have entries for this context and project
|
||||||
if not result.hasKey(context): result[context] = @[]
|
if not result.hasKey(context): result[context] = @[]
|
||||||
@@ -122,28 +129,33 @@ proc listProjects*(ctx: CliContext, filter = none[IssueFilter]()) =
|
|||||||
let projectsCfg = ctx.loadProjectsConfiguration()
|
let projectsCfg = ctx.loadProjectsConfiguration()
|
||||||
var projectsCfgChanged = false
|
var projectsCfgChanged = false
|
||||||
|
|
||||||
|
var linesToPrint = newSeq[string]()
|
||||||
|
|
||||||
if filter.isSome: ctx.filterIssues(filter.get)
|
if filter.isSome: ctx.filterIssues(filter.get)
|
||||||
|
|
||||||
let projectsByContext = newTable[string, CountTableRef[string]]()
|
let projectsByContext = newTable[string, CountTableRef[string]]()
|
||||||
|
|
||||||
for (state, issues) in pairs(ctx.issues):
|
for (state, issues) in pairs(ctx.issues):
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
if issue.hasProp("project"):
|
let context =
|
||||||
let context =
|
if issue.hasProp("context"): issue["context"]
|
||||||
if issue.hasProp("context"): issue["context"]
|
else: NO_CONTEXT
|
||||||
else: "<no-context>"
|
|
||||||
|
|
||||||
if not projectsByContext.hasKey(context):
|
let projectName =
|
||||||
projectsByContext[context] = newCountTable[string]()
|
if issue.hasProp("project"): issue["project"]
|
||||||
|
else: NO_PROJECT
|
||||||
|
|
||||||
projectsByContext[context].inc(issue["project"])
|
if not projectsByContext.hasKey(context):
|
||||||
|
projectsByContext[context] = newCountTable[string]()
|
||||||
|
|
||||||
|
projectsByContext[context].inc(projectName)
|
||||||
|
|
||||||
for (context, projects) in pairs(projectsByContext):
|
for (context, projects) in pairs(projectsByContext):
|
||||||
|
|
||||||
stdout.writeLine(withColor(
|
linesToPrint.add(termFmt(
|
||||||
ctx.getIssueContextDisplayName(context) & ":",
|
ctx.getIssueContextDisplayName(context) & ":",
|
||||||
fgYellow) & termReset)
|
fg=cYellow))
|
||||||
stdout.writeLine("")
|
linesToPrint.add("")
|
||||||
|
|
||||||
var toList = toHashSet(toSeq(keys(projects)))
|
var toList = toHashSet(toSeq(keys(projects)))
|
||||||
|
|
||||||
@@ -154,22 +166,29 @@ proc listProjects*(ctx: CliContext, filter = none[IssueFilter]()) =
|
|||||||
for project in projectsCfg[context]:
|
for project in projectsCfg[context]:
|
||||||
if project.name in toList:
|
if project.name in toList:
|
||||||
toList.excl(project.name)
|
toList.excl(project.name)
|
||||||
stdout.writeLine(" " & project.name &
|
linesToPrint.add(" " & project.name &
|
||||||
" (" & $projects[project.name] & " issues)")
|
" (" & $projects[project.name] & " issues)")
|
||||||
|
|
||||||
# Then list any remaining projects not in the configuration, and add them
|
# Then list any remaining projects not in the configuration, and add them
|
||||||
# to the configuration
|
# to the configuration
|
||||||
for (projectName, count) in pairs(projects):
|
for (projectName, count) in pairs(projects):
|
||||||
if projectName in toList:
|
if projectName in toList:
|
||||||
stdout.writeLine(" " & projectName & " (" & $count & " issues)")
|
linesToPrint.add(" " & projectName & " (" & $count & " issues)")
|
||||||
projectsCfg[context].add(ProjectCfg(name: projectName, milestoneOrder: @[]))
|
projectsCfg[context].add(ProjectCfg(name: projectName, milestoneOrder: @[]))
|
||||||
projectsCfgChanged = true
|
projectsCfgChanged = true
|
||||||
|
|
||||||
stdout.writeLine("")
|
linesToPrint.add("")
|
||||||
|
|
||||||
if projectsCfgChanged: ctx.saveProjectsConfiguration(projectsCfg)
|
if projectsCfgChanged: ctx.saveProjectsConfiguration(projectsCfg)
|
||||||
|
|
||||||
|
if isatty(stdout):
|
||||||
|
stdout.writeLine(linesToPrint.join("\p"))
|
||||||
|
else:
|
||||||
|
stdout.writeLine(stripAnsi(linesToPrint.join("\p")))
|
||||||
|
|
||||||
proc listMilestones*(ctx: CliContext, filter = none[IssueFilter]()) =
|
proc listMilestones*(ctx: CliContext, filter = none[IssueFilter]()) =
|
||||||
|
var linesToPrint = newSeq[string]()
|
||||||
|
|
||||||
ctx.loadAllIssues()
|
ctx.loadAllIssues()
|
||||||
if filter.isSome: ctx.filterIssues(filter.get)
|
if filter.isSome: ctx.filterIssues(filter.get)
|
||||||
|
|
||||||
@@ -184,18 +203,24 @@ 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
|
||||||
|
|
||||||
stdout.writeLine(withColor(project.name, fgBlue, bold = true, bright=true))
|
linesToPrint.add(termFmt(project.name, cBrightBlue, style = { tsBold }))
|
||||||
stdout.writeLine(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
|
||||||
project.milestones[milestone].len > 0:
|
project.milestones[milestone].len > 0:
|
||||||
let issueCount = project.milestones[milestone].len
|
let issueCount = project.milestones[milestone].len
|
||||||
stdout.writeLine(" " & milestone & " (" & $issueCount & " issues)")
|
linesToPrint.add(" " & milestone & " (" & $issueCount & " issues)")
|
||||||
|
|
||||||
|
linesToPrint.add("")
|
||||||
|
|
||||||
|
if isatty(stdout):
|
||||||
|
stdout.writeLine(linesToPrint.join("\p"))
|
||||||
|
else:
|
||||||
|
stdout.writeLine(stripAnsi(linesToPrint.join("\p")))
|
||||||
|
|
||||||
stdout.writeLine("")
|
|
||||||
|
|
||||||
proc formatProjectIssue(
|
proc formatProjectIssue(
|
||||||
ctx: CliContext,
|
ctx: CliContext,
|
||||||
@@ -204,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,
|
||||||
@@ -240,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("")
|
||||||
|
|
||||||
@@ -252,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"])
|
||||||
@@ -296,23 +321,25 @@ proc joinColumns(columns: seq[seq[string]], columnWidth: int): seq[string] =
|
|||||||
|
|
||||||
proc showProject*(ctx: CliContext, project: Project) =
|
proc showProject*(ctx: CliContext, project: Project) =
|
||||||
|
|
||||||
|
var linesToPrint = newSeq[string]()
|
||||||
let fullWidth = terminalWidth() - 1
|
let fullWidth = terminalWidth() - 1
|
||||||
let columnWidth = 80
|
let columnWidth = 80
|
||||||
let numColumns = (fullWidth - 4) div (columnWidth + 2)
|
let numColumns = (fullWidth - 4) div (columnWidth + 2)
|
||||||
|
|
||||||
stdout.writeLine("")
|
linesToPrint.add("")
|
||||||
stdout.writeLine(withColor(
|
linesToPrint.add(termFmt(
|
||||||
"┌" & "─".repeat(project.name.len + 2) &
|
"┌" & "─".repeat(project.name.runeLen + 2) &
|
||||||
"┬" & "─".repeat(fullWidth - project.name.len - 4) & "┐",
|
"┬" & "─".repeat(fullWidth - project.name.runeLen - 4) & "┐",
|
||||||
fgBlue, bold=true))
|
fg=cBlue, style={tsBold}))
|
||||||
stdout.writeLine(
|
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.len - 4) & "│", fgBlue, bold=true))
|
termFmt(" │" & " ".repeat(fullWidth - project.name.runeLen - 4) & "│",
|
||||||
stdout.writeLine(withColor(
|
fg=cBlue, style={tsBold}))
|
||||||
"├" & "─".repeat(project.name.len + 2) &
|
linesToPrint.add(termFmt(
|
||||||
"┘" & " ".repeat(fullWidth - project.name.len - 4) & "│",
|
"├" & "─".repeat(project.name.runeLen + 2) &
|
||||||
fgBlue, bold=true))
|
"┘" & " ".repeat(fullWidth - project.name.runeLen - 4) & "│",
|
||||||
|
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).
|
||||||
@@ -328,15 +355,21 @@ 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
|
||||||
stdout.writeLine(
|
linesToPrint.add(
|
||||||
withColor("│ ", fgBlue) &
|
termFmt("│ ", fg=cBlue) &
|
||||||
line &
|
line &
|
||||||
" ".repeat(padLen) &
|
" ".repeat(padLen) &
|
||||||
withColor(" │", fgBlue))
|
termFmt(" │", fg=cBlue))
|
||||||
|
|
||||||
stdout.writeLine(withColor(
|
linesToPrint.add(termFmt(
|
||||||
"└" & "─".repeat(terminalWidth() - 2) & "┘",
|
"└" & "─".repeat(terminalWidth() - 2) & "┘",
|
||||||
fgBlue, bold=true))
|
fg=cBlue, style={tsBold}))
|
||||||
|
|
||||||
|
if isatty(stdout):
|
||||||
|
stdout.writeLine(linesToPrint.join("\p"))
|
||||||
|
else:
|
||||||
|
stdout.writeLine(stripAnsi(linesToPrint.join("\p")))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
proc showProjectBoard*(ctx: CliContext, filter = none[IssueFilter]()) =
|
proc showProjectBoard*(ctx: CliContext, filter = none[IssueFilter]()) =
|
||||||
@@ -357,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:
|
||||||
|
|||||||
Reference in New Issue
Block a user