Add support for a project view like virtual-status-board.probatem.com
This commit is contained in:
53
src/pit.nim
53
src/pit.nim
@@ -6,7 +6,7 @@ import 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 pit/[cliconstants, formatting, libpit, sync_pbm_vsb]
|
import pit/[cliconstants, formatting, libpit, projects, sync_pbm_vsb]
|
||||||
|
|
||||||
export formatting, libpit
|
export formatting, libpit
|
||||||
|
|
||||||
@@ -330,10 +330,14 @@ when isMainModule:
|
|||||||
|
|
||||||
var listContexts = false
|
var listContexts = false
|
||||||
var listTags = false
|
var listTags = false
|
||||||
|
var listProjects = false
|
||||||
|
var listMilestones = 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["projects"]: listProjects = true
|
||||||
|
elif args["milestones"]: listMilestones = true
|
||||||
elif args["tags"]: listTags = true
|
elif args["tags"]: listTags = true
|
||||||
elif args["<stateOrId>"]:
|
elif args["<stateOrId>"]:
|
||||||
try:
|
try:
|
||||||
@@ -381,6 +385,12 @@ when isMainModule:
|
|||||||
let issue = ctx.cfg.tasksDir.loadIssueById(issueId)
|
let issue = ctx.cfg.tasksDir.loadIssueById(issueId)
|
||||||
stdout.writeLine formatIssue(issue)
|
stdout.writeLine formatIssue(issue)
|
||||||
|
|
||||||
|
# List projects
|
||||||
|
elif listProjects: ctx.listProjects(filterOption)
|
||||||
|
|
||||||
|
# List milestones
|
||||||
|
elif listMilestones: ctx.listMilestones(filterOption)
|
||||||
|
|
||||||
# List all issues
|
# List all issues
|
||||||
else:
|
else:
|
||||||
trace "listing all issues"
|
trace "listing all issues"
|
||||||
@@ -393,6 +403,32 @@ when isMainModule:
|
|||||||
showHidden = args["--show-hidden"],
|
showHidden = args["--show-hidden"],
|
||||||
verbose = ctx.verbose)
|
verbose = ctx.verbose)
|
||||||
|
|
||||||
|
elif args["show"]:
|
||||||
|
|
||||||
|
if args["project-board"]:
|
||||||
|
ctx.showProjectBoard(filterOption)
|
||||||
|
discard
|
||||||
|
|
||||||
|
elif args["dupes"]:
|
||||||
|
ctx.loadAllIssues()
|
||||||
|
|
||||||
|
var idsToPaths = newTable[string, var seq[string]]()
|
||||||
|
for (state, issues) in pairs(ctx.issues):
|
||||||
|
for issue in issues:
|
||||||
|
let issueId = $issue.id
|
||||||
|
|
||||||
|
if idsToPaths.hasKey(issueId): idsToPaths[issueId].add(issue.filepath)
|
||||||
|
else: idsToPaths[issueId] = @[issue.filepath]
|
||||||
|
|
||||||
|
for (issueId, issuePaths) in pairs(idsToPaths):
|
||||||
|
if issuePaths.len < 2: continue
|
||||||
|
stdout.writeLine(issueId & ":\p " & issuePaths.join("\p ") & "\p\p")
|
||||||
|
|
||||||
|
else: # list specific Issues
|
||||||
|
for issueId in args["<id>"].mapIt($it):
|
||||||
|
let issue = ctx.cfg.tasksDir.loadIssueById(issueId)
|
||||||
|
stdout.writeLine formatIssue(issue)
|
||||||
|
|
||||||
elif args["add-binary-property"]:
|
elif args["add-binary-property"]:
|
||||||
let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"]))
|
let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"]))
|
||||||
|
|
||||||
@@ -421,21 +457,6 @@ when isMainModule:
|
|||||||
try: write(propOut, decodeDataUri(issue[$(args["<propName>"])]))
|
try: write(propOut, decodeDataUri(issue[$(args["<propName>"])]))
|
||||||
finally: close(propOut)
|
finally: close(propOut)
|
||||||
|
|
||||||
elif args["show-dupes"]:
|
|
||||||
ctx.loadAllIssues()
|
|
||||||
|
|
||||||
var idsToPaths = newTable[string, var seq[string]]()
|
|
||||||
for (state, issues) in pairs(ctx.issues):
|
|
||||||
for issue in issues:
|
|
||||||
let issueId = $issue.id
|
|
||||||
|
|
||||||
if idsToPaths.hasKey(issueId): idsToPaths[issueId].add(issue.filepath)
|
|
||||||
else: idsToPaths[issueId] = @[issue.filepath]
|
|
||||||
|
|
||||||
for (issueId, issuePaths) in pairs(idsToPaths):
|
|
||||||
if issuePaths.len < 2: continue
|
|
||||||
stdout.writeLine(issueId & ":\p " & issuePaths.join("\p ") & "\p\p")
|
|
||||||
|
|
||||||
elif args["sync"]:
|
elif args["sync"]:
|
||||||
if ctx.cfg.syncTargets.len == 0:
|
if ctx.cfg.syncTargets.len == 0:
|
||||||
info "No sync targets configured"
|
info "No sync targets configured"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
const PIT_VERSION* = "4.29.2"
|
const PIT_VERSION* = "4.30.0"
|
||||||
|
|
||||||
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 projects [options]
|
||||||
|
pit list milestones [options]
|
||||||
pit list tags [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]
|
||||||
@@ -15,10 +17,13 @@ const USAGE* = """Usage:
|
|||||||
pit ( delete | rm ) <id>... [options]
|
pit ( delete | rm ) <id>... [options]
|
||||||
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 [options]
|
||||||
|
pit show project-board [options]
|
||||||
|
pit show <id> [options]
|
||||||
pit sync [<syncTarget>...] [options]
|
pit sync [<syncTarget>...] [options]
|
||||||
pit help [options]
|
pit help [options]
|
||||||
|
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-h, --help Print this usage and help information.
|
-h, --help Print this usage and help information.
|
||||||
@@ -144,6 +149,38 @@ Issue Properties:
|
|||||||
|
|
||||||
hide-until: 2024-01-01T13:45:00-05:00
|
hide-until: 2024-01-01T13:45:00-05:00
|
||||||
|
|
||||||
|
priority
|
||||||
|
|
||||||
|
Allows setting a priority/priority level. This is used in the project
|
||||||
|
management view to automatically order issues being displayed. Valid
|
||||||
|
values, in order from most to least important, are:
|
||||||
|
|
||||||
|
- essential Intended for issues that must be done. Failure to
|
||||||
|
complete these issues would result in failure of the
|
||||||
|
project.
|
||||||
|
|
||||||
|
- vital Intended for issues that are vital to the success of the
|
||||||
|
project, but not absolutely essential. Failure to complete
|
||||||
|
these issues may not result in failure of the project but
|
||||||
|
would seriously impact it's value or viability.
|
||||||
|
|
||||||
|
- important Intended for issues that are important to the project,
|
||||||
|
but not vital. These should be completed, but delay is
|
||||||
|
acceptable.
|
||||||
|
|
||||||
|
- optional Intended for issues that are worth doing but can be deferred
|
||||||
|
or skipped if necessary.
|
||||||
|
|
||||||
|
priority: essential
|
||||||
|
|
||||||
|
milestone
|
||||||
|
|
||||||
|
Allows grouping issues according to a milestone name. Milestones are
|
||||||
|
available as subsets of projects. The 'list milestones' command will show
|
||||||
|
all values of 'milestone' set in existing issues for a given project.
|
||||||
|
|
||||||
|
milestone: Phase 1
|
||||||
|
|
||||||
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
|
||||||
@@ -152,6 +189,13 @@ Issue Properties:
|
|||||||
|
|
||||||
pending: Results of WCAG analysis.
|
pending: Results of WCAG analysis.
|
||||||
|
|
||||||
|
project
|
||||||
|
|
||||||
|
Allows grouping issues according to a project name. The 'list projects'
|
||||||
|
command will show all values of 'project' set in existing issues.
|
||||||
|
|
||||||
|
project: Website Redesign
|
||||||
|
|
||||||
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
|
||||||
@@ -191,4 +235,4 @@ Issue Properties:
|
|||||||
|
|
||||||
If present, expected to be a comma-delimited list of text tags. The -g
|
If present, expected to be a comma-delimited list of text tags. The -g
|
||||||
option is a short-hand for '-p tags:<tags-value>'.
|
option is a short-hand for '-p tags:<tags-value>'.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -14,9 +14,22 @@ proc formatIssue*(issue: Issue): string =
|
|||||||
issue.tags.join(",").withColor(fgGreen, true) & "\n"
|
issue.tags.join(",").withColor(fgGreen, true) & "\n"
|
||||||
|
|
||||||
if issue.properties.len > 0:
|
if issue.properties.len > 0:
|
||||||
result &= termColor(fgMagenta)
|
for k, v in issue.properties:
|
||||||
for k, v in issue.properties: result &= k & ": " & v & "\n"
|
|
||||||
|
|
||||||
|
if k == "project":
|
||||||
|
result &= "project: ".withColor(fgMagenta) &
|
||||||
|
v.withColor(fgBlue, bright = true) & "\n"
|
||||||
|
|
||||||
|
elif k == "milestone":
|
||||||
|
result &= "milestone: ".withColor(fgMagenta) &
|
||||||
|
v.withColor(fgBlue, bright = true) & "\n"
|
||||||
|
|
||||||
|
elif k == "priority":
|
||||||
|
result &= "priority: ".withColor(fgMagenta) &
|
||||||
|
v.withColor(fgRed, bright = true) & "\n"
|
||||||
|
|
||||||
|
else:
|
||||||
|
result &= termColor(fgMagenta) & k & ": " & v & "\n"
|
||||||
|
|
||||||
result &= "--------".withColor(fgBlack, true) & "\n"
|
result &= "--------".withColor(fgBlack, true) & "\n"
|
||||||
if not issue.details.isEmptyOrWhitespace:
|
if not issue.details.isEmptyOrWhitespace:
|
||||||
@@ -48,7 +61,8 @@ proc formatSectionIssue*(
|
|||||||
issue: Issue,
|
issue: Issue,
|
||||||
width: int = 80,
|
width: int = 80,
|
||||||
indent = "",
|
indent = "",
|
||||||
verbose = false): string =
|
verbose = false,
|
||||||
|
bold = false): string =
|
||||||
|
|
||||||
result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true) & " "
|
result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true) & " "
|
||||||
|
|
||||||
@@ -61,7 +75,7 @@ proc formatSectionIssue*(
|
|||||||
.wrapWords(summaryWidth)
|
.wrapWords(summaryWidth)
|
||||||
.splitLines
|
.splitLines
|
||||||
|
|
||||||
result &= summaryLines[0].withColor(fgWhite)
|
result &= summaryLines[0].termFmt(fgWhite, bold=bold, underline=bold)
|
||||||
|
|
||||||
for line in summaryLines[1..^1]:
|
for line in summaryLines[1..^1]:
|
||||||
result &= "\p" & line.indent(summaryIndentLen)
|
result &= "\p" & line.indent(summaryIndentLen)
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ type
|
|||||||
properties*: TableRef[string, string]
|
properties*: TableRef[string, string]
|
||||||
exclProperties*: TableRef[string, seq[string]]
|
exclProperties*: TableRef[string, seq[string]]
|
||||||
|
|
||||||
|
IssuePriority* {.pure.} = enum essential, vital, important, optional
|
||||||
|
|
||||||
PitConfig* = ref object
|
PitConfig* = ref object
|
||||||
tasksDir*: string
|
tasksDir*: string
|
||||||
contexts*: TableRef[string, string]
|
contexts*: TableRef[string, string]
|
||||||
@@ -132,6 +134,13 @@ proc getRecurrence*(issue: Issue): Option[Recurrence] =
|
|||||||
else: weeks(1),
|
else: weeks(1),
|
||||||
cloneId: c[6]))
|
cloneId: c[6]))
|
||||||
|
|
||||||
|
proc setPriority*(issue: Issue, priority: IssuePriority) =
|
||||||
|
issue["priority"] = $priority
|
||||||
|
|
||||||
|
proc getPriority*(issue: Issue): IssuePriority =
|
||||||
|
try: result = parseEnum[IssuePriority](issue["priority"].toLowerAscii())
|
||||||
|
except CatchableError: result = IssuePriority.optional
|
||||||
|
|
||||||
|
|
||||||
## Issue filtering
|
## Issue filtering
|
||||||
proc initFilter*(): IssueFilter =
|
proc initFilter*(): IssueFilter =
|
||||||
|
|||||||
361
src/pit/projects.nim
Normal file
361
src/pit/projects.nim
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables, terminal,
|
||||||
|
times, unicode, wordwrap]
|
||||||
|
from std/sequtils import repeat, toSeq
|
||||||
|
import cliutils, uuids, zero_functional
|
||||||
|
import ./[formatting, libpit]
|
||||||
|
|
||||||
|
type
|
||||||
|
ProjectCfg* = ref object of RootObj
|
||||||
|
name: string
|
||||||
|
milestoneOrder*: seq[string]
|
||||||
|
|
||||||
|
Project* = ref object of ProjectCfg
|
||||||
|
milestones*: TableRef[string, seq[Issue]]
|
||||||
|
|
||||||
|
ProjectsConfiguration* = TableRef[string, seq[ProjectCfg]]
|
||||||
|
## ProjectCfgs by context
|
||||||
|
|
||||||
|
ProjectsDatabase* = TableRef[string, seq[Project]]
|
||||||
|
## Projects by context
|
||||||
|
|
||||||
|
|
||||||
|
converter extractConfig(pdb: ProjectsDatabase): ProjectsConfiguration =
|
||||||
|
result = newTable[string, seq[ProjectCfg]]()
|
||||||
|
for (context, projects) in pairs(pdb):
|
||||||
|
result[context] = @[]
|
||||||
|
for project in projects:
|
||||||
|
result[context].add(ProjectCfg(
|
||||||
|
name: project.name,
|
||||||
|
milestoneOrder: project.milestoneOrder))
|
||||||
|
|
||||||
|
|
||||||
|
proc loadProjectsConfiguration*(ctx: CliContext): ProjectsConfiguration =
|
||||||
|
|
||||||
|
let projectsCfgFile = ctx.cfg.tasksDir & "/projects.json"
|
||||||
|
|
||||||
|
if not fileExists(projectsCfgFile):
|
||||||
|
return newTable[string, seq[ProjectCfg]]()
|
||||||
|
else:
|
||||||
|
fromJson[ProjectsConfiguration](result, parseFile(projectsCfgFile))
|
||||||
|
|
||||||
|
|
||||||
|
proc saveProjectsConfiguration*(ctx: CliContext, cfg: ProjectsConfiguration) =
|
||||||
|
let projectsCfgFile = ctx.cfg.tasksDir / "projects.json"
|
||||||
|
writeFile(projectsCfgFile, toJson(cfg).pretty)
|
||||||
|
|
||||||
|
|
||||||
|
proc buildDb*(ctx: CliContext, cfg: ProjectsConfiguration): ProjectsDatabase =
|
||||||
|
result = newTable[string, seq[Project]]()
|
||||||
|
|
||||||
|
# Expand the configuration into the database structure
|
||||||
|
for (context, projectCfgs) in pairs(cfg):
|
||||||
|
result[context] = @[]
|
||||||
|
for projectCfg in projectCfgs:
|
||||||
|
let pcfg = projectCfg
|
||||||
|
let project = Project(
|
||||||
|
name: projectCfg.name,
|
||||||
|
milestoneOrder: projectCfg.milestoneOrder,
|
||||||
|
milestones: newTable[string, seq[Issue]](
|
||||||
|
pcfg.milestoneOrder --> map((it, newSeq[Issue]()))))
|
||||||
|
result[context].add(project)
|
||||||
|
|
||||||
|
# Now populate the database with issues
|
||||||
|
for (state, issues) in pairs(ctx.issues):
|
||||||
|
for issue in issues:
|
||||||
|
if not issue.hasProp("project") or
|
||||||
|
not issue.hasProp("milestone"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
let projectName = issue["project"]
|
||||||
|
let milestone = issue["milestone"]
|
||||||
|
let context =
|
||||||
|
if issue.hasProp("context"): issue["context"]
|
||||||
|
else: "<no-context>"
|
||||||
|
|
||||||
|
# Make sure we have entries for this context and project
|
||||||
|
if not result.hasKey(context): result[context] = @[]
|
||||||
|
|
||||||
|
var projectsInContext = result[context]
|
||||||
|
|
||||||
|
if not (projectsInContext --> exists(it.name == projectName)):
|
||||||
|
projectsInContext.add(Project(
|
||||||
|
name: projectName,
|
||||||
|
milestoneOrder: @[],
|
||||||
|
milestones: newTable[string, seq[Issue]]()))
|
||||||
|
|
||||||
|
let projectIdx = projectsInContext --> index(it.name == projectName)
|
||||||
|
var project = projectsInContext[projectIdx]
|
||||||
|
|
||||||
|
# Make sure we have entries for this milestone
|
||||||
|
if not project.milestones.hasKey(milestone):
|
||||||
|
project.milestones[milestone] = @[]
|
||||||
|
|
||||||
|
if not project.milestoneOrder.contains(milestone):
|
||||||
|
project.milestoneOrder.add(milestone)
|
||||||
|
|
||||||
|
project.milestones[milestone].add(issue)
|
||||||
|
|
||||||
|
result[context] = projectsInContext
|
||||||
|
|
||||||
|
ctx.saveProjectsConfiguration(result)
|
||||||
|
|
||||||
|
proc cmp*(a, b: Issue): int =
|
||||||
|
if a.state != b.state:
|
||||||
|
return cmp(ord(a.state), ord(b.state))
|
||||||
|
|
||||||
|
if a.hasProp("priority") or b.hasProp("priority"):
|
||||||
|
if a.getPriority != b.getPriority:
|
||||||
|
return cmp(a.getPriority, b.getPriority) # higher priority first
|
||||||
|
|
||||||
|
if a.hasProp("last-updated") or b.hasProp("last-updated"):
|
||||||
|
var aUpdated = a.getDateTime("last-updated", local(fromUnix(0)))
|
||||||
|
var bUpdated = b.getDateTime("last-updated", local(fromUnix(0)))
|
||||||
|
|
||||||
|
if aUpdated != bUpdated:
|
||||||
|
return cmp(bUpdated, aUpdated) # newer first
|
||||||
|
|
||||||
|
return cmp(a.summary, b.summary)
|
||||||
|
|
||||||
|
|
||||||
|
proc listProjects*(ctx: CliContext, filter = none[IssueFilter]()) =
|
||||||
|
ctx.loadAllIssues()
|
||||||
|
let projectsCfg = ctx.loadProjectsConfiguration()
|
||||||
|
var projectsCfgChanged = false
|
||||||
|
|
||||||
|
if filter.isSome: ctx.filterIssues(filter.get)
|
||||||
|
|
||||||
|
let projectsByContext = newTable[string, CountTableRef[string]]()
|
||||||
|
|
||||||
|
for (state, issues) in pairs(ctx.issues):
|
||||||
|
for issue in issues:
|
||||||
|
if issue.hasProp("project"):
|
||||||
|
let context =
|
||||||
|
if issue.hasProp("context"): issue["context"]
|
||||||
|
else: "<no-context>"
|
||||||
|
|
||||||
|
if not projectsByContext.hasKey(context):
|
||||||
|
projectsByContext[context] = newCountTable[string]()
|
||||||
|
|
||||||
|
projectsByContext[context].inc(issue["project"])
|
||||||
|
|
||||||
|
for (context, projects) in pairs(projectsByContext):
|
||||||
|
|
||||||
|
stdout.writeLine(withColor(
|
||||||
|
ctx.getIssueContextDisplayName(context) & ":",
|
||||||
|
fgYellow) & termReset)
|
||||||
|
stdout.writeLine("")
|
||||||
|
|
||||||
|
var toList = toHashSet(toSeq(keys(projects)))
|
||||||
|
|
||||||
|
# Loop through the projects in the configured order first
|
||||||
|
if not projectsCfg.hasKey(context):
|
||||||
|
projectsCfg[context] = @[]
|
||||||
|
|
||||||
|
for project in projectsCfg[context]:
|
||||||
|
if project.name in toList:
|
||||||
|
toList.excl(project.name)
|
||||||
|
stdout.writeLine(" " & project.name &
|
||||||
|
" (" & $projects[project.name] & " issues)")
|
||||||
|
|
||||||
|
# Then list any remaining projects not in the configuration, and add them
|
||||||
|
# to the configuration
|
||||||
|
for (projectName, count) in pairs(projects):
|
||||||
|
if projectName in toList:
|
||||||
|
stdout.writeLine(" " & projectName & " (" & $count & " issues)")
|
||||||
|
projectsCfg[context].add(ProjectCfg(name: projectName, milestoneOrder: @[]))
|
||||||
|
projectsCfgChanged = true
|
||||||
|
|
||||||
|
stdout.writeLine("")
|
||||||
|
|
||||||
|
if projectsCfgChanged: ctx.saveProjectsConfiguration(projectsCfg)
|
||||||
|
|
||||||
|
proc listMilestones*(ctx: CliContext, filter = none[IssueFilter]()) =
|
||||||
|
ctx.loadAllIssues()
|
||||||
|
if filter.isSome: ctx.filterIssues(filter.get)
|
||||||
|
|
||||||
|
let projectsCfg = ctx.loadProjectsConfiguration()
|
||||||
|
let projectsDb = ctx.buildDb(projectsCfg)
|
||||||
|
|
||||||
|
var milestones = newCountTable[string]()
|
||||||
|
|
||||||
|
for (context, projects) in pairs(projectsDb):
|
||||||
|
for p in projects:
|
||||||
|
let project = p
|
||||||
|
if values(project.milestones) --> all(it.len == 0):
|
||||||
|
continue
|
||||||
|
|
||||||
|
stdout.writeLine(withColor(project.name, fgBlue, bold = true, bright=true))
|
||||||
|
stdout.writeLine(withColor(
|
||||||
|
"─".repeat(runeLen(stripAnsi(project.name))),
|
||||||
|
fgBlue, bold = true))
|
||||||
|
|
||||||
|
for milestone in project.milestoneOrder:
|
||||||
|
if project.milestones.hasKey(milestone) and
|
||||||
|
project.milestones[milestone].len > 0:
|
||||||
|
let issueCount = project.milestones[milestone].len
|
||||||
|
stdout.writeLine(" " & milestone & " (" & $issueCount & " issues)")
|
||||||
|
|
||||||
|
stdout.writeLine("")
|
||||||
|
|
||||||
|
proc formatProjectIssue(
|
||||||
|
ctx: CliContext,
|
||||||
|
issue: Issue,
|
||||||
|
width: int): seq[string] =
|
||||||
|
|
||||||
|
var firstLine = ""
|
||||||
|
if issue.state == IssueState.Done:
|
||||||
|
firstLine &= withColor(" ✔ ", fgBlack, bold=true, bright=true)
|
||||||
|
else:
|
||||||
|
case issue.getPriority
|
||||||
|
of IssuePriority.essential:
|
||||||
|
firstLine &= withColor("❶ ", fgRed, bold=true, bright=true)
|
||||||
|
of IssuePriority.vital:
|
||||||
|
firstLine &= withColor("❷ ", fgYellow, bold=true, bright=true)
|
||||||
|
of IssuePriority.important:
|
||||||
|
firstLine &= withColor("❸ ", fgBlue, bold=true, bright=true)
|
||||||
|
of IssuePriority.optional:
|
||||||
|
firstLine &= withColor("❹ ", fgBlack, bold=false, bright=true)
|
||||||
|
|
||||||
|
let summaryText = formatSectionIssue(issue, width - 3,
|
||||||
|
bold = [Current, TodoToday].contains(issue.state)).splitLines
|
||||||
|
firstLine &= summaryText[0]
|
||||||
|
|
||||||
|
if issue.state == IssueState.Done:
|
||||||
|
firstLine = withColor(stripAnsi(firstLine), fgBlack, bright=true)
|
||||||
|
|
||||||
|
result.add(firstLine)
|
||||||
|
result.add(summaryText[1 .. ^1] --> map(" " & it))
|
||||||
|
|
||||||
|
if issue.state == IssueState.Done:
|
||||||
|
let origLines = result
|
||||||
|
result = origLines --> map(withColor(stripAnsi(it), fgBlack, bright=true))
|
||||||
|
|
||||||
|
proc formatParentIssue*(
|
||||||
|
ctx: CliContext,
|
||||||
|
parentIssue: Issue,
|
||||||
|
children: seq[Issue],
|
||||||
|
width: int): seq[string] =
|
||||||
|
|
||||||
|
result.add(ctx.formatProjectIssue(parentIssue, width))
|
||||||
|
|
||||||
|
for child in sorted(children, cmp):
|
||||||
|
let childLines = ctx.formatProjectIssue(child, width - 3)
|
||||||
|
result.add(childLines --> map(withColor(" │ ", fgBlack, bright=true) & it))
|
||||||
|
|
||||||
|
result.add("")
|
||||||
|
|
||||||
|
|
||||||
|
proc formatMilestone*(
|
||||||
|
ctx: CliContext,
|
||||||
|
milestone: string,
|
||||||
|
issues: seq[Issue],
|
||||||
|
availWidth: int): seq[string] =
|
||||||
|
|
||||||
|
result = @[""]
|
||||||
|
result.add(withColor(milestone, fgWhite, bold=true))
|
||||||
|
result.add(withColor("─".repeat(availWidth), fgWhite))
|
||||||
|
|
||||||
|
var parentsToChildren = issues -->
|
||||||
|
filter(it.hasProp("parent")) -->
|
||||||
|
group(it["parent"])
|
||||||
|
|
||||||
|
var issuesToFormat = sorted(issues, cmp) -->
|
||||||
|
filter(not it.hasProp("parent"))
|
||||||
|
|
||||||
|
for issue in issuesToFormat:
|
||||||
|
if parentsToChildren.hasKey($issue.id):
|
||||||
|
result.add(
|
||||||
|
ctx.formatParentIssue(issue, parentsToChildren[$issue.id], availWidth))
|
||||||
|
else:
|
||||||
|
result.add(ctx.formatProjectIssue(issue, availWidth))
|
||||||
|
|
||||||
|
proc findShortestColumn(columns: seq[seq[string]]): int =
|
||||||
|
var shortestIdx = 0
|
||||||
|
var shortestLen = columns[0].len
|
||||||
|
|
||||||
|
for i in 1 ..< columns.len:
|
||||||
|
if columns[i].len < shortestLen:
|
||||||
|
shortestLen = columns[i].len
|
||||||
|
shortestIdx = i
|
||||||
|
|
||||||
|
return shortestIdx
|
||||||
|
|
||||||
|
|
||||||
|
proc joinColumns(columns: seq[seq[string]], columnWidth: int): seq[string] =
|
||||||
|
let maxLines = columns --> map(it.len) --> max()
|
||||||
|
|
||||||
|
for lineNo in 0 ..< maxLines:
|
||||||
|
var newLine = ""
|
||||||
|
for col in columns:
|
||||||
|
if lineNo < col.len:
|
||||||
|
let lineLen = runeLen(stripAnsi(col[lineNo]))
|
||||||
|
newLine &= col[lineNo] & " ".repeat(max(0, columnWidth - lineLen) + 2)
|
||||||
|
else:
|
||||||
|
newLine &= " ".repeat(columnWidth + 2)
|
||||||
|
|
||||||
|
result.add(newLine)
|
||||||
|
|
||||||
|
proc showProject*(ctx: CliContext, project: Project) =
|
||||||
|
|
||||||
|
let fullWidth = terminalWidth() - 1
|
||||||
|
let columnWidth = 80
|
||||||
|
let numColumns = (fullWidth - 4) div (columnWidth + 2)
|
||||||
|
|
||||||
|
stdout.writeLine("")
|
||||||
|
stdout.writeLine(withColor(
|
||||||
|
"┌" & "─".repeat(project.name.len + 2) &
|
||||||
|
"┬" & "─".repeat(fullWidth - project.name.len - 4) & "┐",
|
||||||
|
fgBlue, bold=true))
|
||||||
|
stdout.writeLine(
|
||||||
|
withColor("│ ", fgBlue, bold=true) &
|
||||||
|
withColor(project.name, fgBlue, bold=true, bright=true) &
|
||||||
|
withColor(" │" & " ".repeat(fullWidth - project.name.len - 4) & "│", fgBlue, bold=true))
|
||||||
|
stdout.writeLine(withColor(
|
||||||
|
"├" & "─".repeat(project.name.len + 2) &
|
||||||
|
"┘" & " ".repeat(fullWidth - project.name.len - 4) & "│",
|
||||||
|
fgBlue, bold=true))
|
||||||
|
|
||||||
|
let milestoneTexts: seq[seq[string]] = project.milestoneOrder -->
|
||||||
|
filter(project.milestones.hasKey(it) and project.milestones[it].len > 0) -->
|
||||||
|
map(ctx.formatMilestone(it, project.milestones[it], columnWidth))
|
||||||
|
|
||||||
|
var columns: seq[seq[string]] = repeat(newSeq[string](), numColumns)
|
||||||
|
|
||||||
|
for milestoneText in milestoneTexts:
|
||||||
|
let shortestColumnIdx = findShortestColumn(columns)
|
||||||
|
columns[shortestColumnIdx].add(milestoneText)
|
||||||
|
|
||||||
|
let joinedLines = joinColumns(columns, columnWidth)
|
||||||
|
|
||||||
|
for line in joinedLines:
|
||||||
|
let padLen = fullWidth - runeLen(stripAnsi(line)) - 3
|
||||||
|
stdout.writeLine(
|
||||||
|
withColor("│ ", fgBlue) &
|
||||||
|
line &
|
||||||
|
" ".repeat(padLen) &
|
||||||
|
withColor(" │", fgBlue))
|
||||||
|
|
||||||
|
stdout.writeLine(withColor(
|
||||||
|
"└" & "─".repeat(terminalWidth() - 2) & "┘",
|
||||||
|
fgBlue, bold=true))
|
||||||
|
|
||||||
|
|
||||||
|
proc showProjectBoard*(ctx: CliContext, filter = none[IssueFilter]()) =
|
||||||
|
ctx.loadAllIssues()
|
||||||
|
if filter.isSome: ctx.filterIssues(filter.get)
|
||||||
|
|
||||||
|
let projectsCfg = ctx.loadProjectsConfiguration()
|
||||||
|
let projectsDb = ctx.buildDb(projectsCfg)
|
||||||
|
|
||||||
|
for (context, projects) in pairs(projectsDb):
|
||||||
|
if projectsDb.len > 1:
|
||||||
|
stdout.writeLine("")
|
||||||
|
stdout.writeLine(withColor(
|
||||||
|
ctx.getIssueContextDisplayName(context) & ":",
|
||||||
|
fgYellow, bold=true))
|
||||||
|
stdout.writeLine("")
|
||||||
|
|
||||||
|
for p in projects:
|
||||||
|
let project = p
|
||||||
|
if (values(project.milestones) --> exists(it.len > 0)):
|
||||||
|
ctx.showProject(project)
|
||||||
Reference in New Issue
Block a user