Compare commits
2 Commits
5dd7a15bf4
...
4.31.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ceca9b009 | |||
| de07665a8b |
@@ -1,6 +1,6 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "4.30.0"
|
version = "4.31.0"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Personal issue tracker."
|
description = "Personal issue tracker."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
113
src/pit.nim
113
src/pit.nim
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const PIT_VERSION* = "4.30.0"
|
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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables, terminal,
|
import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables,
|
||||||
times, unicode, wordwrap]
|
terminal, times, unicode, wordwrap]
|
||||||
from std/sequtils import repeat, toSeq
|
from std/sequtils import repeat, toSeq
|
||||||
import cliutils, uuids, zero_functional
|
import cliutils, uuids, zero_functional
|
||||||
import ./[formatting, libpit]
|
import ./[formatting, libpit]
|
||||||
@@ -256,8 +256,7 @@ proc formatMilestone*(
|
|||||||
result.add(withColor("─".repeat(availWidth), fgWhite))
|
result.add(withColor("─".repeat(availWidth), fgWhite))
|
||||||
|
|
||||||
var parentsToChildren = issues -->
|
var parentsToChildren = issues -->
|
||||||
filter(it.hasProp("parent")) -->
|
filter(it.hasProp("parent")).group(it["parent"])
|
||||||
group(it["parent"])
|
|
||||||
|
|
||||||
var issuesToFormat = sorted(issues, cmp) -->
|
var issuesToFormat = sorted(issues, cmp) -->
|
||||||
filter(not it.hasProp("parent"))
|
filter(not it.hasProp("parent"))
|
||||||
@@ -282,7 +281,7 @@ proc findShortestColumn(columns: seq[seq[string]]): int =
|
|||||||
|
|
||||||
|
|
||||||
proc joinColumns(columns: seq[seq[string]], columnWidth: int): seq[string] =
|
proc joinColumns(columns: seq[seq[string]], columnWidth: int): seq[string] =
|
||||||
let maxLines = columns --> map(it.len) --> max()
|
let maxLines = columns --> map(it.len).max()
|
||||||
|
|
||||||
for lineNo in 0 ..< maxLines:
|
for lineNo in 0 ..< maxLines:
|
||||||
var newLine = ""
|
var newLine = ""
|
||||||
@@ -316,7 +315,7 @@ proc showProject*(ctx: CliContext, project: Project) =
|
|||||||
fgBlue, bold=true))
|
fgBlue, bold=true))
|
||||||
|
|
||||||
let milestoneTexts: seq[seq[string]] = project.milestoneOrder -->
|
let milestoneTexts: seq[seq[string]] = project.milestoneOrder -->
|
||||||
filter(project.milestones.hasKey(it) and project.milestones[it].len > 0) -->
|
filter(project.milestones.hasKey(it) and project.milestones[it].len > 0).
|
||||||
map(ctx.formatMilestone(it, project.milestones[it], columnWidth))
|
map(ctx.formatMilestone(it, project.milestones[it], columnWidth))
|
||||||
|
|
||||||
var columns: seq[seq[string]] = repeat(newSeq[string](), numColumns)
|
var columns: seq[seq[string]] = repeat(newSeq[string](), numColumns)
|
||||||
@@ -347,8 +346,16 @@ proc showProjectBoard*(ctx: CliContext, filter = none[IssueFilter]()) =
|
|||||||
let projectsCfg = ctx.loadProjectsConfiguration()
|
let projectsCfg = ctx.loadProjectsConfiguration()
|
||||||
let projectsDb = ctx.buildDb(projectsCfg)
|
let projectsDb = ctx.buildDb(projectsCfg)
|
||||||
|
|
||||||
for (context, projects) in pairs(projectsDb):
|
var contextsAndProjects: seq[(string, seq[Project])] = @[]
|
||||||
if projectsDb.len > 1:
|
|
||||||
|
for (context, pjs) in pairs(projectsDb):
|
||||||
|
let projects = pjs
|
||||||
|
let issues: seq[Issue] = projects --> map(toSeq(values(it.milestones))).flatten().flatten()
|
||||||
|
if issues.len > 0:
|
||||||
|
contextsAndProjects.add((context, projects))
|
||||||
|
|
||||||
|
for (context, projects) in contextsAndProjects:
|
||||||
|
if contextsAndProjects.len > 1:
|
||||||
stdout.writeLine("")
|
stdout.writeLine("")
|
||||||
stdout.writeLine(withColor(
|
stdout.writeLine(withColor(
|
||||||
ctx.getIssueContextDisplayName(context) & ":",
|
ctx.getIssueContextDisplayName(context) & ":",
|
||||||
|
|||||||
Reference in New Issue
Block a user