Compare commits

..

4 Commits

Author SHA1 Message Date
3d1dc7512a Allow including or excluding properties by name only. 2025-12-01 14:04:19 -06:00
1d18be9d1b Include issues without project or milestone on boards.
In order to help organize issues, show issues on boards even if they
don't have an assigned project or milestone.

Refactor the issue hiding feature (using the `hide-until` property) to
be an option to IssueFilter rather than a separate, special-case. This
means that the CLI always filters by default.

Hide issues in the Done state on project boards unless the new
`--show-done` arg is passed.
2025-12-01 13:50:13 -06:00
6ceca9b009 Make add interactive and allow defining default properties that should be prompted. 2025-11-24 10:47:22 -06:00
de07665a8b Project boards: only show contexts with selected issues. 2025-11-19 19:00:01 -06:00
6 changed files with 190 additions and 88 deletions

View File

@@ -1,6 +1,6 @@
# Package # Package
version = "4.30.0" version = "4.31.1"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Personal issue tracker." description = "Personal issue tracker."
license = "MIT" license = "MIT"
@@ -27,4 +27,4 @@ requires @[
] ]
task updateVersion, "Update the version of this package.": task updateVersion, "Update the version of this package.":
exec "update_nim_package_version pit 'src/pit/cliconstants.nim'" exec "update_nim_package_version pit 'src/pit/cliconstants.nim'"

View File

@@ -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" &
withColor(propName, fgMagenta) & ":" &
withColor(" ", fgBlue, bright=true, skipReset=true))
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(termReset)
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>"]))
@@ -318,7 +372,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 +424,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 +440,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"]:

View File

@@ -1,4 +1,4 @@
const PIT_VERSION* = "4.30.0" const PIT_VERSION* = "4.31.1"
const USAGE* = """Usage: const USAGE* = """Usage:
pit ( new | add) <summary> [<state>] [options] pit ( new | add) <summary> [<state>] [options]
@@ -18,7 +18,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 +33,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 +79,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 +94,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:

View File

@@ -173,7 +173,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 +218,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 +238,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)

View File

@@ -1,4 +1,4 @@
import std/[json, logging, options, os, strformat, strutils, tables, times, import std/[json, jsonutils, logging, options, os, strformat, strutils, tables, times,
unicode] unicode]
import cliutils, docopt, langutils, uuids, zero_functional import cliutils, docopt, langutils, uuids, zero_functional
@@ -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:
@@ -415,11 +423,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 +506,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")

View File

@@ -1,9 +1,13 @@
import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables, terminal, import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables,
times, unicode, wordwrap] terminal, times, unicode, wordwrap]
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-project>"
const NO_CONTEXT* = "<no-project>"
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] = @[]
@@ -128,15 +135,18 @@ proc listProjects*(ctx: CliContext, filter = none[IssueFilter]()) =
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):
@@ -256,8 +266,7 @@ proc formatMilestone*(
result.add(withColor("".repeat(availWidth), fgWhite)) result.add(withColor("".repeat(availWidth), fgWhite))
var parentsToChildren = issues --> var parentsToChildren = issues -->
filter(it.hasProp("parent")) --> filter(it.hasProp("parent")).group(it["parent"])
group(it["parent"])
var issuesToFormat = sorted(issues, cmp) --> var issuesToFormat = sorted(issues, cmp) -->
filter(not it.hasProp("parent")) filter(not it.hasProp("parent"))
@@ -282,7 +291,7 @@ proc findShortestColumn(columns: seq[seq[string]]): int =
proc joinColumns(columns: seq[seq[string]], columnWidth: int): seq[string] = proc joinColumns(columns: seq[seq[string]], columnWidth: int): seq[string] =
let maxLines = columns --> map(it.len) --> max() let maxLines = columns --> map(it.len).max()
for lineNo in 0 ..< maxLines: for lineNo in 0 ..< maxLines:
var newLine = "" var newLine = ""
@@ -316,7 +325,7 @@ proc showProject*(ctx: CliContext, project: Project) =
fgBlue, bold=true)) fgBlue, bold=true))
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).
map(ctx.formatMilestone(it, project.milestones[it], columnWidth)) map(ctx.formatMilestone(it, project.milestones[it], columnWidth))
var columns: seq[seq[string]] = repeat(newSeq[string](), numColumns) var columns: seq[seq[string]] = repeat(newSeq[string](), numColumns)
@@ -347,8 +356,16 @@ proc showProjectBoard*(ctx: CliContext, filter = none[IssueFilter]()) =
let projectsCfg = ctx.loadProjectsConfiguration() let projectsCfg = ctx.loadProjectsConfiguration()
let projectsDb = ctx.buildDb(projectsCfg) let projectsDb = ctx.buildDb(projectsCfg)
for (context, projects) in pairs(projectsDb): var contextsAndProjects: seq[(string, seq[Project])] = @[]
if projectsDb.len > 1:
for (context, pjs) in pairs(projectsDb):
let projects = pjs
let issues: seq[Issue] = projects --> map(toSeq(values(it.milestones))).flatten().flatten()
if issues.len > 0:
contextsAndProjects.add((context, projects))
for (context, projects) in contextsAndProjects:
if contextsAndProjects.len > 1:
stdout.writeLine("") stdout.writeLine("")
stdout.writeLine(withColor( stdout.writeLine(withColor(
ctx.getIssueContextDisplayName(context) & ":", ctx.getIssueContextDisplayName(context) & ":",