|
|
@ -1,8 +1,9 @@
|
|
|
|
import std/json, std/logging, std/options, std/os, std/sequtils, std/strformat,
|
|
|
|
import std/[json, logging, options, os, strformat, strutils, tables, times]
|
|
|
|
std/strutils, std/tables, std/times
|
|
|
|
import cliutils, docopt, langutils, uuids, zero_functional
|
|
|
|
import cliutils, docopt, langutils, timeutils, uuids
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import nre except toSeq
|
|
|
|
import nre except toSeq
|
|
|
|
|
|
|
|
import timeutils except `>`
|
|
|
|
|
|
|
|
from sequtils import deduplicate, toSeq
|
|
|
|
|
|
|
|
|
|
|
|
type
|
|
|
|
type
|
|
|
|
Issue* = ref object
|
|
|
|
Issue* = ref object
|
|
|
@ -11,6 +12,7 @@ type
|
|
|
|
summary*, details*: string
|
|
|
|
summary*, details*: string
|
|
|
|
properties*: TableRef[string, string]
|
|
|
|
properties*: TableRef[string, string]
|
|
|
|
tags*: seq[string]
|
|
|
|
tags*: seq[string]
|
|
|
|
|
|
|
|
state*: IssueState
|
|
|
|
|
|
|
|
|
|
|
|
IssueState* = enum
|
|
|
|
IssueState* = enum
|
|
|
|
Current = "current",
|
|
|
|
Current = "current",
|
|
|
@ -201,12 +203,13 @@ proc fromStorageFormat*(id: string, issueTxt: string): Issue =
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let parts = line.split({':'}, 1).mapIt(it.strip())
|
|
|
|
let parts = line.split({':'}, 1) --> map(it.strip())
|
|
|
|
if parts.len != 2:
|
|
|
|
if parts.len != 2:
|
|
|
|
raise newException(ValueError, "unable to parse property line: " & line)
|
|
|
|
raise newException(ValueError, "unable to parse property line: " & line)
|
|
|
|
|
|
|
|
|
|
|
|
# Take care of special properties: `tags`
|
|
|
|
# Take care of special properties: `tags`
|
|
|
|
if parts[0] == "tags": result.tags = parts[1].split({','}).mapIt(it.strip())
|
|
|
|
if parts[0] == "tags":
|
|
|
|
|
|
|
|
result.tags = parts[1].split({','}) --> map(it.strip())
|
|
|
|
else: result[parts[0]] = parts[1]
|
|
|
|
else: result[parts[0]] = parts[1]
|
|
|
|
|
|
|
|
|
|
|
|
of ReadingDetails:
|
|
|
|
of ReadingDetails:
|
|
|
@ -234,6 +237,11 @@ proc loadIssue*(filePath: string): Issue =
|
|
|
|
result = fromStorageFormat(splitFile(filePath).name, readFile(filePath))
|
|
|
|
result = fromStorageFormat(splitFile(filePath).name, readFile(filePath))
|
|
|
|
result.filepath = filePath
|
|
|
|
result.filepath = filePath
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let parentDirName = filePath.splitFile().dir.splitFile().name
|
|
|
|
|
|
|
|
let issueState = IssueState.items.toSeq --> find($it == parentDirName)
|
|
|
|
|
|
|
|
if issueState.isSome: result.state = issueState.get
|
|
|
|
|
|
|
|
else: result.state = IssueState.Done
|
|
|
|
|
|
|
|
|
|
|
|
proc loadIssueById*(tasksDir, id: string): Issue =
|
|
|
|
proc loadIssueById*(tasksDir, id: string): Issue =
|
|
|
|
for path in walkDirRec(tasksDir):
|
|
|
|
for path in walkDirRec(tasksDir):
|
|
|
|
if path.splitFile.name.startsWith(id):
|
|
|
|
if path.splitFile.name.startsWith(id):
|
|
|
@ -273,10 +281,10 @@ proc loadIssues*(path: string): seq[Issue] =
|
|
|
|
|
|
|
|
|
|
|
|
let orderedIds =
|
|
|
|
let orderedIds =
|
|
|
|
if fileExists(orderFile):
|
|
|
|
if fileExists(orderFile):
|
|
|
|
toSeq(orderFile.lines)
|
|
|
|
(orderFile.lines.toSeq -->
|
|
|
|
.mapIt(it.split(' ')[0])
|
|
|
|
map(it.split(' ')[0]).
|
|
|
|
.deduplicate
|
|
|
|
filter(not it.startsWith("> ") and not it.isEmptyOrWhitespace)).
|
|
|
|
.filterIt(not it.startsWith("> ") and not it.isEmptyOrWhitespace)
|
|
|
|
deduplicate()
|
|
|
|
else: newSeq[string]()
|
|
|
|
else: newSeq[string]()
|
|
|
|
|
|
|
|
|
|
|
|
type TaggedIssue = tuple[issue: Issue, ordered: bool]
|
|
|
|
type TaggedIssue = tuple[issue: Issue, ordered: bool]
|
|
|
@ -332,6 +340,7 @@ proc nextRecurrence*(tasksDir: string, rec: Recurrence, defaultIssue: Issue): Is
|
|
|
|
|
|
|
|
|
|
|
|
result = Issue(
|
|
|
|
result = Issue(
|
|
|
|
id: genUUID(),
|
|
|
|
id: genUUID(),
|
|
|
|
|
|
|
|
state: baseIssue.state,
|
|
|
|
summary: baseIssue.summary,
|
|
|
|
summary: baseIssue.summary,
|
|
|
|
properties: newProps,
|
|
|
|
properties: newProps,
|
|
|
|
tags: baseIssue.tags)
|
|
|
|
tags: baseIssue.tags)
|
|
|
@ -354,36 +363,38 @@ proc nextRecurrence*(tasksDir: string, rec: Recurrence, defaultIssue: Issue): Is
|
|
|
|
|
|
|
|
|
|
|
|
## Utilities for working with issue collections.
|
|
|
|
## Utilities for working with issue collections.
|
|
|
|
proc filter*(issues: seq[Issue], filter: IssueFilter): seq[Issue] =
|
|
|
|
proc filter*(issues: seq[Issue], filter: IssueFilter): seq[Issue] =
|
|
|
|
result = issues
|
|
|
|
var f: seq[Issue] = issues
|
|
|
|
|
|
|
|
|
|
|
|
for k,v in filter.properties:
|
|
|
|
for k,v in filter.properties:
|
|
|
|
result = result.filterIt(it.hasProp(k) and it[k] == v)
|
|
|
|
f = f --> filter(it.hasProp(k) and it[k] == v)
|
|
|
|
|
|
|
|
|
|
|
|
for k,v in filter.exclProperties:
|
|
|
|
for k,v in filter.exclProperties:
|
|
|
|
result = result.filter(proc (iss: Issue): bool =
|
|
|
|
f = f --> filter(not (it.hasProp(k) and v.contains(it[k])))
|
|
|
|
not iss.hasProp(k) or
|
|
|
|
|
|
|
|
not v.anyIt(it == iss[k])
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if filter.completedRange.isSome:
|
|
|
|
if filter.completedRange.isSome:
|
|
|
|
let range = filter.completedRange.get
|
|
|
|
let range = filter.completedRange.get
|
|
|
|
result = result.filterIt(
|
|
|
|
f = f --> filter(
|
|
|
|
not it.hasProp("completed") or
|
|
|
|
not it.hasProp("completed") or
|
|
|
|
it.getDateTime("completed").between(range.b, range.e))
|
|
|
|
it.getDateTime("completed").between(range.b, range.e))
|
|
|
|
|
|
|
|
|
|
|
|
if filter.summaryMatch.isSome:
|
|
|
|
if filter.summaryMatch.isSome:
|
|
|
|
let p = filter.summaryMatch.get
|
|
|
|
let p = filter.summaryMatch.get
|
|
|
|
result = result.filterIt(it.summary.find(p).isSome)
|
|
|
|
f = f --> filter(it.summary.find(p).isSome)
|
|
|
|
|
|
|
|
|
|
|
|
if filter.fullMatch.isSome:
|
|
|
|
if filter.fullMatch.isSome:
|
|
|
|
let p = filter.fullMatch.get
|
|
|
|
let p = filter.fullMatch.get
|
|
|
|
result = result.filterIt( it.summary.find(p).isSome or it.details.find(p).isSome)
|
|
|
|
f = f -->
|
|
|
|
|
|
|
|
filter(it.summary.find(p).isSome or it.details.find(p).isSome)
|
|
|
|
|
|
|
|
|
|
|
|
for tag in filter.hasTags:
|
|
|
|
for tagLent in filter.hasTags:
|
|
|
|
result = result.filterIt(it.tags.find(tag) >= 0)
|
|
|
|
let tag = tagLent
|
|
|
|
|
|
|
|
f = f --> filter(it.tags.find(tag) >= 0)
|
|
|
|
|
|
|
|
|
|
|
|
for exclTag in filter.exclTags:
|
|
|
|
for exclTagLent in filter.exclTags:
|
|
|
|
result = result.filterIt(it.tags.find(exclTag) < 0)
|
|
|
|
let exclTag = exclTagLent
|
|
|
|
|
|
|
|
f = f --> filter(it.tags.find(exclTag) < 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return f # not using result because zero_functional doesn't play nice with it
|
|
|
|
|
|
|
|
|
|
|
|
proc find*(
|
|
|
|
proc find*(
|
|
|
|
issues: TableRef[IssueState, seq[Issue]],
|
|
|
|
issues: TableRef[IssueState, seq[Issue]],
|
|
|
@ -400,7 +411,7 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitCo
|
|
|
|
".pitrc", $getEnv("PITRC"), $getEnv("HOME") & "/.pitrc"]
|
|
|
|
".pitrc", $getEnv("PITRC"), $getEnv("HOME") & "/.pitrc"]
|
|
|
|
|
|
|
|
|
|
|
|
var pitrcFilename: string =
|
|
|
|
var pitrcFilename: string =
|
|
|
|
foldl(pitrcLocations, if len(a) > 0: a elif fileExists(b): b else: "")
|
|
|
|
pitrcLocations --> fold("", if fileExists(it): it else: a)
|
|
|
|
|
|
|
|
|
|
|
|
if not fileExists(pitrcFilename):
|
|
|
|
if not fileExists(pitrcFilename):
|
|
|
|
warn "could not find .pitrc file: " & pitrcFilename
|
|
|
|
warn "could not find .pitrc file: " & pitrcFilename
|
|
|
|