Compare commits

..

11 Commits

Author SHA1 Message Date
34ce2b61b9 When creating new recurrences, put them in the TodoToday state, not Todo. 2023-07-06 08:07:22 -05:00
661d5959c6 Add show-dupes command, fix BareExcept warnings. 2023-05-19 09:24:53 -05:00
6665f09b7b Fixed missed version bump in cliconstants. 2023-05-19 09:05:02 -05:00
bcb1c7c17c Extract logic for locating the config file to the cliutils library. 2023-05-13 07:30:25 -05:00
b0e3f5a9d8 Expose issue formating functionality. 2023-03-21 11:11:44 -05:00
fee4ba70a6 Update state field when changing an issue's state. 2023-03-21 10:27:25 -05:00
171adbb59d Make IssueState available as a field on Issue.
* Add `state` on `Issue` to be able to query the state of an issue even
  if you only have a reference to this issue and don't have a reference
  to the context or issues table. This does not change the persisted
  format of the issue. On disk the state of an issue is still
  represented by it's location in the file hierarchy.

* Refactored libpit to use zero_functional instead of sequtils.
2023-03-21 08:30:29 -05:00
d01d6e37f4 Update timeutils version to include support for the shorter ISO8601 date format. 2023-02-28 23:29:06 -06:00
b98596574d Add find utility method for searching for issues among multiple issue states. 2023-02-17 12:12:13 -06:00
ea9f8ea7ac Move issue loading logic into the publicly-exposed library methods. 2023-02-16 11:07:09 -06:00
ae4a943e82 Allow access to pit functionality as a Nim libaray. 2023-02-16 09:07:02 -06:00
5 changed files with 307 additions and 86 deletions

174
nimble.lock Normal file
View File

@ -0,0 +1,174 @@
{
"version": 2,
"packages": {
"asynctools": {
"version": "0.1.1",
"vcsRevision": "0e6bdc3ed5bae8c7cc9e03cfbf66b7c882a908a7",
"url": "https://github.com/cheatfate/asynctools",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "54314dceabb06b20908ecb0f2c007e9ff3aaa054"
}
},
"isaac": {
"version": "0.1.3",
"vcsRevision": "45a5cbbd54ff59ba3ed94242620c818b9aad1b5b",
"url": "https://github.com/pragmagic/isaac/",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "05c3583a954715d84b0bf1be97f9a503249e9cdf"
}
},
"uuids": {
"version": "0.1.11",
"vcsRevision": "8cb8720b567c6bcb261bd1c0f7491bdb5209ad06",
"url": "https://github.com/pragmagic/uuids/",
"downloadMethod": "git",
"dependencies": [
"isaac"
],
"checksums": {
"sha1": "393f5fcefbc8ad3cf167e59760144208ff8f9f76"
}
},
"unicodedb": {
"version": "0.11.2",
"vcsRevision": "c70f8bc8c7373265670e0575bc5eda36fe3761b0",
"url": "https://github.com/nitely/nim-unicodedb",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "612c5955f91bd90a263ce914d1d5de74a33af3c6"
}
},
"regex": {
"version": "0.20.1",
"vcsRevision": "66f144f935cc73977c61185fab15a3147bf117ff",
"url": "https://github.com/nitely/nim-regex",
"downloadMethod": "git",
"dependencies": [
"unicodedb"
],
"checksums": {
"sha1": "ea9b6600443e73b1ea89a477c7a5d1fce742c9da"
}
},
"docopt": {
"version": "0.7.0",
"vcsRevision": "17803d1205f9e752cce03a66b0a29b710520398e",
"url": "https://github.com/docopt/docopt.nim",
"downloadMethod": "git",
"dependencies": [
"regex"
],
"checksums": {
"sha1": "21150284640b882fa91147181c52da3e5bb44df8"
}
},
"filetype": {
"version": "0.9.0",
"vcsRevision": "1fe1e7d988cd802abc26505efb5a91891bd6f53e",
"url": "https://github.com/jiro4989/filetype",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "d2242b94eeb0f6d3810a8c71af4664f28853e1be"
}
},
"zero_functional": {
"version": "1.3.0",
"vcsRevision": "edf3b7f59119f75706da435c2b7f080a0cf4960c",
"url": "https://github.com/zero-functional/zero-functional",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "2dc01ca0925ac1c2dcb46a0c6d9c93c57a9cddec"
}
},
"update_nim_package_version": {
"version": "0.2.0",
"vcsRevision": "5a78579fd7f88014263aed38c60327c85f6f8bcf",
"url": "https://git.jdb-software.com/jdb/update-nim-package-version",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "37052b7ce30d5493ef24253a82a6087350b4eabb"
}
},
"data_uri": {
"version": "1.0.2",
"vcsRevision": "f43ac66e44c37edd3cc7282d75d6fa2fa031b2ec",
"url": "",
"downloadMethod": "git",
"dependencies": [
"docopt",
"filetype",
"zero_functional",
"update_nim_package_version"
],
"checksums": {
"sha1": "949c11ffab4e85ff538b0bd3e5bb193f118b56d7"
}
},
"timeutils": {
"version": "0.5.4",
"vcsRevision": "a9308cbaf3c89496b5832ddd18404dc0debe66a2",
"url": "https://git.jdb-software.com/jdb/nim-time-utils.git",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "9ecd1020f5644bc59acb2f44ca9d30f4c7f066d3"
}
},
"cliutils": {
"version": "0.8.0",
"vcsRevision": "b1cc4fbe51d5e617789363efe716793ebe5bc5f1",
"url": "https://git.jdb-software.com/jdb/nim-cli-utils",
"downloadMethod": "git",
"dependencies": [
"docopt"
],
"checksums": {
"sha1": "5b114094c314007fa6f15e62852d62a58a3cbb62"
}
},
"langutils": {
"version": "0.4.0",
"vcsRevision": "8122660da3fc78132b823e76c9e990fd92802b0e",
"url": "https://git.jdb-software.com/jdb/nim-lang-utils.git",
"downloadMethod": "git",
"dependencies": [],
"checksums": {
"sha1": "2da09deb0e0bfc186000f0d941d06dd974bf6e58"
}
},
"httpbeast": {
"version": "0.4.1",
"vcsRevision": "abc13d11c210b614960fe8760e581d44cfb2e3e9",
"url": "https://github.com/dom96/httpbeast",
"downloadMethod": "git",
"dependencies": [
"asynctools"
],
"checksums": {
"sha1": "b23e57a401057dcb9b7fae1fb8279a6a2ce1d0b8"
}
},
"jester": {
"version": "0.5.0",
"vcsRevision": "a21b36a02b7745d6cdcda32d4ab3fba328cda17a",
"url": "https://github.com/dom96/jester/",
"downloadMethod": "git",
"dependencies": [
"httpbeast",
"asynctools"
],
"checksums": {
"sha1": "a192ca25bfc05d5de5c9a5fafca3b0cee47d82d2"
}
}
},
"tasks": {}
}

View File

@ -1,10 +1,11 @@
# Package
version = "4.21.1"
version = "4.24.0"
author = "Jonathan Bernard"
description = "Personal issue tracker."
license = "MIT"
srcDir = "src"
installExt = @["nim"]
bin = @["pit", "pit_api"]
# Dependencies
@ -13,14 +14,15 @@ requires @[
"nim >= 1.4.0",
"docopt >= 0.6.8",
"jester >= 0.5.0",
"uuids >= 0.1.10"
"uuids >= 0.1.10",
"zero_functional"
]
# Dependencies from git.jdb-software.com/nim-jdb/packages
requires @[
"cliutils >= 0.6.4",
"cliutils >= 0.8.1",
"langutils >= 0.4.0",
"timeutils >= 0.4.0",
"timeutils >= 0.5.4",
"data_uri > 1.0.0",
"https://git.jdb-software.com/jdb/update-nim-package-version >= 0.2.0"
]

View File

@ -1,8 +1,8 @@
## Personal Issue Tracker CLI interface
## ====================================
import std/algorithm, std/logging, std/options, std/os, std/sequtils,
std/wordwrap, std/tables, std/terminal, std/times, std/unicode
import std/[algorithm, logging, options, os, sequtils, wordwrap, tables,
terminal, times, unicode]
import cliutils, data_uri, docopt, json, timeutils, uuids
from nre import re
@ -16,7 +16,6 @@ type
cfg*: PitConfig
contexts*: TableRef[string, string]
defaultContext*: Option[string]
tasksDir*: string
issues*: TableRef[IssueState, seq[Issue]]
termWidth*: int
triggerPtk*, verbose*: bool
@ -43,7 +42,6 @@ proc initContext(args: Table[string, Value]): CliContext =
else: some(cliJson["defaultContext"].getStr()),
verbose: parseBool(cliCfg.getVal("verbose", "false")) and not args["--quiet"],
issues: newTable[IssueState, seq[Issue]](),
tasksDir: pitCfg.tasksDir,
termWidth: parseInt(cliCfg.getVal("termWidth", "80")),
triggerPtk: cliJson.getOrDefault("triggerPtk").getBool(false))
@ -53,7 +51,7 @@ proc getIssueContextDisplayName(ctx: CliContext, context: string): string =
else: return context.capitalize()
return ctx.contexts[context]
proc formatIssue(ctx: CliContext, issue: Issue): string =
proc formatIssue*(issue: Issue): string =
result = ($issue.id).withColor(fgBlack, true) & "\n"&
issue.summary.withColor(fgWhite) & "\n"
@ -72,10 +70,9 @@ proc formatIssue(ctx: CliContext, issue: Issue): string =
result &= termReset
proc formatSectionIssue(
ctx: CliContext,
proc formatSectionIssue*(
issue: Issue,
width: int,
width: int = 80,
indent = "",
verbose = false): string =
@ -137,12 +134,15 @@ proc formatSectionIssue(
result &= termReset
proc formatSectionIssueList(ctx: CliContext, issues: seq[Issue], width: int,
indent: string, verbose: bool): string =
proc formatSectionIssueList*(
issues: seq[Issue],
width: int = 80,
indent: string = "",
verbose: bool = false): string =
result = ""
for i in issues:
var issueText = ctx.formatSectionIssue(i, width, indent, verbose)
var issueText = formatSectionIssue(i, width, indent, verbose)
result &= issueText & "\n"
proc formatSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
@ -163,21 +163,20 @@ proc formatSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
indent & ctx.getIssueContextDisplayName(context) & ":" &
termReset & "\n\n"
result &= ctx.formatSectionIssueList(ctxIssues, innerWidth - 2, indent & " ", verbose)
result &= formatSectionIssueList(ctxIssues, innerWidth - 2, indent & " ", verbose)
result &= "\n"
else: result &= ctx.formatSectionIssueList(issues, innerWidth, indent, verbose)
else: result &= formatSectionIssueList(issues, innerWidth, indent, verbose)
proc loadIssues(ctx: CliContext, state: IssueState) =
ctx.issues[state] = loadIssues(ctx.tasksDir / $state)
ctx.issues[state] = loadIssues(ctx.cfg.tasksDir, state)
proc loadOpenIssues(ctx: CliContext) =
ctx.issues = newTable[IssueState, seq[Issue]]()
for state in [Current, TodoToday, Todo, Pending, Todo]: ctx.loadIssues(state)
proc loadAllIssues(ctx: CliContext) =
ctx.issues = newTable[IssueState, seq[Issue]]()
for state in IssueState: ctx.loadIssues(state)
ctx.issues = ctx.cfg.tasksDir.loadAllIssues()
proc filterIssues(ctx: CliContext, filter: IssueFilter) =
for state, issueList in ctx.issues:
@ -214,7 +213,7 @@ 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")
discard os.execShellCmd(EDITOR & " '" & (ctx.cfg.tasksDir / $state / "order.txt") & "' </dev/tty >/dev/tty")
proc edit(issue: Issue) =
@ -227,7 +226,7 @@ proc edit(issue: Issue) =
# Try to parse the newly-edited issue to make sure it was successful.
let editedIssue = loadIssue(issue.filepath)
editedIssue.store()
except:
except CatchableError:
fatal "updated issue is invalid (ignoring edits): \n\t" &
getCurrentExceptionMsg()
issue.store()
@ -380,9 +379,9 @@ when isMainModule:
if tagsOption.isSome: tagsOption.get
else: newSeq[string]())
ctx.tasksDir.store(issue, state)
ctx.cfg.tasksDir.store(issue, state)
stdout.writeLine ctx.formatIssue(issue)
stdout.writeLine formatIssue(issue)
elif args["reorder"]:
ctx.reorder(parseEnum[IssueState]($args["<state>"]))
@ -393,14 +392,14 @@ when isMainModule:
var stateOption = none(IssueState)
try: stateOption = some(parseEnum[IssueState](editRef))
except: discard
except CatchableError: discard
if stateOption.isSome:
let state = stateOption.get
ctx.loadIssues(state)
for issue in ctx.issues[state]: edit(issue)
else: edit(ctx.tasksDir.loadIssueById(editRef))
else: edit(ctx.cfg.tasksDir.loadIssueById(editRef))
elif args["tag"]:
if tagsOption.isNone: raise newException(Exception, "no tags given")
@ -408,7 +407,7 @@ when isMainModule:
let newTags = tagsOption.get
for id in @(args["<id>"]):
var issue = ctx.tasksDir.loadIssueById(id)
var issue = ctx.cfg.tasksDir.loadIssueById(id)
issue.tags = deduplicate(issue.tags & newTags)
issue.store()
@ -418,7 +417,7 @@ when isMainModule:
else: @[]
for id in @(args["<id>"]):
var issue = ctx.tasksDir.loadIssueById(id)
var issue = ctx.cfg.tasksDir.loadIssueById(id)
if tagsToRemove.len > 0:
issue.tags = issue.tags.filter(
proc (tag: string): bool = not tagsToRemove.anyIt(it == tag))
@ -437,24 +436,24 @@ when isMainModule:
elif args["suspend"]: targetState = Dormant
for id in @(args["<id>"]):
var issue = ctx.tasksDir.loadIssueById(id)
var issue = ctx.cfg.tasksDir.loadIssueById(id)
if propertiesOption.isSome:
for k,v in propertiesOption.get:
issue[k] = v
if targetState == Done:
issue["completed"] = getTime().local.formatIso8601
if issue.hasProp("recurrence") and issue.getRecurrence.isSome:
let nextIssue = ctx.tasksDir.nextRecurrence(issue.getRecurrence.get, issue)
ctx.tasksDir.store(nextIssue, Todo)
let nextIssue = ctx.cfg.tasksDir.nextRecurrence(issue.getRecurrence.get, issue)
ctx.cfg.tasksDir.store(nextIssue, TodoToday)
info "created the next recurrence:"
stdout.writeLine ctx.formatIssue(nextIssue)
stdout.writeLine formatIssue(nextIssue)
issue.changeState(ctx.tasksDir, targetState)
issue.changeState(ctx.cfg.tasksDir, targetState)
if ctx.triggerPtk or args["--ptk"]:
if targetState == Current:
let issue = ctx.tasksDir.loadIssueById($(args["<id>"][0]))
let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"][0]))
var cmd = "ptk start"
if issue.tags.len > 0 or issue.hasProp("context"):
let tags = concat(
@ -471,14 +470,14 @@ when isMainModule:
elif args["hide-until"]:
let issue = ctx.tasksDir.loadIssueById($(args["<id>"]))
let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"]))
issue.setDateTime("hide-until", parseDate($args["<date>"]))
issue.store()
elif args["delegate"]:
let issue = ctx.tasksDir.loadIssueById($(args["<id>"]))
let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"]))
issue["delegated-to"] = $args["<delegated-to>"]
issue.store()
@ -486,7 +485,7 @@ when isMainModule:
elif args["delete"] or args["rm"]:
for id in @(args["<id>"]):
let issue = ctx.tasksDir.loadIssueById(id)
let issue = ctx.cfg.tasksDir.loadIssueById(id)
if not args["--yes"]:
stderr.write("Delete '" & issue.summary & "' (y/n)? ")
@ -546,8 +545,12 @@ when isMainModule:
if args["contexts"]: listContexts = true
elif args["<stateOrId>"]:
try: statesOption = some(args["<stateOrId>"].mapIt(parseEnum[IssueState]($it)))
except: issueIdsOption = some(args["<stateOrId>"].mapIt($it))
try:
statesOption =
some(args["<stateOrId>"].
mapIt(parseEnum[IssueState]($it)))
except CatchableError:
issueIdsOption = some(args["<stateOrId>"].mapIt($it))
# List the known contexts
if listContexts:
@ -569,8 +572,8 @@ when isMainModule:
# List a specific issue
elif issueIdsOption.isSome:
for issueId in issueIdsOption.get:
let issue = ctx.tasksDir.loadIssueById(issueId)
stdout.writeLine ctx.formatIssue(issue)
let issue = ctx.cfg.tasksDir.loadIssueById(issueId)
stdout.writeLine formatIssue(issue)
# List all issues
else:
@ -585,7 +588,7 @@ when isMainModule:
verbose = ctx.verbose)
elif args["add-binary-property"]:
let issue = ctx.tasksDir.loadIssueById($(args["<id>"]))
let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"]))
let propIn =
if $(args["<propSource>"]) == "-": stdin
@ -597,7 +600,7 @@ when isMainModule:
issue.store()
elif args["get-binary-property"]:
let issue = ctx.tasksDir.loadIssueById($(args["<id>"]))
let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"]))
if not issue.hasProp($(args["<propName>"])):
raise newException(Exception,
@ -611,7 +614,23 @@ when isMainModule:
try: write(propOut, decodeDataUri(issue[$(args["<propName>"])]))
finally: close(propOut)
except:
elif args["show-dupes"]:
ctx.loadAllIssues()
var idsToPaths = newTable[string, var seq[string]]()
for (state, issues) in pairs(ctx.issues):
for issue in issues:
let issueId = $issue.id
if idsToPaths.hasKey(issueId): idsToPaths[issueId].add(issue.filepath)
else: idsToPaths[issueId] = @[issue.filepath]
for (issueId, issuePaths) in pairs(idsToPaths):
if issuePaths.len < 2: continue
stdout.writeLine(issueId & ":\p " & issuePaths.join("\p ") & "\p\p")
except CatchableError:
fatal getCurrentExceptionMsg()
debug getCurrentException().getStackTrace()
#raise getCurrentException()
quit(QuitFailure)

View File

@ -1,4 +1,4 @@
const PIT_VERSION* = "4.21.1"
const PIT_VERSION* = "4.24.0"
const USAGE* = """Usage:
pit ( new | add) <summary> [<state>] [options]
@ -14,6 +14,7 @@ const USAGE* = """Usage:
pit ( delete | rm ) <id>... [options]
pit add-binary-property <id> <propName> <propSource> [options]
pit get-binary-property <id> <propName> <propDest> [options]
pit show-dupes
pit help [options]
Options:

View File

@ -1,8 +1,9 @@
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 std/[json, logging, options, os, strformat, strutils, tables, times]
import cliutils, docopt, langutils, uuids, zero_functional
import nre except toSeq
import timeutils except `>`
from sequtils import deduplicate, toSeq
type
Issue* = ref object
@ -11,6 +12,7 @@ type
summary*, details*: string
properties*: TableRef[string, string]
tags*: seq[string]
state*: IssueState
IssueState* = enum
Current = "current",
@ -39,6 +41,7 @@ type
isFromCompletion*: bool
const DONE_FOLDER_FORMAT* = "yyyy-MM"
const ISO8601_MS = "yyyy-MM-dd'T'HH:mm:ss'.'fffzzz"
let ISSUE_FILE_PATTERN = re"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}\.txt"
let RECURRENCE_PATTERN = re"(every|after) ((\d+) )?((hour|day|week|month|year)s?)(, ([0-9a-fA-F]+))?"
@ -165,7 +168,7 @@ proc parseDate*(d: string): DateTime =
var errMsg = ""
for df in DATE_FORMATS:
try: return d.parse(df)
except:
except CatchableError:
errMsg &= "\n\tTried " & df & " with " & d
continue
raise newException(ValueError, "Unable to parse input as a date: " & d & errMsg)
@ -201,12 +204,13 @@ proc fromStorageFormat*(id: string, issueTxt: string): Issue =
continue
let parts = line.split({':'}, 1).mapIt(it.strip())
let parts = line.split({':'}, 1) --> map(it.strip())
if parts.len != 2:
raise newException(ValueError, "unable to parse property line: " & line)
# 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]
of ReadingDetails:
@ -216,12 +220,17 @@ proc fromStorageFormat*(id: string, issueTxt: string): Issue =
proc toStorageFormat*(issue: Issue, withComments = false): string =
var lines: seq[string] = @[]
if withComments: lines.add("# Summary (one line):")
lines.add(issue.summary)
if withComments: lines.add("# Properties (\"key:value\" per line):")
issue.properties["last-updated"] = now().format(ISO8601_MS)
for key, val in issue.properties:
if not val.isEmptyOrWhitespace: lines.add(key & ": " & val)
if issue.tags.len > 0: lines.add("tags: " & issue.tags.join(","))
if not isEmptyOrWhitespace(issue.details) or withComments:
if withComments: lines.add("# Details go below the \"--------\"")
lines.add("--------")
@ -234,6 +243,11 @@ proc loadIssue*(filePath: string): Issue =
result = fromStorageFormat(splitFile(filePath).name, readFile(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 =
for path in walkDirRec(tasksDir):
if path.splitFile.name.startsWith(id):
@ -273,10 +287,10 @@ proc loadIssues*(path: string): seq[Issue] =
let orderedIds =
if fileExists(orderFile):
toSeq(orderFile.lines)
.mapIt(it.split(' ')[0])
.deduplicate
.filterIt(not it.startsWith("> ") and not it.isEmptyOrWhitespace)
(orderFile.lines.toSeq -->
map(it.split(' ')[0]).
filter(not it.startsWith("> ") and not it.isEmptyOrWhitespace)).
deduplicate()
else: newSeq[string]()
type TaggedIssue = tuple[issue: Issue, ordered: bool]
@ -306,11 +320,19 @@ proc loadIssues*(path: string): seq[Issue] =
# Finally, save current order
result.storeOrder(path)
proc loadIssues*(tasksDir: string, state: IssueState): seq[Issue] =
loadIssues(tasksDir / $state)
proc loadAllIssues*(tasksDir: string): TableRef[IssueState, seq[Issue]] =
result = newTable[IssueState, seq[Issue]]()
for state in IssueState: result[state] = tasksDir.loadIssues(state)
proc changeState*(issue: Issue, tasksDir: string, newState: IssueState) =
let oldFilepath = issue.filepath
if newState == Done: issue.setDateTime("completed", getTime().local)
tasksDir.store(issue, newState)
if oldFilePath != issue.filepath: removeFile(oldFilepath)
issue.state = newState
proc delete*(issue: Issue) = removeFile(issue.filepath)
@ -325,6 +347,7 @@ proc nextRecurrence*(tasksDir: string, rec: Recurrence, defaultIssue: Issue): Is
result = Issue(
id: genUUID(),
state: baseIssue.state,
summary: baseIssue.summary,
properties: newProps,
tags: baseIssue.tags)
@ -347,47 +370,55 @@ proc nextRecurrence*(tasksDir: string, rec: Recurrence, defaultIssue: Issue): Is
## Utilities for working with issue collections.
proc filter*(issues: seq[Issue], filter: IssueFilter): seq[Issue] =
result = issues
var f: seq[Issue] = issues
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:
result = result.filter(proc (iss: Issue): bool =
not iss.hasProp(k) or
not v.anyIt(it == iss[k])
)
f = f --> filter(not (it.hasProp(k) and v.contains(it[k])))
if filter.completedRange.isSome:
let range = filter.completedRange.get
result = result.filterIt(
f = f --> filter(
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)
f = f --> filter(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)
f = f -->
filter(it.summary.find(p).isSome or it.details.find(p).isSome)
for tag in filter.hasTags:
result = result.filterIt(it.tags.find(tag) >= 0)
for tagLent in filter.hasTags:
let tag = tagLent
f = f --> filter(it.tags.find(tag) >= 0)
for exclTag in filter.exclTags:
result = result.filterIt(it.tags.find(exclTag) < 0)
for exclTagLent in filter.exclTags:
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*(
issues: TableRef[IssueState, seq[Issue]],
filter: IssueFilter
): seq[Issue] =
result = @[]
for stateIssues in issues.values: result &= stateIssues.filter(filter)
### Configuration utilities
proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitConfig =
let pitrcLocations = @[
if args["--config"]: $args["--config"] else: "",
".pitrc", $getEnv("PITRC"), $getEnv("HOME") & "/.pitrc"]
var pitrcFilename: string
var pitrcFilename: string =
foldl(pitrcLocations, if len(a) > 0: a elif fileExists(b): b else: "")
if not fileExists(pitrcFilename):
try:
pitrcFilename = findConfigFile(".pitrc",
if args["--config"]: @[$args["--config"]] else: @[])
except ValueError:
warn "could not find .pitrc file: " & pitrcFilename
if isEmptyOrWhitespace(pitrcFilename):
pitrcFilename = $getEnv("HOME") & "/.pitrc"
@ -395,25 +426,19 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitCo
try:
cfgFile = open(pitrcFilename, fmWrite)
cfgFile.write("{\"tasksDir\": \"/path/to/tasks\"}")
except: warn "could not write default .pitrc to " & pitrcFilename
except CatchableError: warn "could not write default .pitrc to " & pitrcFilename
finally: close(cfgFile)
var cfgJson: JsonNode
try: cfgJson = parseFile(pitrcFilename)
except: raise newException(IOError,
"unable to read config file: " & pitrcFilename &
"\x0D\x0A" & getCurrentExceptionMsg())
let cfg = CombinedConfig(docopt: args, json: cfgJson)
debug "loading config from '$#'" % [pitrcFilename]
let cfg = initCombinedConfig(pitrcFilename, args)
result = PitConfig(
cfg: cfg,
contexts: newTable[string,string](),
tasksDir: cfg.getVal("tasks-dir", ""))
if cfgJson.hasKey("contexts"):
for k, v in cfgJson["contexts"]:
result.contexts[k] = v.getStr()
for k, v in cfg.getJson("contexts", newJObject()):
result.contexts[k] = v.getStr()
if isEmptyOrWhitespace(result.tasksDir):
raise newException(Exception, "no tasks directory configured")