Compare commits

...

18 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
58a5321d95 Rework dependencies using JDB Softwar package repo instead of URLs. 2023-02-13 08:48:40 -06:00
7215b4969b 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.
2022-07-31 20:01:39 -05:00
c7891de310 Show Pending in either Today's list of the Future list, but not both at the same time. 2022-07-28 10:48:51 -05:00
a373af0658 Add support for filtering based on property exclusion.
For example, allow commands like:

  # exclude issues from context "abc"
  pit list -C abc

  # exclude issues delegated to John Doe
  pit list -P "delgated-to:John Doe"
2022-07-22 10:39:13 -05:00
de3ee05680 Change logging format. 2022-01-22 10:31:58 -06:00
59440d2c9d Remove unused, old copies of usage and outline text. 2022-01-21 15:01:07 -06:00
6226ff21c5 Fix compile-time message includes when installing via Nimble. 2022-01-21 14:59:22 -06:00
9 changed files with 603 additions and 282 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,26 +1,31 @@
# Package # Package
version = "4.18.0" version = "4.24.0"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Personal issue tracker." description = "Personal issue tracker."
license = "MIT" license = "MIT"
srcDir = "src" srcDir = "src"
installExt = @["nim"]
bin = @["pit", "pit_api"] bin = @["pit", "pit_api"]
# Dependencies # Dependencies
requires @[ requires @[
"nim >= 1.4.0", "nim >= 1.4.0",
"docopt 0.6.8", "docopt >= 0.6.8",
"jester 0.5.0", "jester >= 0.5.0",
"uuids 0.1.10", "uuids >= 0.1.10",
"zero_functional"
]
"https://git.jdb-software.com/jdb/nim-cli-utils.git >= 0.6.4", # Dependencies from git.jdb-software.com/nim-jdb/packages
"https://git.jdb-software.com/jdb/nim-lang-utils.git >= 0.4.0", requires @[
"https://git.jdb-software.com/jdb/nim-time-utils.git >= 0.4.0", "cliutils >= 0.8.1",
"https://git.jdb-software.com/jdb/nim-data-uri.git >= 1.0.0", "langutils >= 0.4.0",
"https://git.jdb-software.com/jdb/update-nim-package-version" "timeutils >= 0.5.4",
"data_uri > 1.0.0",
"https://git.jdb-software.com/jdb/update-nim-package-version >= 0.2.0"
] ]
task updateVersion, "Update the version of this package.": task updateVersion, "Update the version of this package.":
exec "update_nim_package_version pit 'src/pitpkg/version.nim'" exec "update_nim_package_version pit 'src/pitpkg/cliconstants.nim'"

View File

@ -1,93 +0,0 @@
Issue States:
PIT organizes issues around their state, which is one of:
current - issues actively being worked
todo-today - issues planned for today
pending - issues that are blocked by some third-party
done - issues that have been completely resolved
todo - issues that need to be done in the future
dormant - issues that are low-priority, to be tracked, but hidden
by default
Issue Properties:
PIT supports adding arbitrary properties to any issue to track any metadata
about the issue the user may wish. There are several properties that have
special behavior attached to them. They are:
created
If present, expected to be an ISO 8601-formatted date that represents the
time when the issue was created.
completed
If present, expected to be an ISO 8601-formatted date that represents the
time when the issue moved to the "done" state. PIT will add this
property automatically when you use the "done" command, and can filter on
this value.
context
Allows issues to be organized into contexts. The -c option is short-hand
for '-p context:<context-name>' and the 'list contexts' command will show
all values of 'context' set in existing issues.
delegated-to
When an issue now belongs to someone else, but needs to be monitored for
completion, this allows you to keep the issue in its current state but
note how it has been delegated. When present PIT will prepend this value
to the issue summary with an accent color.
hide-until
When present, expected to be an ISO 8601-formatted date and used to
supress the display of the issue until on or after the given date.
pending
When an issue is blocked by a third party, this property can be used to
capture details about the dependency When present PIT will display this
value after the issue summary.
recurrence
When an issue is moved to the "done" state, if the issue has a valid
"recurrence" property, PIT will create a new issue and set the
"hide-until" property for that new issue depending on the recurrence
definition.
A valid recurrence value has a time value and optionally has an source
issue ID. For example:
every 5 days, 10a544
The first word, "every", is expected to be either "every" or "after".
The second portion is expected to be a time period. Supported time units
are "hour", "day", "week", "month", an "year", along with the plural
forms (e.g. "5 days", "8 hours", etc.).
The final portion is the source issue ID. This is optional. When a source
issue ID is given, the new issue is created as a clone of the given
issue. When not given, the issue being closed is used for cloning.
The "every" and "after" keywords allow the user to choose whether the new
issue is created based on the creation time ("every") or the completion
time ("after") of the issue being closed based.
Examples:
every day
every 2 days
after 2 days
every week
after 12 hours
every 2 weeks, 10a544
tags
If present, expected to be a comma-delimited list of text tags. The -g
option is a short-hand for '-p tags:<tags-value>'.

View File

@ -1,22 +1,21 @@
## Personal Issue Tracker CLI interface ## Personal Issue Tracker CLI interface
## ==================================== ## ====================================
import algorithm, cliutils, data_uri, docopt, json, logging, options, os, import std/[algorithm, logging, options, os, sequtils, wordwrap, tables,
sequtils, std/wordwrap, tables, terminal, times, timeutils, unicode, uuids terminal, times, unicode]
import cliutils, data_uri, docopt, json, timeutils, uuids
from nre import re from nre import re
import strutils except alignLeft, capitalize, strip, toUpper, toLower import strutils except alignLeft, capitalize, strip, toUpper, toLower
import pitpkg/private/libpit import pitpkg/private/libpit
import pitpkg/cliconstants
export libpit export libpit
include "pitpkg/version.nim"
type type
CliContext = ref object CliContext = ref object
cfg*: PitConfig cfg*: PitConfig
contexts*: TableRef[string, string] contexts*: TableRef[string, string]
defaultContext*: Option[string] defaultContext*: Option[string]
tasksDir*: string
issues*: TableRef[IssueState, seq[Issue]] issues*: TableRef[IssueState, seq[Issue]]
termWidth*: int termWidth*: int
triggerPtk*, verbose*: bool triggerPtk*, verbose*: bool
@ -43,7 +42,6 @@ proc initContext(args: Table[string, Value]): CliContext =
else: some(cliJson["defaultContext"].getStr()), else: some(cliJson["defaultContext"].getStr()),
verbose: parseBool(cliCfg.getVal("verbose", "false")) and not args["--quiet"], verbose: parseBool(cliCfg.getVal("verbose", "false")) and not args["--quiet"],
issues: newTable[IssueState, seq[Issue]](), issues: newTable[IssueState, seq[Issue]](),
tasksDir: pitCfg.tasksDir,
termWidth: parseInt(cliCfg.getVal("termWidth", "80")), termWidth: parseInt(cliCfg.getVal("termWidth", "80")),
triggerPtk: cliJson.getOrDefault("triggerPtk").getBool(false)) triggerPtk: cliJson.getOrDefault("triggerPtk").getBool(false))
@ -53,7 +51,7 @@ proc getIssueContextDisplayName(ctx: CliContext, context: string): string =
else: return context.capitalize() else: return context.capitalize()
return ctx.contexts[context] return ctx.contexts[context]
proc formatIssue(ctx: CliContext, issue: Issue): string = proc formatIssue*(issue: Issue): string =
result = ($issue.id).withColor(fgBlack, true) & "\n"& result = ($issue.id).withColor(fgBlack, true) & "\n"&
issue.summary.withColor(fgWhite) & "\n" issue.summary.withColor(fgWhite) & "\n"
@ -72,55 +70,79 @@ proc formatIssue(ctx: CliContext, issue: Issue): string =
result &= termReset result &= termReset
proc formatSectionIssue(ctx: CliContext, issue: Issue, width: int, indent = "", proc formatSectionIssue*(
verbose = false): string = issue: Issue,
width: int = 80,
result = "" indent = "",
verbose = false): string =
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]
result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true) & " " result = (indent & ($issue.id)[0..<6]).withColor(fgBlack, true) & " "
if issue.hasProp("delegated-to"): let showDetails = not issue.details.isEmptyOrWhitespace and verbose
result &= (issue["delegated-to"] & ": ").withColor(fgGreen)
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: if issue.tags.len > 0:
let tagsStr = "(" & issue.tags.join(", ") & ")" let tagsStrLines = ("(" & issue.tags.join(", ") & ")")
if (result.splitLines[^1].len + tagsStr.len + 1) > (width - 2): .wrapWords(summaryWidth)
result &= "\n" & indent .splitLines
result &= " " & tagsStr.withColor(fgGreen)
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"): if issue.hasProp("pending"):
let startIdx = "Pending: ".len result &= "\p" & ("Pending: " & issue["pending"])
var pendingText = issue["pending"].wrapWords(width - startIdx - summaryIndentLen) .wrapwords(summaryWidth)
.indent(startIdx) .withColor(fgCyan)
pendingText = ("Pending: " & pendingText[startIdx..^1]).indent(summaryIndentLen) .indent(summaryIndentLen)
result &= "\n" & pendingText.withColor(fgCyan)
if showDetails: 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 result &= termReset
proc formatSectionIssueList(ctx: CliContext, issues: seq[Issue], width: int, proc formatSectionIssueList*(
indent: string, verbose: bool): string = issues: seq[Issue],
width: int = 80,
indent: string = "",
verbose: bool = false): string =
result = "" result = ""
for i in issues: for i in issues:
var issueText = ctx.formatSectionIssue(i, width, indent, verbose) var issueText = formatSectionIssue(i, width, indent, verbose)
result &= issueText & "\n" result &= issueText & "\n"
proc formatSection(ctx: CliContext, issues: seq[Issue], state: IssueState, proc formatSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
@ -141,21 +163,20 @@ proc formatSection(ctx: CliContext, issues: seq[Issue], state: IssueState,
indent & ctx.getIssueContextDisplayName(context) & ":" & indent & ctx.getIssueContextDisplayName(context) & ":" &
termReset & "\n\n" termReset & "\n\n"
result &= ctx.formatSectionIssueList(ctxIssues, innerWidth - 2, indent & " ", verbose) result &= formatSectionIssueList(ctxIssues, innerWidth - 2, indent & " ", verbose)
result &= "\n" result &= "\n"
else: result &= ctx.formatSectionIssueList(issues, innerWidth, indent, verbose) else: result &= formatSectionIssueList(issues, innerWidth, indent, verbose)
proc loadIssues(ctx: CliContext, state: IssueState) = 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) = proc loadOpenIssues(ctx: CliContext) =
ctx.issues = newTable[IssueState, seq[Issue]]() ctx.issues = newTable[IssueState, seq[Issue]]()
for state in [Current, TodoToday, Todo, Pending, Todo]: ctx.loadIssues(state) for state in [Current, TodoToday, Todo, Pending, Todo]: ctx.loadIssues(state)
proc loadAllIssues(ctx: CliContext) = proc loadAllIssues(ctx: CliContext) =
ctx.issues = newTable[IssueState, seq[Issue]]() ctx.issues = ctx.cfg.tasksDir.loadAllIssues()
for state in IssueState: ctx.loadIssues(state)
proc filterIssues(ctx: CliContext, filter: IssueFilter) = proc filterIssues(ctx: CliContext, filter: IssueFilter) =
for state, issueList in ctx.issues: for state, issueList in ctx.issues:
@ -168,6 +189,16 @@ proc parsePropertiesOption(propsOpt: string): TableRef[string, string] =
if pair.len == 1: result[pair[0]] = "true" if pair.len == 1: result[pair[0]] = "true"
else: result[pair[0]] = pair[1] else: result[pair[0]] = pair[1]
proc parseExclPropertiesOption(propsOpt: string): TableRef[string, seq[string]] =
result = newTable[string, seq[string]]()
for propText in propsOpt.split(";"):
let pair = propText.split(":", 1)
let val =
if pair.len == 1: "true"
else: pair[1]
if result.hasKey(pair[0]): result[pair[0]].add(val)
else: result[pair[0]] = @[val]
proc sameDay(a, b: DateTime): bool = proc sameDay(a, b: DateTime): bool =
result = a.year == b.year and a.yearday == b.yearday result = a.year == b.year and a.yearday == b.yearday
@ -182,7 +213,7 @@ proc reorder(ctx: CliContext, state: IssueState) =
# load the issues to make sure the order file contains all issues in the state. # load the issues to make sure the order file contains all issues in the state.
ctx.loadIssues(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) = proc edit(issue: Issue) =
@ -195,7 +226,7 @@ proc edit(issue: Issue) =
# Try to parse the newly-edited issue to make sure it was successful. # Try to parse the newly-edited issue to make sure it was successful.
let editedIssue = loadIssue(issue.filepath) let editedIssue = loadIssue(issue.filepath)
editedIssue.store() editedIssue.store()
except: except CatchableError:
fatal "updated issue is invalid (ignoring edits): \n\t" & fatal "updated issue is invalid (ignoring edits): \n\t" &
getCurrentExceptionMsg() getCurrentExceptionMsg()
issue.store() issue.store()
@ -254,7 +285,11 @@ proc list(
if future: if future:
if today: ctx.writeHeader("Future") if today: ctx.writeHeader("Future")
for s in [Pending, Todo]: let futureCategories =
if showToday: @[Todo]
else: @[Pending, Todo]
for s in futureCategories:
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0: if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
let visibleIssues = ctx.issues[s].filterIt( let visibleIssues = ctx.issues[s].filterIt(
showHidden or showHidden or
@ -267,16 +302,14 @@ proc list(
when isMainModule: when isMainModule:
try: try:
const usage = readFile("src/usage.txt")
const onlineHelp = readFile("src/online-help.txt")
let consoleLogger = newConsoleLogger( let consoleLogger = newConsoleLogger(
levelThreshold=lvlInfo, levelThreshold=lvlInfo,
fmtStr="$app - $levelname: ") fmtStr="pit - $levelname: ")
logging.addHandler(consoleLogger) logging.addHandler(consoleLogger)
# Parse arguments # Parse arguments
let args = docopt(usage, version = PIT_VERSION) let args = docopt(USAGE, version = PIT_VERSION)
if args["--debug"]: if args["--debug"]:
consoleLogger.levelThreshold = lvlDebug consoleLogger.levelThreshold = lvlDebug
@ -284,8 +317,8 @@ when isMainModule:
if args["--echo-args"]: stderr.writeLine($args) if args["--echo-args"]: stderr.writeLine($args)
if args["help"]: if args["help"]:
stderr.writeLine(usage & "\n") stderr.writeLine(USAGE & "\p")
stderr.writeLine(onlineHelp) stderr.writeLine(ONLINE_HELP)
quit() quit()
let ctx = initContext(args) let ctx = initContext(args)
@ -293,7 +326,9 @@ when isMainModule:
trace "context initiated" trace "context initiated"
var propertiesOption = none(TableRef[string,string]) var propertiesOption = none(TableRef[string,string])
var exclPropsOption = none(TableRef[string,seq[string]])
var tagsOption = none(seq[string]) var tagsOption = none(seq[string])
var exclTagsOption = none(seq[string])
if args["--properties"] or args["--context"]: if args["--properties"] or args["--context"]:
@ -305,8 +340,25 @@ when isMainModule:
propertiesOption = some(props) propertiesOption = some(props)
if args["--excl-properties"] or args["--excl-context"]:
var exclProps =
if args["--excl-properties"]:
parseExclPropertiesOption($args["--excl-properties"])
else: newTable[string,seq[string]]()
if args["--excl-context"]:
if not exclProps.hasKey("context"): exclProps["context"] = @[]
let exclContexts = split($args["--excl-context"], ",")
exclProps["context"] = exclProps["context"].concat(exclContexts)
exclPropsOption = some(exclProps)
if args["--tags"]: tagsOption = some(($args["--tags"]).split(",").mapIt(it.strip)) 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 ## Actual command runners
if args["new"] or args["add"]: if args["new"] or args["add"]:
let state = let state =
@ -324,12 +376,12 @@ when isMainModule:
summary: $args["<summary>"], summary: $args["<summary>"],
properties: issueProps, properties: issueProps,
tags: tags:
if args["--tags"]: ($args["--tags"]).split(",").mapIt(it.strip) if tagsOption.isSome: tagsOption.get
else: newSeq[string]()) 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"]: elif args["reorder"]:
ctx.reorder(parseEnum[IssueState]($args["<state>"])) ctx.reorder(parseEnum[IssueState]($args["<state>"]))
@ -340,32 +392,32 @@ when isMainModule:
var stateOption = none(IssueState) var stateOption = none(IssueState)
try: stateOption = some(parseEnum[IssueState](editRef)) try: stateOption = some(parseEnum[IssueState](editRef))
except: discard except CatchableError: discard
if stateOption.isSome: if stateOption.isSome:
let state = stateOption.get let state = stateOption.get
ctx.loadIssues(state) ctx.loadIssues(state)
for issue in ctx.issues[state]: edit(issue) for issue in ctx.issues[state]: edit(issue)
else: edit(ctx.tasksDir.loadIssueById(editRef)) else: edit(ctx.cfg.tasksDir.loadIssueById(editRef))
elif args["tag"]: 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>"]): 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.tags = deduplicate(issue.tags & newTags)
issue.store() issue.store()
elif args["untag"]: elif args["untag"]:
let tagsToRemove: seq[string] = let tagsToRemove: seq[string] =
if args["--tags"]: ($args["--tags"]).split(",").mapIt(it.strip) if tagsOption.isSome: tagsOption.get
else: @[] else: @[]
for id in @(args["<id>"]): for id in @(args["<id>"]):
var issue = ctx.tasksDir.loadIssueById(id) var issue = ctx.cfg.tasksDir.loadIssueById(id)
if tagsToRemove.len > 0: if tagsToRemove.len > 0:
issue.tags = issue.tags.filter( issue.tags = issue.tags.filter(
proc (tag: string): bool = not tagsToRemove.anyIt(it == tag)) proc (tag: string): bool = not tagsToRemove.anyIt(it == tag))
@ -384,24 +436,24 @@ when isMainModule:
elif args["suspend"]: targetState = Dormant elif args["suspend"]: targetState = Dormant
for id in @(args["<id>"]): for id in @(args["<id>"]):
var issue = ctx.tasksDir.loadIssueById(id) var issue = ctx.cfg.tasksDir.loadIssueById(id)
if propertiesOption.isSome: if propertiesOption.isSome:
for k,v in propertiesOption.get: for k,v in propertiesOption.get:
issue[k] = v issue[k] = v
if targetState == Done: if targetState == Done:
issue["completed"] = getTime().local.formatIso8601 issue["completed"] = getTime().local.formatIso8601
if issue.hasProp("recurrence") and issue.getRecurrence.isSome: if issue.hasProp("recurrence") and issue.getRecurrence.isSome:
let nextIssue = ctx.tasksDir.nextRecurrence(issue.getRecurrence.get, issue) let nextIssue = ctx.cfg.tasksDir.nextRecurrence(issue.getRecurrence.get, issue)
ctx.tasksDir.store(nextIssue, Todo) ctx.cfg.tasksDir.store(nextIssue, TodoToday)
info "created the next recurrence:" 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 ctx.triggerPtk or args["--ptk"]:
if targetState == Current: if targetState == Current:
let issue = ctx.tasksDir.loadIssueById($(args["<id>"][0])) let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"][0]))
var cmd = "ptk start" var cmd = "ptk start"
if issue.tags.len > 0 or issue.hasProp("context"): if issue.tags.len > 0 or issue.hasProp("context"):
let tags = concat( let tags = concat(
@ -418,14 +470,14 @@ when isMainModule:
elif args["hide-until"]: 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.setDateTime("hide-until", parseDate($args["<date>"]))
issue.store() issue.store()
elif args["delegate"]: 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["delegated-to"] = $args["<delegated-to>"]
issue.store() issue.store()
@ -433,7 +485,7 @@ when isMainModule:
elif args["delete"] or args["rm"]: elif args["delete"] or args["rm"]:
for id in @(args["<id>"]): for id in @(args["<id>"]):
let issue = ctx.tasksDir.loadIssueById(id) let issue = ctx.cfg.tasksDir.loadIssueById(id)
if not args["--yes"]: if not args["--yes"]:
stderr.write("Delete '" & issue.summary & "' (y/n)? ") stderr.write("Delete '" & issue.summary & "' (y/n)? ")
@ -452,6 +504,11 @@ when isMainModule:
filter.properties = propertiesOption.get filter.properties = propertiesOption.get
filterOption = some(filter) filterOption = some(filter)
# Add property exclusions (if given)
if exclPropsOption.isSome:
filter.exclProperties = exclPropsOption.get
filterOption = some(filter)
# If they supplied text matches, add that to the filter. # If they supplied text matches, add that to the filter.
if args["--match"]: if args["--match"]:
filter.summaryMatch = some(re("(?i)" & $args["--match"])) filter.summaryMatch = some(re("(?i)" & $args["--match"]))
@ -467,8 +524,12 @@ when isMainModule:
filter.properties["context"] = ctx.defaultContext.get filter.properties["context"] = ctx.defaultContext.get
filterOption = some(filter) filterOption = some(filter)
if args["--tags"]: if tagsOption.isSome:
filter.hasTags = ($args["--tags"]).split(',') filter.hasTags = tagsOption.get
filterOption = some(filter)
if exclTagsOption.isSome:
filter.exclTags = exclTagsOption.get
filterOption = some(filter) filterOption = some(filter)
# Finally, if the "context" is "all", don't filter on context # Finally, if the "context" is "all", don't filter on context
@ -476,6 +537,7 @@ when isMainModule:
filter.properties["context"] == "all": filter.properties["context"] == "all":
filter.properties.del("context") filter.properties.del("context")
filter.exclProperties.del("context")
var listContexts = false var listContexts = false
var statesOption = none(seq[IssueState]) var statesOption = none(seq[IssueState])
@ -483,8 +545,12 @@ when isMainModule:
if args["contexts"]: listContexts = true if args["contexts"]: listContexts = true
elif args["<stateOrId>"]: elif args["<stateOrId>"]:
try: statesOption = some(args["<stateOrId>"].mapIt(parseEnum[IssueState]($it))) try:
except: issueIdsOption = some(args["<stateOrId>"].mapIt($it)) statesOption =
some(args["<stateOrId>"].
mapIt(parseEnum[IssueState]($it)))
except CatchableError:
issueIdsOption = some(args["<stateOrId>"].mapIt($it))
# List the known contexts # List the known contexts
if listContexts: if listContexts:
@ -506,8 +572,8 @@ when isMainModule:
# List a specific issue # List a specific issue
elif issueIdsOption.isSome: elif issueIdsOption.isSome:
for issueId in issueIdsOption.get: for issueId in issueIdsOption.get:
let issue = ctx.tasksDir.loadIssueById(issueId) let issue = ctx.cfg.tasksDir.loadIssueById(issueId)
stdout.writeLine ctx.formatIssue(issue) stdout.writeLine formatIssue(issue)
# List all issues # List all issues
else: else:
@ -522,7 +588,7 @@ when isMainModule:
verbose = ctx.verbose) verbose = ctx.verbose)
elif args["add-binary-property"]: elif args["add-binary-property"]:
let issue = ctx.tasksDir.loadIssueById($(args["<id>"])) let issue = ctx.cfg.tasksDir.loadIssueById($(args["<id>"]))
let propIn = let propIn =
if $(args["<propSource>"]) == "-": stdin if $(args["<propSource>"]) == "-": stdin
@ -534,7 +600,7 @@ when isMainModule:
issue.store() issue.store()
elif args["get-binary-property"]: 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>"])): if not issue.hasProp($(args["<propName>"])):
raise newException(Exception, raise newException(Exception,
@ -548,7 +614,23 @@ when isMainModule:
try: write(propOut, decodeDataUri(issue[$(args["<propName>"])])) try: write(propOut, decodeDataUri(issue[$(args["<propName>"])]))
finally: close(propOut) 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() fatal getCurrentExceptionMsg()
debug getCurrentException().getStackTrace()
#raise getCurrentException() #raise getCurrentException()
quit(QuitFailure) quit(QuitFailure)

View File

@ -5,8 +5,7 @@ import asyncdispatch, cliutils, docopt, jester, json, logging, options, sequtils
import nre except toSeq import nre except toSeq
import pitpkg/private/libpit import pitpkg/private/libpit
import pitpkg/cliconstants
include "pitpkg/version.nim"
type type
PitApiCfg* = object PitApiCfg* = object

176
src/pitpkg/cliconstants.nim Normal file
View File

@ -0,0 +1,176 @@
const PIT_VERSION* = "4.24.0"
const USAGE* = """Usage:
pit ( new | add) <summary> [<state>] [options]
pit list contexts [options]
pit list [<stateOrId>...] [options]
pit ( start | done | pending | todo-today | todo | suspend ) <id>... [options]
pit edit <ref>... [options]
pit tag <id>... [options]
pit untag <id>... [options]
pit reorder <state> [options]
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 show-dupes
pit help [options]
Options:
-h, --help Print this usage and help information.
-p, --properties <props> Specify properties. Formatted as "key:val;key:val"
When used with the list command this option applies
a filter to the issues listed, only allowing those
which have all of the given properties.
-P, --excl-properties <props>
When used with the list command, exclude issues
that contain properties with the given value. This
parameter is formatted the same as the --properties
parameter: "key:val;key:val"
-c, --context <ctx> Shorthand for '-p context:<ctx>'
-C, --excl-context <ctx> Don't show issues from the given context(s).
Multiple contexts can be excluded using a ',' to
separate names. For example: -C ctx1,ctx2
Shorthand for '-P context:<ctx>'
-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.
-F, --future Limit to future issues.
-H, --show-hidden Show all matching issues, ignoring any 'hide-until'
properties set.
-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.
-y, --yes Automatically answer "yes" to any prompts.
--config <cfgFile> Location of the config file (defaults to $HOME/.pitrc)
-E, --echo-args Echo arguments (for debug purposes).
-d, --tasks-dir Path to the tasks directory (defaults to the value
configured in the .pitrc file)
--term-width <width> Manually set the terminal width to use.
--ptk Enable PTK integration for this command.
--debug Enable debug-level log output.
"""
const ONLINE_HELP* = """Issue States:
PIT organizes issues around their state, which is one of:
current - issues actively being worked
todo-today - issues planned for today
pending - issues that are blocked by some third-party
done - issues that have been completely resolved
todo - issues that need to be done in the future
dormant - issues that are low-priority, to be tracked, but hidden
by default
Issue Properties:
PIT supports adding arbitrary properties to any issue to track any metadata
about the issue the user may wish. There are several properties that have
special behavior attached to them. They are:
created
If present, expected to be an ISO 8601-formatted date that represents the
time when the issue was created.
completed
If present, expected to be an ISO 8601-formatted date that represents the
time when the issue moved to the "done" state. PIT will add this
property automatically when you use the "done" command, and can filter on
this value.
context
Allows issues to be organized into contexts. The -c option is short-hand
for '-p context:<context-name>' and the 'list contexts' command will show
all values of 'context' set in existing issues.
delegated-to
When an issue now belongs to someone else, but needs to be monitored for
completion, this allows you to keep the issue in its current state but
note how it has been delegated. When present PIT will prepend this value
to the issue summary with an accent color.
hide-until
When present, expected to be an ISO 8601-formatted date and used to
supress the display of the issue until on or after the given date.
pending
When an issue is blocked by a third party, this property can be used to
capture details about the dependency When present PIT will display this
value after the issue summary.
recurrence
When an issue is moved to the "done" state, if the issue has a valid
"recurrence" property, PIT will create a new issue and set the
"hide-until" property for that new issue depending on the recurrence
definition.
A valid recurrence value has a time value and optionally has an source
issue ID. For example:
every 5 days, 10a544
The first word, "every", is expected to be either "every" or "after".
The second portion is expected to be a time period. Supported time units
are "hour", "day", "week", "month", an "year", along with the plural
forms (e.g. "5 days", "8 hours", etc.).
The final portion is the source issue ID. This is optional. When a source
issue ID is given, the new issue is created as a clone of the given
issue. When not given, the issue being closed is used for cloning.
The "every" and "after" keywords allow the user to choose whether the new
issue is created based on the creation time ("every") or the completion
time ("after") of the issue being closed based.
Examples:
every day
every 2 days
after 2 days
every week
after 12 hours
every 2 weeks, 10a544
tags
If present, expected to be a comma-delimited list of text tags. The -g
option is a short-hand for '-p tags:<tags-value>'.
"""

View File

@ -1,7 +1,9 @@
import cliutils, docopt, json, logging, langutils, options, os, import std/[json, logging, options, os, strformat, strutils, tables, times]
sequtils, strformat, strutils, tables, times, timeutils, uuids import cliutils, docopt, langutils, uuids, zero_functional
import nre except toSeq import nre except toSeq
import timeutils except `>`
from sequtils import deduplicate, toSeq
type type
Issue* = ref object Issue* = ref object
@ -10,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",
@ -23,7 +26,9 @@ type
completedRange*: Option[tuple[b, e: DateTime]] completedRange*: Option[tuple[b, e: DateTime]]
fullMatch*, summaryMatch*: Option[Regex] fullMatch*, summaryMatch*: Option[Regex]
hasTags*: seq[string] hasTags*: seq[string]
exclTags*: seq[string]
properties*: TableRef[string, string] properties*: TableRef[string, string]
exclProperties*: TableRef[string, seq[string]]
PitConfig* = ref object PitConfig* = ref object
tasksDir*: string tasksDir*: string
@ -36,6 +41,7 @@ type
isFromCompletion*: bool isFromCompletion*: bool
const DONE_FOLDER_FORMAT* = "yyyy-MM" 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 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]+))?" let RECURRENCE_PATTERN = re"(every|after) ((\d+) )?((hour|day|week|month|year)s?)(, ([0-9a-fA-F]+))?"
@ -114,7 +120,9 @@ proc initFilter*(): IssueFilter =
fullMatch: none(Regex), fullMatch: none(Regex),
summaryMatch: none(Regex), summaryMatch: none(Regex),
hasTags: @[], hasTags: @[],
properties: newTable[string, string]()) exclTags: @[],
properties: newTable[string, string](),
exclProperties: newTable[string,seq[string]]())
proc propsFilter*(props: TableRef[string, string]): IssueFilter = proc propsFilter*(props: TableRef[string, string]): IssueFilter =
if isNil(props): if isNil(props):
@ -160,7 +168,7 @@ proc parseDate*(d: string): DateTime =
var errMsg = "" var errMsg = ""
for df in DATE_FORMATS: for df in DATE_FORMATS:
try: return d.parse(df) try: return d.parse(df)
except: except CatchableError:
errMsg &= "\n\tTried " & df & " with " & d errMsg &= "\n\tTried " & df & " with " & d
continue continue
raise newException(ValueError, "Unable to parse input as a date: " & d & errMsg) raise newException(ValueError, "Unable to parse input as a date: " & d & errMsg)
@ -196,12 +204,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:
@ -211,12 +220,17 @@ proc fromStorageFormat*(id: string, issueTxt: string): Issue =
proc toStorageFormat*(issue: Issue, withComments = false): string = proc toStorageFormat*(issue: Issue, withComments = false): string =
var lines: seq[string] = @[] var lines: seq[string] = @[]
if withComments: lines.add("# Summary (one line):") if withComments: lines.add("# Summary (one line):")
lines.add(issue.summary) lines.add(issue.summary)
if withComments: lines.add("# Properties (\"key:value\" per line):") if withComments: lines.add("# Properties (\"key:value\" per line):")
issue.properties["last-updated"] = now().format(ISO8601_MS)
for key, val in issue.properties: for key, val in issue.properties:
if not val.isEmptyOrWhitespace: lines.add(key & ": " & val) if not val.isEmptyOrWhitespace: lines.add(key & ": " & val)
if issue.tags.len > 0: lines.add("tags: " & issue.tags.join(",")) if issue.tags.len > 0: lines.add("tags: " & issue.tags.join(","))
if not isEmptyOrWhitespace(issue.details) or withComments: if not isEmptyOrWhitespace(issue.details) or withComments:
if withComments: lines.add("# Details go below the \"--------\"") if withComments: lines.add("# Details go below the \"--------\"")
lines.add("--------") lines.add("--------")
@ -229,6 +243,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):
@ -268,10 +287,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]
@ -301,11 +320,19 @@ proc loadIssues*(path: string): seq[Issue] =
# Finally, save current order # Finally, save current order
result.storeOrder(path) 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) = proc changeState*(issue: Issue, tasksDir: string, newState: IssueState) =
let oldFilepath = issue.filepath let oldFilepath = issue.filepath
if newState == Done: issue.setDateTime("completed", getTime().local) if newState == Done: issue.setDateTime("completed", getTime().local)
tasksDir.store(issue, newState) tasksDir.store(issue, newState)
if oldFilePath != issue.filepath: removeFile(oldFilepath) if oldFilePath != issue.filepath: removeFile(oldFilepath)
issue.state = newState
proc delete*(issue: Issue) = removeFile(issue.filepath) proc delete*(issue: Issue) = removeFile(issue.filepath)
@ -320,6 +347,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)
@ -342,38 +370,55 @@ 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:
f = f --> filter(not (it.hasProp(k) and v.contains(it[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 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 ### Configuration utilities
proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitConfig = proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitConfig =
let pitrcLocations = @[ var pitrcFilename: string
if args["--config"]: $args["--config"] else: "",
".pitrc", $getEnv("PITRC"), $getEnv("HOME") & "/.pitrc"]
var pitrcFilename: string = try:
foldl(pitrcLocations, if len(a) > 0: a elif fileExists(b): b else: "") pitrcFilename = findConfigFile(".pitrc",
if args["--config"]: @[$args["--config"]] else: @[])
if not fileExists(pitrcFilename): except ValueError:
warn "could not find .pitrc file: " & pitrcFilename warn "could not find .pitrc file: " & pitrcFilename
if isEmptyOrWhitespace(pitrcFilename): if isEmptyOrWhitespace(pitrcFilename):
pitrcFilename = $getEnv("HOME") & "/.pitrc" pitrcFilename = $getEnv("HOME") & "/.pitrc"
@ -381,25 +426,19 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitCo
try: try:
cfgFile = open(pitrcFilename, fmWrite) cfgFile = open(pitrcFilename, fmWrite)
cfgFile.write("{\"tasksDir\": \"/path/to/tasks\"}") 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) finally: close(cfgFile)
var cfgJson: JsonNode debug "loading config from '$#'" % [pitrcFilename]
try: cfgJson = parseFile(pitrcFilename) let cfg = initCombinedConfig(pitrcFilename, args)
except: raise newException(IOError,
"unable to read config file: " & pitrcFilename &
"\x0D\x0A" & getCurrentExceptionMsg())
let cfg = CombinedConfig(docopt: args, json: cfgJson)
result = PitConfig( result = PitConfig(
cfg: cfg, cfg: cfg,
contexts: newTable[string,string](), contexts: newTable[string,string](),
tasksDir: cfg.getVal("tasks-dir", "")) tasksDir: cfg.getVal("tasks-dir", ""))
if cfgJson.hasKey("contexts"): for k, v in cfg.getJson("contexts", newJObject()):
for k, v in cfgJson["contexts"]: result.contexts[k] = v.getStr()
result.contexts[k] = v.getStr()
if isEmptyOrWhitespace(result.tasksDir): if isEmptyOrWhitespace(result.tasksDir):
raise newException(Exception, "no tasks directory configured") raise newException(Exception, "no tasks directory configured")

View File

@ -1 +0,0 @@
const PIT_VERSION* = "4.18.0"

View File

@ -1,60 +0,0 @@
Usage:
pit ( new | add) <summary> [<state>] [options]
pit list contexts [options]
pit list [<stateOrId>...] [options]
pit ( start | done | pending | todo-today | todo | suspend ) <id>... [options]
pit edit <ref>... [options]
pit tag <id>... [options]
pit untag <id>... [options]
pit reorder <state> [options]
pit delegate <id> <delegated-to>
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
Options:
-h, --help Print this usage and help information.
-p, --properties <props> Specify properties. Formatted as "key:val;key:val"
When used with the list command this option applies
a filter to the issues listed, only allowing those
which have all of the given properties.
-c, --context <ctxName> Shorthand for '-p context:<ctxName>'
-g, --tags <tags> Specify tags for an issue.
-T, --today Limit to today's issues.
-F, --future Limit to future issues.
-H, --show-hidden Show all matching issues, ignoring any 'hide-until'
properties set.
-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.
-y, --yes Automatically answer "yes" to any prompts.
-C, --config <cfgFile> Location of the config file (defaults to $HOME/.pitrc)
-E, --echo-args Echo arguments (for debug purposes).
-d, --tasks-dir Path to the tasks directory (defaults to the value
configured in the .pitrc file)
--term-width <width> Manually set the terminal width to use.
--ptk Enable PTK integration for this command.
--debug Enable debug-level log output.