Compare commits

...

6 Commits

5 changed files with 37 additions and 31 deletions

View File

@ -46,7 +46,7 @@ Some other common properties I use are:
- `resolution`: for short notes about why an issue was moved to `done`,
especially if it the action wasn't taken or if it is not completely clear
that this issue was completed.
that this issue was completed.
## Configuration Options
@ -107,7 +107,7 @@ in the configuration file. All options are optional unless stated otherwise.
- `defaultContext`: if present all invokations to the CLI will
be in this context. This is like adding a `--context <defaultContext>`
parameter to every CLI invocation. Any actual `--context` parameter will
override this value.
override this value.
- `verbose`: Show issue details when listing issues (same as
`--verbose` flag).
@ -135,7 +135,4 @@ in the configuration file. All options are optional unless stated otherwise.
"Personal"; it does not need an alternate display name.
* `tasksDir` **required**: a file path to the root directory for the issue
repository (same as `--tasks-dir` CLI parameter).
- CLI parameter: *cannot be specified via CLI*
- config file key: `contexts`
repository (same as `--tasks-dir` CLI parameter).

View File

@ -1,6 +1,6 @@
# Package
version = "4.9.1"
version = "4.11.0"
author = "Jonathan Bernard"
description = "Personal issue tracker."
license = "MIT"
@ -10,9 +10,9 @@ bin = @["pit", "pit_api"]
# Dependencies
requires @[
"nim >= 0.19.0",
"nim >= 1.4.0",
"docopt 0.6.8",
"jester 0.4.1",
"jester 0.5.0",
"uuids 0.1.10",
"https://git.jdb-labs.com/jdb/nim-cli-utils.git >= 0.6.4",

View File

@ -1,7 +1,7 @@
## Personal Issue Tracker CLI interface
## ====================================
import cliutils, docopt, json, logging, options, os, sequtils,
import algorithm, cliutils, docopt, json, logging, options, os, sequtils,
std/wordwrap, tables, terminal, times, timeutils, unicode, uuids
from nre import re
@ -199,6 +199,10 @@ proc list(ctx: CliContext, filter: Option[IssueFilter], state: Option[IssueState
if state.isSome:
ctx.loadIssues(state.get)
if filter.isSome: ctx.filterIssues(filter.get)
if state.get == Done and showToday:
ctx.issues[Done] = ctx.issues[Done].filterIt(
it.hasProp("completed") and
sameDay(getTime().local, it.getDateTime("completed")))
stdout.write ctx.formatSection(ctx.issues[state.get], state.get, "", verbose)
return
@ -221,13 +225,6 @@ proc list(ctx: CliContext, filter: Option[IssueFilter], state: Option[IssueState
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
stdout.write ctx.formatSection(ctx.issues[s], s, indent, verbose)
if ctx.issues.hasKey(Done):
let doneIssues = ctx.issues[Done].filterIt(
it.hasProp("completed") and
sameDay(getTime().local, it.getDateTime("completed")))
if doneIssues.len > 0:
stdout.write ctx.formatSection(doneIssues, Done, indent, verbose)
# Future items
if future:
if today: ctx.writeHeader("Future")
@ -242,7 +239,8 @@ when isMainModule:
let doc = """
Usage:
pit ( new | add) <summary> [<state>] [options]
pit list [<listable>] [options]
pit list contexts
pit list [<stateOrId>] [options]
pit ( start | done | pending | todo-today | todo | suspend ) <id>... [options]
pit edit <ref>...
pit tag <id>... [options]
@ -465,6 +463,10 @@ Options:
filter.properties["context"] = ctx.defaultContext.get
filterOption = some(filter)
if args["--tags"]:
filter.hasTags = ($args["--tags"]).split(',')
filterOption = some(filter)
# Finally, if the "context" is "all", don't filter on context
if filter.properties.hasKey("context") and
filter.properties["context"] == "all":
@ -475,11 +477,10 @@ Options:
var stateOption = none(IssueState)
var issueIdOption = none(string)
if args["<listable>"]:
if $args["<listable>"] == "contexts": listContexts = true
else:
try: stateOption = some(parseEnum[IssueState]($args["<listable>"]))
except: issueIdOption = some($args["<listable>"])
if args["contexts"]: listContexts = true
elif args["<stateOrId>"]:
try: stateOption = some(parseEnum[IssueState]($args["<stateOrId>"]))
except: issueIdOption = some($args["<stateOrId>"])
# List the known contexts
if listContexts:
@ -495,7 +496,7 @@ Options:
else: b
).len
for c in uniqContexts:
for c in uniqContexts.sorted:
stdout.writeLine(c.alignLeft(maxLen+2) & ctx.getIssueContextDisplayName(c))
# List a specific issue

View File

@ -22,6 +22,7 @@ type
IssueFilter* = ref object
completedRange*: Option[tuple[b, e: DateTime]]
fullMatch*, summaryMatch*: Option[Regex]
hasTags*: seq[string]
properties*: TableRef[string, string]
PitConfig* = ref object
@ -69,6 +70,7 @@ proc initFilter*(): IssueFilter =
completedRange: none(tuple[b, e: DateTime]),
fullMatch: none(Regex),
summaryMatch: none(Regex),
hasTags: @[],
properties: newTable[string, string]())
proc propsFilter*(props: TableRef[string, string]): IssueFilter =
@ -91,6 +93,10 @@ proc fullMatchFilter*(pattern: string): IssueFilter =
result = initFilter()
result.fullMatch = some(re("(?i)" & pattern))
proc hasTagsFilter*(tags: seq[string]): IssueFilter =
result = initFilter()
result.hasTags = tags
proc groupBy*(issues: seq[Issue], propertyKey: string): TableRef[string, seq[Issue]] =
result = newTable[string, seq[Issue]]()
for i in issues:
@ -170,6 +176,7 @@ proc loadIssueById*(tasksDir, id: string): Issue =
raise newException(KeyError, "cannot find issue for id: " & id)
proc store*(issue: Issue, withComments = false) =
discard existsOrCreateDir(issue.filePath.parentDir)
writeFile(issue.filepath, toStorageFormat(issue, withComments))
proc store*(tasksDir: string, issue: Issue, state: IssueState, withComments = false) =
@ -258,6 +265,9 @@ proc filter*(issues: seq[Issue], filter: IssueFilter): seq[Issue] =
let p = filter.fullMatch.get
result = result.filterIt( it.summary.find(p).isSome or it.details.find(p).isSome)
for tag in filter.hasTags:
result = result.filterIt(it.tags.find(tag) >= 0)
### Configuration utilities
proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitConfig =
let pitrcLocations = @[
@ -265,9 +275,9 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitCo
".pitrc", $getEnv("PITRC"), $getEnv("HOME") & "/.pitrc"]
var pitrcFilename: string =
foldl(pitrcLocations, if len(a) > 0: a elif existsFile(b): b else: "")
foldl(pitrcLocations, if len(a) > 0: a elif fileExists(b): b else: "")
if not existsFile(pitrcFilename):
if not fileExists(pitrcFilename):
warn "pit: could not find .pitrc file: " & pitrcFilename
if isEmptyOrWhitespace(pitrcFilename):
pitrcFilename = $getEnv("HOME") & "/.pitrc"
@ -298,12 +308,10 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitCo
if isEmptyOrWhitespace(result.tasksDir):
raise newException(Exception, "no tasks directory configured")
if not existsDir(result.tasksDir):
if not dirExists(result.tasksDir):
raise newException(Exception, "cannot find tasks dir: " & result.tasksDir)
# Create our tasks directory structure if needed
for s in IssueState:
if not existsDir(result.tasksDir / $s):
if not dirExists(result.tasksDir / $s):
(result.tasksDir / $s).createDir

View File

@ -1 +1 @@
const PIT_VERSION* = "4.9.1"
const PIT_VERSION* = "4.11.0"