Compare commits

...

3 Commits
4.3.0 ... 4.4.1

4 changed files with 129 additions and 36 deletions

View File

@ -12,4 +12,4 @@ bin = @["pit", "pit_api"]
# Dependencies
requires @[ "nim >= 0.18.0", "cliutils 0.4.1", "docopt 0.6.5", "jester 0.2.0",
"timeutils 0.3.0", "uuids 0.1.9" ]
"langutils >= 0.4.0", "timeutils 0.3.0", "uuids 0.1.9" ]

View File

@ -4,6 +4,7 @@
import cliutils, docopt, json, logging, options, os, ospaths, sequtils,
tables, terminal, times, timeutils, unicode, uuids
from nre import re
import strutils except capitalize, toUpper, toLower
import pitpkg/private/libpit
export libpit
@ -20,6 +21,11 @@ type
termWidth*: int
triggerPtk*, verbose*: bool
let EDITOR =
if existsEnv("EDITOR"): getEnv("EDITOR")
else: "vi"
proc initContext(args: Table[string, Value]): CliContext =
let pitCfg = loadConfig(args)
@ -162,16 +168,18 @@ proc writeHeader(ctx: CliContext, header: string) =
stdout.writeLine('~'.repeat(ctx.termWidth))
stdout.resetAttributes
proc reorder(ctx: CliContext, state: IssueState) =
# load the issues to make sure the order file contains all issues in the state.
ctx.loadIssues(state)
discard os.execShellCmd(EDITOR & " '" & (ctx.tasksDir / $state / "order.txt") & "' </dev/tty >/dev/tty")
proc edit(issue: Issue) =
# Write format comments (to help when editing)
writeFile(issue.filepath, toStorageFormat(issue, true))
let editor =
if existsEnv("EDITOR"): getEnv("EDITOR")
else: "vi"
discard os.execShellCmd(editor & " " & issue.filepath & " </dev/tty >/dev/tty")
discard os.execShellCmd(EDITOR & " '" & issue.filepath & "' </dev/tty >/dev/tty")
try:
# Try to parse the newly-edited issue to make sure it was successful.
@ -182,7 +190,7 @@ proc edit(issue: Issue) =
getCurrentExceptionMsg()
issue.store()
proc list(ctx: CliContext, filter: Option[IssueFilter], state: Option[IssueState], today, future, verbose: bool) =
proc list(ctx: CliContext, filter: Option[IssueFilter], state: Option[IssueState], showToday, showFuture, verbose: bool) =
if state.isSome:
ctx.loadIssues(state.get)
@ -193,6 +201,12 @@ proc list(ctx: CliContext, filter: Option[IssueFilter], state: Option[IssueState
ctx.loadAllIssues()
if filter.isSome: ctx.filterIssues(filter.get)
let today = showToday and [Current, TodoToday].anyIt(
ctx.issues.hasKey(it) and ctx.issues[it].len > 0)
let future = showFuture and [Pending, Todo].anyIt(
ctx.issues.hasKey(it) and ctx.issues[it].len > 0)
let indent = if today and future: " " else: ""
# Today's items
@ -227,6 +241,7 @@ Usage:
pit list [<listable>] [options]
pit ( start | done | pending | todo-today | todo | suspend ) <id>... [options]
pit edit <ref>...
pit reorder <state>
pit ( delete | rm ) <id>...
Options:
@ -246,6 +261,12 @@ Options:
-F, --future Limit to future issues.
-m, --match <pattern> Limit to issues whose summaries match the given
pattern (PCRE regex supported).
-M, --match-all <pat> Limit to the issues whose summaries or details
match the given pattern (PCRE regex supported).
-v, --verbose Show issue details when listing issues.
-q, --quiet Suppress verbose output.
@ -314,6 +335,9 @@ Options:
stdout.writeLine ctx.formatIssue(issue)
elif args["reorder"]:
ctx.reorder(parseEnum[IssueState]($args["<state>"]))
elif args["edit"]:
for editRef in @(args["<ref>"]):
@ -380,6 +404,15 @@ Options:
filter.properties = propertiesOption.get
filterOption = some(filter)
# If they supplied text matches, add that to the filter.
if args["--match"]:
filter.summaryMatch = some(re("(?i)" & $args["--match"]))
filterOption = some(filter)
if 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 ctx.defaultContext.isSome and not filter.properties.hasKey("context"):
stderr.writeLine("Limiting to default context: " & ctx.defaultContext.get)

View File

@ -1,7 +1,8 @@
import cliutils, docopt, json, logging, options, os, ospaths, sequtils,
strutils, tables, times, timeutils, uuids
import cliutils, docopt, json, logging, langutils, options, os, ospaths,
sequtils, strutils, tables, times, timeutils, uuids
from nre import find, match, re, Regex
from nre import re, match
type
Issue* = ref object
id*: UUID
@ -19,8 +20,9 @@ type
Dormant = "dormant"
IssueFilter* = ref object
completedRange*: Option[tuple[b, e: DateTime]]
fullMatch*, summaryMatch*: Option[Regex]
properties*: TableRef[string, string]
completedRange*: tuple[b, e: DateTime]
PitConfig* = ref object
tasksDir*: string
@ -62,22 +64,38 @@ proc setDateTime*(issue: Issue, key: string, dt: DateTime) =
proc initFilter*(): IssueFilter =
result = IssueFilter(
properties: newTable[string,string](),
completedRange: (fromUnix(0).local, fromUnix(253400659199).local))
completedRange: none(tuple[b, e: DateTime]),
fullMatch: none(Regex),
summaryMatch: none(Regex),
properties: newTable[string, string]())
proc initFilter*(props: TableRef[string, string]): IssueFilter =
proc propsFilter*(props: TableRef[string, string]): IssueFilter =
if isNil(props):
raise newException(ValueError,
"cannot initialize property filter without properties")
result = IssueFilter(
properties: props,
completedRange: (fromUnix(0).local, fromUnix(253400659199).local))
result = initFilter()
result.properties = props
proc dateFilter*(range: tuple[b, e: DateTime]): IssueFilter =
result = initFilter()
result.completedRange = some(range)
proc summaryMatchFilter*(pattern: string): IssueFilter =
result = initFilter()
result.summaryMatch = some(re("(?i)" & pattern))
proc fullMatchFilter*(pattern: string): IssueFilter =
result = initFilter()
result.fullMatch = some(re("(?i)" & pattern))
proc groupBy*(issues: seq[Issue], propertyKey: string): TableRef[string, seq[Issue]] =
result = newTable[string, seq[Issue]]()
for i in issues:
let key = if i.hasProp(propertyKey): i[propertyKey] else: ""
if not result.hasKey(key): result[key] = newSeq[Issue]()
result[key].add(i)
proc initFilter*(range: tuple[b, e: DateTime]): IssueFilter =
result = IssueFilter(
properties: newTable[string, string](),
completedRange: range)
## Parse and format issues
proc fromStorageFormat*(id: string, issueTxt: string): Issue =
@ -162,38 +180,80 @@ proc store*(tasksDir: string, issue: Issue, state: IssueState, withComments = fa
issue.store()
proc storeOrder*(issues: seq[Issue], path: string) =
var orderLines = newSeq[string]()
for context, issues in issues.groupBy("context"):
orderLines.add("> " & context)
for issue in issues: orderLines.add($issue.id & " " & issue.summary)
orderLines.add("")
let orderFile = path / "order.txt"
orderFile.writeFile(orderLines.join("\n"))
proc loadIssues*(path: string): seq[Issue] =
result = @[]
let orderFile = path / "order.txt"
let orderedIds =
if fileExists(orderFile):
toSeq(orderFile.lines)
.mapIt(it.split(' ')[0])
.deduplicate
.filterIt(not it.startsWith("> ") and not it.isNilOrWhitespace)
else: newSeq[string]()
type TaggedIssue = tuple[issue: Issue, ordered: bool]
var unorderedIssues: seq[TaggedIssue] = @[]
for path in walkDirRec(path):
if extractFilename(path).match(ISSUE_FILE_PATTERN).isSome():
result.add(loadIssue(path))
unorderedIssues.add((loadIssue(path), false))
result = @[]
# Add all ordered issues in order
for id in orderedIds:
let idx = unorderedIssues.indexOf(($it.issue.id).startsWith(id))
if idx > 0:
result.add(unorderedIssues[idx].issue)
unorderedIssues[idx].ordered = true
# Add all remaining, unordered issues in the order they were loaded
for taggedIssue in unorderedIssues:
if taggedIssue.ordered: continue
result.add(taggedIssue.issue)
# Finally, save current order
result.storeOrder(path)
proc changeState*(issue: Issue, tasksDir: string, newState: IssueState) =
let oldFilepath = issue.filepath
if newState == Done: issue.setDateTime("completed", getTime().local)
tasksDir.store(issue, newState)
removeFile(oldFilepath)
if oldFilePath != issue.filepath: removeFile(oldFilepath)
proc delete*(issue: Issue) = removeFile(issue.filepath)
## Utilities for working with issue collections.
proc groupBy*(issues: seq[Issue], propertyKey: string): TableRef[string, seq[Issue]] =
result = newTable[string, seq[Issue]]()
for i in issues:
let key = if i.hasProp(propertyKey): i[propertyKey] else: ""
if not result.hasKey(key): result[key] = newSeq[Issue]()
result[key].add(i)
proc filter*(issues: seq[Issue], filter: IssueFilter): seq[Issue] =
result = issues
for k,v in filter.properties:
result = result.filterIt(it.hasProp(k) and it[k] == v)
result = result.filterIt(not it.hasProp("completed") or
it.getDateTime("completed").between(
filter.completedRange.b,
filter.completedRange.e))
if filter.completedRange.isSome:
let range = filter.completedRange.get
result = result.filterIt(
not it.hasProp("completed") or
it.getDateTime("completed").between(range.b, range.e))
if filter.summaryMatch.isSome:
let p = filter.summaryMatch.get
result = result.filterIt(it.summary.find(p).isSome)
if filter.fullMatch.isSome:
let p = filter.fullMatch.get
result = result.filterIt( it.summary.find(p).isSome or it.details.find(p).isSome)
### Configuration utilities
proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitConfig =

View File

@ -1 +1 @@
const PIT_VERSION = "4.3.0"
const PIT_VERSION = "4.4.1"