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.
This commit is contained in:
		@@ -1,6 +1,6 @@
 | 
			
		||||
# Package
 | 
			
		||||
 | 
			
		||||
version       = "4.20.0"
 | 
			
		||||
version       = "4.21.0"
 | 
			
		||||
author        = "Jonathan Bernard"
 | 
			
		||||
description   = "Personal issue tracker."
 | 
			
		||||
license       = "MIT"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										105
									
								
								src/pit.nim
									
									
									
									
									
								
							
							
						
						
									
										105
									
								
								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["<summary>"],
 | 
			
		||||
      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["<id>"]):
 | 
			
		||||
      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["<id>"]):
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
const PIT_VERSION* = "4.20.0"
 | 
			
		||||
const PIT_VERSION* = "4.21.0"
 | 
			
		||||
 | 
			
		||||
const USAGE* = """Usage:
 | 
			
		||||
  pit ( new | add) <summary> [<state>] [options]
 | 
			
		||||
@@ -9,12 +9,12 @@ const USAGE* = """Usage:
 | 
			
		||||
  pit tag <id>... [options]
 | 
			
		||||
  pit untag <id>... [options]
 | 
			
		||||
  pit reorder <state> [options]
 | 
			
		||||
  pit delegate <id> <delegated-to>
 | 
			
		||||
  pit delegate <id> <delegated-to> [options]
 | 
			
		||||
  pit hide-until <id> <date> [options]
 | 
			
		||||
  pit ( delete | rm ) <id>... [options]
 | 
			
		||||
  pit add-binary-property <id> <propName> <propSource> [options]
 | 
			
		||||
  pit get-binary-property <id> <propName> <propDest> [options]
 | 
			
		||||
  pit help
 | 
			
		||||
  pit help [options]
 | 
			
		||||
 | 
			
		||||
Options:
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +38,13 @@ Options:
 | 
			
		||||
                            separate names. For example: -C ctx1,ctx2
 | 
			
		||||
                            Shorthand for '-P context:<ctx>'
 | 
			
		||||
 | 
			
		||||
  -g, --tags <tags>         Specify tags for an issue.
 | 
			
		||||
  -g, --tags <tags>         Specify tags for an issue. Tags are specified as a
 | 
			
		||||
                            comma-delimited list. For example: -g tag1,tag2
 | 
			
		||||
 | 
			
		||||
  -G, --excl-tags <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:<tags-value>'.
 | 
			
		||||
"""
 | 
			
		||||
"""
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = @[
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user