From 6ceca9b00923d6175aa5eb7849978bfe4d75a4fc Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Mon, 24 Nov 2025 10:47:22 -0600 Subject: [PATCH] Make `add` interactive and allow defining default properties that should be prompted. --- pit.nimble | 2 +- src/pit.nim | 113 ++++++++++++++++++++++++++++++--------- src/pit/cliconstants.nim | 12 +++-- src/pit/libpit.nim | 9 +++- 4 files changed, 105 insertions(+), 31 deletions(-) diff --git a/pit.nimble b/pit.nimble index e52cf1b..f8fdd8f 100644 --- a/pit.nimble +++ b/pit.nimble @@ -1,6 +1,6 @@ # Package -version = "4.30.1" +version = "4.31.0" author = "Jonathan Bernard" description = "Personal issue tracker." license = "MIT" diff --git a/src/pit.nim b/src/pit.nim index 2d6de1c..637bfa7 100644 --- a/src/pit.nim +++ b/src/pit.nim @@ -1,10 +1,11 @@ ## Personal Issue Tracker CLI interface ## ==================================== -import std/[algorithm, logging, options, os, sequtils, tables, times, unicode] -import data_uri, docopt, json, timeutils, uuids +import std/[algorithm, logging, options, os, sequtils, sets, tables, terminal, + 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 pit/[cliconstants, formatting, libpit, projects, sync_pbm_vsb] @@ -38,6 +39,88 @@ proc reorder(ctx: CliContext, state: IssueState) = ctx.loadIssues(state) discard os.execShellCmd(EDITOR & " '" & (ctx.cfg.tasksDir / $state / "order.txt") & "' /dev/tty") +proc addIssue( + ctx: CliContext, + args: Table[string, Value], + propertiesOption = none[TableRef[string, string]](), + tagsOption = none[seq[string]]()): Issue = + + let state = + if args[""]: parseEnum[IssueState]($args[""]) + 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(""): + ctx.cfg.defaultPropertiesByContext[""] + 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[""], + 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) = # Write format comments (to help when editing) @@ -171,27 +254,7 @@ when isMainModule: ## Actual command runners if args["new"] or args["add"]: - let state = - if args[""]: parseEnum[IssueState]($args[""]) - 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[""], - properties: issueProps, - tags: - if tagsOption.isSome: tagsOption.get - else: newSeq[string]()) - - ctx.cfg.tasksDir.store(issue, state) - updatedIssues.add(issue) - stdout.writeLine formatIssue(issue) + updatedIssues.add(ctx.addIssue(args, propertiesOption, tagsOption)) elif args["reorder"]: ctx.reorder(parseEnum[IssueState]($args[""])) @@ -318,7 +381,7 @@ when isMainModule: let issue = ctx.cfg.tasksDir.loadIssueById(id) - if not args["--yes"]: + if not args["--non-interactive"]: stderr.write("Delete '" & issue.summary & "' (y/n)? ") if not "yes".startsWith(stdin.readLine.toLower): continue diff --git a/src/pit/cliconstants.nim b/src/pit/cliconstants.nim index 010d8bf..49d4422 100644 --- a/src/pit/cliconstants.nim +++ b/src/pit/cliconstants.nim @@ -1,4 +1,4 @@ -const PIT_VERSION* = "4.30.1" +const PIT_VERSION* = "4.31.0" const USAGE* = """Usage: pit ( new | add) [] [options] @@ -71,8 +71,6 @@ Options: -q, --quiet Suppress verbose output. - -y, --yes Automatically answer "yes" to any prompts. - --config Location of the config file (defaults to $HOME/.pitrc) -E, --echo-args Echo arguments (for debug purposes). @@ -88,7 +86,13 @@ Options: only print the changes that would be made, but do 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: diff --git a/src/pit/libpit.nim b/src/pit/libpit.nim index aea4b4d..8c9f990 100644 --- a/src/pit/libpit.nim +++ b/src/pit/libpit.nim @@ -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] import cliutils, docopt, langutils, uuids, zero_functional @@ -38,6 +38,7 @@ type PitConfig* = ref object tasksDir*: string contexts*: TableRef[string, string] + defaultPropertiesByContext*: TableRef[string, seq[string]] autoSync*: bool syncTargets*: seq[JsonNode] cfg*: CombinedConfig @@ -486,12 +487,18 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): PitCo cfg: cfg, autoSync: parseBool(cfg.getVal("auto-sync", "false")), contexts: newTable[string,string](), + defaultPropertiesByContext: newTable[string, seq[string]](), tasksDir: cfg.getVal("tasks-dir", ""), syncTargets: cfg.getJson("sync-targets", newJArray()).getElems) for k, v in cfg.getJson("contexts", newJObject()): 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): raise newException(Exception, "no tasks directory configured")