Compare commits

...

1 Commits
4.30.1 ... main

4 changed files with 105 additions and 31 deletions

View File

@@ -1,6 +1,6 @@
# Package # Package
version = "4.30.1" version = "4.31.0"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Personal issue tracker." description = "Personal issue tracker."
license = "MIT" license = "MIT"

View File

@@ -1,10 +1,11 @@
## Personal Issue Tracker CLI interface ## Personal Issue Tracker CLI interface
## ==================================== ## ====================================
import std/[algorithm, logging, options, os, sequtils, tables, times, unicode] import std/[algorithm, logging, options, os, sequtils, sets, tables, terminal,
import data_uri, docopt, json, timeutils, uuids times, unicode]
import cliutils, data_uri, docopt, json, timeutils, uuids, zero_functional
from nre import re from nre import match, re
import strutils except alignLeft, capitalize, strip, toUpper, toLower import strutils except alignLeft, capitalize, strip, toUpper, toLower
import pit/[cliconstants, formatting, libpit, projects, sync_pbm_vsb] import pit/[cliconstants, formatting, libpit, projects, sync_pbm_vsb]
@@ -38,6 +39,88 @@ proc reorder(ctx: CliContext, state: IssueState) =
ctx.loadIssues(state) ctx.loadIssues(state)
discard os.execShellCmd(EDITOR & " '" & (ctx.cfg.tasksDir / $state / "order.txt") & "' </dev/tty >/dev/tty") discard os.execShellCmd(EDITOR & " '" & (ctx.cfg.tasksDir / $state / "order.txt") & "' </dev/tty >/dev/tty")
proc addIssue(
ctx: CliContext,
args: Table[string, Value],
propertiesOption = none[TableRef[string, string]](),
tagsOption = none[seq[string]]()): Issue =
let state =
if args["<state>"]: parseEnum[IssueState]($args["<state>"])
else: TodoToday
var issueProps = propertiesOption.get(newTable[string,string]())
if not issueProps.hasKey("created"): issueProps["created"] = getTime().local.formatIso8601
if not issueProps.hasKey("context") and ctx.defaultContext.isSome():
stderr.writeLine("Using default context: " & ctx.defaultContext.get)
issueProps["context"] = ctx.defaultContext.get
if not args["--non-interactive"]:
# look for default properties for this context
let globalDefaultProps =
if ctx.cfg.defaultPropertiesByContext.hasKey("<all>"):
ctx.cfg.defaultPropertiesByContext["<all>"]
else: newSeq[string]()
let contextDefaultProps =
if issueProps.hasKey("context") and
ctx.cfg.defaultPropertiesByContext.hasKey(issueProps["context"]):
ctx.cfg.defaultPropertiesByContext[issueProps["context"]]
else: newSeq[string]()
let defaultProps = toOrderedSet(globalDefaultProps & contextDefaultProps)
if defaultProps.len > 0:
ctx.loadAllIssues()
if issueProps.hasKey("context"):
ctx.filterIssues(propsFilter(newTable({"context": issueProps["context"]})))
let numberRegex = re("^[0-9]+$")
for propName in defaultProps:
if not issueProps.hasKey(propName):
let allIssues: seq[seq[Issue]] = toSeq(values(ctx.issues))
let previousValues = toSeq(toHashSet(allIssues -->
flatten()
.filter(it.hasProp(propName))
.map(it[propName])))
let idxValPairs: seq[tuple[key: int, val: string]] = toSeq(pairs(previousValues))
let previousValuesDisplay: seq[string] = idxValPairs -->
map(" " & $it[0] & " - " & it[1])
stdout.write(
"Previous values for property '" & propName & "':\p" &
previousValuesDisplay.join("\p") & "\p" &
"Do you want to set a value for '" & propName & "'? " &
"You can use the numbers above to use an existing value, enter " &
"something new, or leave blank to indicate no value.\p" &
withColor(propName, fgMagenta) & ":" &
withColor(" ", fgBlue, bright=true, skipReset=true))
let resp = stdin.readLine.strip
let numberResp = resp.match(numberRegex)
if numberResp.isSome:
let idx = parseInt(resp)
if idx >= 0 and idx < previousValues.len:
issueProps[propName] = previousValues[idx]
elif resp.len > 0:
issueProps[propName] = resp
stdout.writeLine(termReset)
result = Issue(
id: genUUID(),
summary: $args["<summary>"],
properties: issueProps,
tags:
if tagsOption.isSome: tagsOption.get
else: newSeq[string]())
ctx.cfg.tasksDir.store(result, state)
stdout.writeLine "\p" & formatIssue(result)
proc edit(issue: Issue) = proc edit(issue: Issue) =
# Write format comments (to help when editing) # Write format comments (to help when editing)
@@ -171,27 +254,7 @@ when isMainModule:
## Actual command runners ## Actual command runners
if args["new"] or args["add"]: if args["new"] or args["add"]:
let state = updatedIssues.add(ctx.addIssue(args, propertiesOption, tagsOption))
if args["<state>"]: parseEnum[IssueState]($args["<state>"])
else: TodoToday
var issueProps = propertiesOption.get(newTable[string,string]())
if not issueProps.hasKey("created"): issueProps["created"] = getTime().local.formatIso8601
if not issueProps.hasKey("context") and ctx.defaultContext.isSome():
stderr.writeLine("Using default context: " & ctx.defaultContext.get)
issueProps["context"] = ctx.defaultContext.get
var issue = Issue(
id: genUUID(),
summary: $args["<summary>"],
properties: issueProps,
tags:
if tagsOption.isSome: tagsOption.get
else: newSeq[string]())
ctx.cfg.tasksDir.store(issue, state)
updatedIssues.add(issue)
stdout.writeLine formatIssue(issue)
elif args["reorder"]: elif args["reorder"]:
ctx.reorder(parseEnum[IssueState]($args["<state>"])) ctx.reorder(parseEnum[IssueState]($args["<state>"]))
@@ -318,7 +381,7 @@ when isMainModule:
let issue = ctx.cfg.tasksDir.loadIssueById(id) let issue = ctx.cfg.tasksDir.loadIssueById(id)
if not args["--yes"]: if not args["--non-interactive"]:
stderr.write("Delete '" & issue.summary & "' (y/n)? ") stderr.write("Delete '" & issue.summary & "' (y/n)? ")
if not "yes".startsWith(stdin.readLine.toLower): if not "yes".startsWith(stdin.readLine.toLower):
continue continue

View File

@@ -1,4 +1,4 @@
const PIT_VERSION* = "4.30.1" const PIT_VERSION* = "4.31.0"
const USAGE* = """Usage: const USAGE* = """Usage:
pit ( new | add) <summary> [<state>] [options] pit ( new | add) <summary> [<state>] [options]
@@ -71,8 +71,6 @@ Options:
-q, --quiet Suppress verbose output. -q, --quiet Suppress verbose output.
-y, --yes Automatically answer "yes" to any prompts.
--config <cfgFile> Location of the config file (defaults to $HOME/.pitrc) --config <cfgFile> Location of the config file (defaults to $HOME/.pitrc)
-E, --echo-args Echo arguments (for debug purposes). -E, --echo-args Echo arguments (for debug purposes).
@@ -88,7 +86,13 @@ Options:
only print the changes that would be made, but do only print the changes that would be made, but do
not actually make them. not actually make them.
-s, --silent Suppress all logging and status output. -I, --non-interactive Run in non-interactive mode. Commands that would
normally prompt for user input will instead use
default values or fail if required input is not
provided via command-line options.
-s, --silent Suppress all logging and status output and run in
non-interactive mode (implies --non-interactive).
""" """
const ONLINE_HELP* = """Issue States: const ONLINE_HELP* = """Issue States:

View File

@@ -1,4 +1,4 @@
import std/[json, logging, options, os, strformat, strutils, tables, times, import std/[json, jsonutils, logging, options, os, strformat, strutils, tables, times,
unicode] unicode]
import cliutils, docopt, langutils, uuids, zero_functional import cliutils, docopt, langutils, uuids, zero_functional
@@ -38,6 +38,7 @@ type
PitConfig* = ref object PitConfig* = ref object
tasksDir*: string tasksDir*: string
contexts*: TableRef[string, string] contexts*: TableRef[string, string]
defaultPropertiesByContext*: TableRef[string, seq[string]]
autoSync*: bool autoSync*: bool
syncTargets*: seq[JsonNode] syncTargets*: seq[JsonNode]
cfg*: CombinedConfig cfg*: CombinedConfig
@@ -486,12 +487,18 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitCo
cfg: cfg, cfg: cfg,
autoSync: parseBool(cfg.getVal("auto-sync", "false")), autoSync: parseBool(cfg.getVal("auto-sync", "false")),
contexts: newTable[string,string](), contexts: newTable[string,string](),
defaultPropertiesByContext: newTable[string, seq[string]](),
tasksDir: cfg.getVal("tasks-dir", ""), tasksDir: cfg.getVal("tasks-dir", ""),
syncTargets: cfg.getJson("sync-targets", newJArray()).getElems) syncTargets: cfg.getJson("sync-targets", newJArray()).getElems)
for k, v in cfg.getJson("contexts", newJObject()): for k, v in cfg.getJson("contexts", newJObject()):
result.contexts[k] = v.getStr() result.contexts[k] = v.getStr()
for k, v in cfg.getJson("defaultPropertiesByContext", newJObject()):
result.defaultPropertiesByContext[k] = v.getElems() -->
map(it.getStr("").strip())
.filter(not it.isEmptyOrWhitespace)
if isEmptyOrWhitespace(result.tasksDir): if isEmptyOrWhitespace(result.tasksDir):
raise newException(Exception, "no tasks directory configured") raise newException(Exception, "no tasks directory configured")