From 7215b4969b9224d2c3de33fae54ccfa79885694e Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Sun, 31 Jul 2022 20:01:39 -0500 Subject: [PATCH] Re-design output to make skimming easier. - We now always protect the left margin when printing task details (including tags) to make it easier to skim down that line. - Also made the actual summary always follow immediately after the ID, to align to that skimmable line. - Moved the information about the delegatee to the end of the summary, next to the tags, and changed the color of the delegatee to make it easier to distinguish. - Added the `-G` option, to allow filtering out issues matching any of the provided tags. - We now allow options to be passed to both the `delegate` and `help` command. Any options are ignored, but this allows the use of tools like `cmd_shell` which always wrap commands with the pre-given options. --- pit.nimble | 2 +- src/pit.nim | 105 ++++++++++++++++++++++------------ src/pitpkg/cliconstants.nim | 16 ++++-- src/pitpkg/private/libpit.nim | 10 +++- 4 files changed, 88 insertions(+), 45 deletions(-) diff --git a/pit.nimble b/pit.nimble index 7da5e8b..2dcdb95 100644 --- a/pit.nimble +++ b/pit.nimble @@ -1,6 +1,6 @@ # Package -version = "4.20.0" +version = "4.21.0" author = "Jonathan Bernard" description = "Personal issue tracker." license = "MIT" diff --git a/src/pit.nim b/src/pit.nim index b7dfa9d..fe0d7c9 100644 --- a/src/pit.nim +++ b/src/pit.nim @@ -1,8 +1,9 @@ ## Personal Issue Tracker CLI interface ## ==================================== -import algorithm, cliutils, data_uri, docopt, json, logging, options, os, - sequtils, std/wordwrap, tables, terminal, times, timeutils, unicode, uuids +import std/algorithm, std/logging, std/options, std/os, std/sequtils, + std/wordwrap, std/tables, std/terminal, std/times, std/unicode +import cliutils, data_uri, docopt, json, timeutils, uuids from nre import re import strutils except alignLeft, capitalize, strip, toUpper, toLower @@ -71,46 +72,68 @@ proc formatIssue(ctx: CliContext, issue: Issue): string = result &= termReset -proc formatSectionIssue(ctx: CliContext, issue: Issue, width: int, indent = "", - verbose = false): string = - - result = "" - - var showDetails = not issue.details.isEmptyOrWhitespace and verbose - - var prefixLen = 0 - var summaryIndentLen = indent.len + 7 - - if issue.hasProp("delegated-to"): prefixLen += issue["delegated-to"].len + 2 # space for the ':' and ' ' - - # Wrap and write the summary. - var wrappedSummary = ("+".repeat(prefixLen) & issue.summary).wrapWords(width - summaryIndentLen).indent(summaryIndentLen) - - wrappedSummary = wrappedSummary[(prefixLen + summaryIndentLen)..^1] +proc formatSectionIssue( + ctx: CliContext, + issue: Issue, + width: int, + indent = "", + verbose = false): string = result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true) & " " - if issue.hasProp("delegated-to"): - result &= (issue["delegated-to"] & ": ").withColor(fgGreen) + let showDetails = not issue.details.isEmptyOrWhitespace and verbose - result &= wrappedSummary.withColor(fgWhite) + let summaryIndentLen = indent.len + 7 + let summaryWidth = width - summaryIndentLen + + let summaryLines = issue.summary + .wrapWords(summaryWidth) + .splitLines + + result &= summaryLines[0].withColor(fgWhite) + + for line in summaryLines[1..^1]: + result &= "\p" & line.indent(summaryIndentLen) + + var lastLineLen = summaryLines[^1].len + + if issue.hasProp("delegated-to"): + if lastLineLen + issue["delegated-to"].len + 1 < summaryWidth: + result &= " " & issue["delegated-to"].withColor(fgMagenta) + lastLineLen += issue["delegated-to"].len + 1 + else: + result &= "\p" & issue["delegated-to"] + .withColor(fgMagenta) + .indent(summaryIndentLen) + lastLineLen = issue["delegated-to"].len if issue.tags.len > 0: - let tagsStr = "(" & issue.tags.join(", ") & ")" - if (result.splitLines[^1].len + tagsStr.len + 1) > (width - 2): - result &= "\n" & indent - result &= " " & tagsStr.withColor(fgGreen) + let tagsStrLines = ("(" & issue.tags.join(", ") & ")") + .wrapWords(summaryWidth) + .splitLines + if tagsStrLines.len == 1 and + (lastLineLen + tagsStrLines[0].len + 1) < summaryWidth: + result &= " " & tagsStrLines[0].withColor(fgGreen) + lastLineLen += tagsStrLines[0].len + 1 + else: + result &= "\p" & tagsStrLines + .mapIt(it.indent(summaryIndentLen)) + .join("\p") + .withColor(fgGreen) + lastLineLen = tagsStrLines[^1].len if issue.hasProp("pending"): - let startIdx = "Pending: ".len - var pendingText = issue["pending"].wrapWords(width - startIdx - summaryIndentLen) - .indent(startIdx) - pendingText = ("Pending: " & pendingText[startIdx..^1]).indent(summaryIndentLen) - result &= "\n" & pendingText.withColor(fgCyan) + result &= "\p" & ("Pending: " & issue["pending"]) + .wrapwords(summaryWidth) + .withColor(fgCyan) + .indent(summaryIndentLen) if showDetails: - result &= "\n" & issue.details.strip.indent(indent.len + 2).withColor(fgCyan) + result &= "\p" & issue.details + .strip + .withColor(fgBlack, bright = true) + .indent(summaryIndentLen) result &= termReset @@ -306,6 +329,7 @@ when isMainModule: var propertiesOption = none(TableRef[string,string]) var exclPropsOption = none(TableRef[string,seq[string]]) var tagsOption = none(seq[string]) + var exclTagsOption = none(seq[string]) if args["--properties"] or args["--context"]: @@ -333,6 +357,9 @@ when isMainModule: if args["--tags"]: tagsOption = some(($args["--tags"]).split(",").mapIt(it.strip)) + if args["--excl-tags"]: exclTagsOption = + some(($args["--excl-tags"]).split(",").mapIt(it.strip)) + ## Actual command runners if args["new"] or args["add"]: let state = @@ -350,7 +377,7 @@ when isMainModule: summary: $args[""], properties: issueProps, tags: - if args["--tags"]: ($args["--tags"]).split(",").mapIt(it.strip) + if tagsOption.isSome: tagsOption.get else: newSeq[string]()) ctx.tasksDir.store(issue, state) @@ -376,9 +403,9 @@ when isMainModule: else: edit(ctx.tasksDir.loadIssueById(editRef)) elif args["tag"]: - if not args["--tags"]: raise newException(Exception, "no tags given") + if tagsOption.isNone: raise newException(Exception, "no tags given") - let newTags = ($args["--tags"]).split(",").mapIt(it.strip) + let newTags = tagsOption.get for id in @(args[""]): var issue = ctx.tasksDir.loadIssueById(id) @@ -387,7 +414,7 @@ when isMainModule: elif args["untag"]: let tagsToRemove: seq[string] = - if args["--tags"]: ($args["--tags"]).split(",").mapIt(it.strip) + if tagsOption.isSome: tagsOption.get else: @[] for id in @(args[""]): @@ -498,8 +525,12 @@ when isMainModule: filter.properties["context"] = ctx.defaultContext.get filterOption = some(filter) - if args["--tags"]: - filter.hasTags = ($args["--tags"]).split(',') + if tagsOption.isSome: + filter.hasTags = tagsOption.get + filterOption = some(filter) + + if exclTagsOption.isSome: + filter.exclTags = exclTagsOption.get filterOption = some(filter) # Finally, if the "context" is "all", don't filter on context diff --git a/src/pitpkg/cliconstants.nim b/src/pitpkg/cliconstants.nim index bd6ce94..69cd8f2 100644 --- a/src/pitpkg/cliconstants.nim +++ b/src/pitpkg/cliconstants.nim @@ -1,4 +1,4 @@ -const PIT_VERSION* = "4.20.0" +const PIT_VERSION* = "4.21.0" const USAGE* = """Usage: pit ( new | add) [] [options] @@ -9,12 +9,12 @@ const USAGE* = """Usage: pit tag ... [options] pit untag ... [options] pit reorder [options] - pit delegate + pit delegate [options] pit hide-until [options] pit ( delete | rm ) ... [options] pit add-binary-property [options] pit get-binary-property [options] - pit help + pit help [options] Options: @@ -38,7 +38,13 @@ Options: separate names. For example: -C ctx1,ctx2 Shorthand for '-P context:' - -g, --tags Specify tags for an issue. + -g, --tags Specify tags for an issue. Tags are specified as a + comma-delimited list. For example: -g tag1,tag2 + + -G, --excl-tags When used with the list command, exclude issues + that contain any of the provided tags. Tags are + specified as a comma-delimited list. + For example: -G tag1,tag2 -T, --today Limit to today's issues. @@ -166,4 +172,4 @@ Issue Properties: If present, expected to be a comma-delimited list of text tags. The -g option is a short-hand for '-p tags:'. -""" \ No newline at end of file +""" diff --git a/src/pitpkg/private/libpit.nim b/src/pitpkg/private/libpit.nim index 9c46af7..9809cce 100644 --- a/src/pitpkg/private/libpit.nim +++ b/src/pitpkg/private/libpit.nim @@ -1,5 +1,6 @@ -import cliutils, docopt, json, logging, langutils, options, os, - sequtils, strformat, strutils, tables, times, timeutils, uuids +import std/json, std/logging, std/options, std/os, std/sequtils, std/strformat, + std/strutils, std/tables, std/times +import cliutils, docopt, langutils, timeutils, uuids import nre except toSeq @@ -23,6 +24,7 @@ type completedRange*: Option[tuple[b, e: DateTime]] fullMatch*, summaryMatch*: Option[Regex] hasTags*: seq[string] + exclTags*: seq[string] properties*: TableRef[string, string] exclProperties*: TableRef[string, seq[string]] @@ -115,6 +117,7 @@ proc initFilter*(): IssueFilter = fullMatch: none(Regex), summaryMatch: none(Regex), hasTags: @[], + exclTags: @[], properties: newTable[string, string](), exclProperties: newTable[string,seq[string]]()) @@ -372,6 +375,9 @@ proc filter*(issues: seq[Issue], filter: IssueFilter): seq[Issue] = for tag in filter.hasTags: result = result.filterIt(it.tags.find(tag) >= 0) + for exclTag in filter.exclTags: + result = result.filterIt(it.tags.find(exclTag) < 0) + ### Configuration utilities proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitConfig = let pitrcLocations = @[