Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e35bd9f85a | |||
| 3d8fafd7b2 | |||
| 7d5d55d24a | |||
| 89c924bb72 |
@@ -1,6 +1,6 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "4.31.1"
|
version = "4.33.0"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Personal issue tracker."
|
description = "Personal issue tracker."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
36
src/pit.nim
36
src/pit.nim
@@ -253,11 +253,6 @@ when isMainModule:
|
|||||||
elif args["edit"]:
|
elif args["edit"]:
|
||||||
for editRef in @(args["<ref>"]):
|
for editRef in @(args["<ref>"]):
|
||||||
|
|
||||||
let propsOption =
|
|
||||||
if args["--properties"]:
|
|
||||||
some(parsePropertiesOption($args["--properties"]))
|
|
||||||
else: none(TableRef[string, string])
|
|
||||||
|
|
||||||
var stateOption = none(IssueState)
|
var stateOption = none(IssueState)
|
||||||
|
|
||||||
try: stateOption = some(parseEnum[IssueState](editRef))
|
try: stateOption = some(parseEnum[IssueState](editRef))
|
||||||
@@ -267,10 +262,16 @@ when isMainModule:
|
|||||||
let state = stateOption.get
|
let state = stateOption.get
|
||||||
ctx.loadIssues(state)
|
ctx.loadIssues(state)
|
||||||
for issue in ctx.issues[state]:
|
for issue in ctx.issues[state]:
|
||||||
if propsOption.isSome:
|
if propertiesOption.isSome:
|
||||||
for k,v in propsOption.get:
|
for k,v in propertiesOption.get:
|
||||||
issue[k] = v
|
issue[k] = v
|
||||||
edit(issue)
|
if tagsOption.isSome:
|
||||||
|
issue.tags = deduplicate(issue.tags & tagsOption.get)
|
||||||
|
if exclTagsOption.isSome:
|
||||||
|
issue.tags = issue.tags.filter(
|
||||||
|
proc (tag: string): bool = not exclTagsOption.get.anyIt(it == tag))
|
||||||
|
if args["--non-interactive"]: issue.store()
|
||||||
|
else: edit(issue)
|
||||||
updatedIssues.add(issue)
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -278,7 +279,13 @@ when isMainModule:
|
|||||||
if propertiesOption.isSome:
|
if propertiesOption.isSome:
|
||||||
for k,v in propertiesOption.get:
|
for k,v in propertiesOption.get:
|
||||||
issue[k] = v
|
issue[k] = v
|
||||||
edit(issue)
|
if tagsOption.isSome:
|
||||||
|
issue.tags = deduplicate(issue.tags & tagsOption.get)
|
||||||
|
if exclTagsOption.isSome:
|
||||||
|
issue.tags = issue.tags.filter(
|
||||||
|
proc (tag: string): bool = not exclTagsOption.get.anyIt(it == tag))
|
||||||
|
if args["--non-interactive"]: issue.store()
|
||||||
|
else: edit(issue)
|
||||||
updatedIssues.add(issue)
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
elif args["tag"]:
|
elif args["tag"]:
|
||||||
@@ -306,6 +313,17 @@ when isMainModule:
|
|||||||
issue.store()
|
issue.store()
|
||||||
updatedIssues.add(issue)
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
|
elif args["update-details"]:
|
||||||
|
let details =
|
||||||
|
if not args["--file"] or $args["--file"] == "-": readAll(stdin)
|
||||||
|
else: readFile($args["--file"])
|
||||||
|
|
||||||
|
for id in @(args["<id>"]):
|
||||||
|
var issue = ctx.cfg.tasksDir.loadIssueById(id)
|
||||||
|
issue.details = details
|
||||||
|
issue.store()
|
||||||
|
updatedIssues.add(issue)
|
||||||
|
|
||||||
elif args["start"] or args["todo-today"] or args["done"] or
|
elif args["start"] or args["todo-today"] or args["done"] or
|
||||||
args["pending"] or args["todo"] or args["suspend"]:
|
args["pending"] or args["todo"] or args["suspend"]:
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const PIT_VERSION* = "4.31.1"
|
const PIT_VERSION* = "4.33.0"
|
||||||
|
|
||||||
const USAGE* = """Usage:
|
const USAGE* = """Usage:
|
||||||
pit ( new | add) <summary> [<state>] [options]
|
pit ( new | add) <summary> [<state>] [options]
|
||||||
@@ -10,6 +10,7 @@ const USAGE* = """Usage:
|
|||||||
pit ( start | done | pending | todo-today | todo | suspend ) <id>... [options]
|
pit ( start | done | pending | todo-today | todo | suspend ) <id>... [options]
|
||||||
pit edit <ref>... [options]
|
pit edit <ref>... [options]
|
||||||
pit tag <id>... [options]
|
pit tag <id>... [options]
|
||||||
|
pit update-details <id> [--file=<path>] [options]
|
||||||
pit untag <id>... [options]
|
pit untag <id>... [options]
|
||||||
pit reorder <state> [options]
|
pit reorder <state> [options]
|
||||||
pit delegate <id> <delegated-to> [options]
|
pit delegate <id> <delegated-to> [options]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import std/[json, jsonutils, logging, options, os, strformat, strutils, tables, times,
|
import std/[json, logging, options, os, strformat, strutils, tables, times,
|
||||||
unicode]
|
unicode]
|
||||||
import cliutils, docopt, langutils, uuids, zero_functional
|
import cliutils, docopt, langutils, uuids, zero_functional
|
||||||
|
|
||||||
@@ -230,7 +230,9 @@ proc fromStorageFormat*(id: string, issueTxt: string): Issue =
|
|||||||
var detailLines: seq[string] = @[]
|
var detailLines: seq[string] = @[]
|
||||||
|
|
||||||
for line in issueTxt.splitLines():
|
for line in issueTxt.splitLines():
|
||||||
if line.startsWith("#"): continue # ignore lines starting with '#'
|
if line.startsWith("#") and parseState != ReadingDetails:
|
||||||
|
# ignore lines starting with '#', unless we're in the details section.
|
||||||
|
continue
|
||||||
|
|
||||||
case parseState
|
case parseState
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables,
|
import std/[algorithm, json, jsonutils, options, os, sets, strutils, tables,
|
||||||
terminal, times, unicode, wordwrap]
|
terminal, times, unicode]
|
||||||
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]
|
||||||
|
|
||||||
const NO_PROJECT* = "<no-project>"
|
const NO_PROJECT* = "∅ No Project"
|
||||||
const NO_MILESTONE* = "<no-project>"
|
const NO_MILESTONE* = "∅ No Milestone"
|
||||||
const NO_CONTEXT* = "<no-project>"
|
const NO_CONTEXT* = "<no-context>"
|
||||||
|
|
||||||
type
|
type
|
||||||
ProjectCfg* = ref object of RootObj
|
ProjectCfg* = ref object of RootObj
|
||||||
@@ -129,6 +129,8 @@ proc listProjects*(ctx: CliContext, filter = none[IssueFilter]()) =
|
|||||||
let projectsCfg = ctx.loadProjectsConfiguration()
|
let projectsCfg = ctx.loadProjectsConfiguration()
|
||||||
var projectsCfgChanged = false
|
var projectsCfgChanged = false
|
||||||
|
|
||||||
|
var linesToPrint = newSeq[string]()
|
||||||
|
|
||||||
if filter.isSome: ctx.filterIssues(filter.get)
|
if filter.isSome: ctx.filterIssues(filter.get)
|
||||||
|
|
||||||
let projectsByContext = newTable[string, CountTableRef[string]]()
|
let projectsByContext = newTable[string, CountTableRef[string]]()
|
||||||
@@ -150,10 +152,10 @@ proc listProjects*(ctx: CliContext, filter = none[IssueFilter]()) =
|
|||||||
|
|
||||||
for (context, projects) in pairs(projectsByContext):
|
for (context, projects) in pairs(projectsByContext):
|
||||||
|
|
||||||
stdout.writeLine(withColor(
|
linesToPrint.add(withColor(
|
||||||
ctx.getIssueContextDisplayName(context) & ":",
|
ctx.getIssueContextDisplayName(context) & ":",
|
||||||
fgYellow) & termReset)
|
fgYellow) & termReset)
|
||||||
stdout.writeLine("")
|
linesToPrint.add("")
|
||||||
|
|
||||||
var toList = toHashSet(toSeq(keys(projects)))
|
var toList = toHashSet(toSeq(keys(projects)))
|
||||||
|
|
||||||
@@ -164,22 +166,29 @@ proc listProjects*(ctx: CliContext, filter = none[IssueFilter]()) =
|
|||||||
for project in projectsCfg[context]:
|
for project in projectsCfg[context]:
|
||||||
if project.name in toList:
|
if project.name in toList:
|
||||||
toList.excl(project.name)
|
toList.excl(project.name)
|
||||||
stdout.writeLine(" " & project.name &
|
linesToPrint.add(" " & project.name &
|
||||||
" (" & $projects[project.name] & " issues)")
|
" (" & $projects[project.name] & " issues)")
|
||||||
|
|
||||||
# Then list any remaining projects not in the configuration, and add them
|
# Then list any remaining projects not in the configuration, and add them
|
||||||
# to the configuration
|
# to the configuration
|
||||||
for (projectName, count) in pairs(projects):
|
for (projectName, count) in pairs(projects):
|
||||||
if projectName in toList:
|
if projectName in toList:
|
||||||
stdout.writeLine(" " & projectName & " (" & $count & " issues)")
|
linesToPrint.add(" " & projectName & " (" & $count & " issues)")
|
||||||
projectsCfg[context].add(ProjectCfg(name: projectName, milestoneOrder: @[]))
|
projectsCfg[context].add(ProjectCfg(name: projectName, milestoneOrder: @[]))
|
||||||
projectsCfgChanged = true
|
projectsCfgChanged = true
|
||||||
|
|
||||||
stdout.writeLine("")
|
linesToPrint.add("")
|
||||||
|
|
||||||
if projectsCfgChanged: ctx.saveProjectsConfiguration(projectsCfg)
|
if projectsCfgChanged: ctx.saveProjectsConfiguration(projectsCfg)
|
||||||
|
|
||||||
|
if isatty(stdout):
|
||||||
|
stdout.writeLine(linesToPrint.join("\p"))
|
||||||
|
else:
|
||||||
|
stdout.writeLine(stripAnsi(linesToPrint.join("\p")))
|
||||||
|
|
||||||
proc listMilestones*(ctx: CliContext, filter = none[IssueFilter]()) =
|
proc listMilestones*(ctx: CliContext, filter = none[IssueFilter]()) =
|
||||||
|
var linesToPrint = newSeq[string]()
|
||||||
|
|
||||||
ctx.loadAllIssues()
|
ctx.loadAllIssues()
|
||||||
if filter.isSome: ctx.filterIssues(filter.get)
|
if filter.isSome: ctx.filterIssues(filter.get)
|
||||||
|
|
||||||
@@ -194,8 +203,8 @@ proc listMilestones*(ctx: CliContext, filter = none[IssueFilter]()) =
|
|||||||
if values(project.milestones) --> all(it.len == 0):
|
if values(project.milestones) --> all(it.len == 0):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
stdout.writeLine(withColor(project.name, fgBlue, bold = true, bright=true))
|
linesToPrint.add(withColor(project.name, fgBlue, bold = true, bright=true))
|
||||||
stdout.writeLine(withColor(
|
linesToPrint.add(withColor(
|
||||||
"─".repeat(runeLen(stripAnsi(project.name))),
|
"─".repeat(runeLen(stripAnsi(project.name))),
|
||||||
fgBlue, bold = true))
|
fgBlue, bold = true))
|
||||||
|
|
||||||
@@ -203,9 +212,15 @@ proc listMilestones*(ctx: CliContext, filter = none[IssueFilter]()) =
|
|||||||
if project.milestones.hasKey(milestone) and
|
if project.milestones.hasKey(milestone) and
|
||||||
project.milestones[milestone].len > 0:
|
project.milestones[milestone].len > 0:
|
||||||
let issueCount = project.milestones[milestone].len
|
let issueCount = project.milestones[milestone].len
|
||||||
stdout.writeLine(" " & milestone & " (" & $issueCount & " issues)")
|
linesToPrint.add(" " & milestone & " (" & $issueCount & " issues)")
|
||||||
|
|
||||||
|
linesToPrint.add("")
|
||||||
|
|
||||||
|
if isatty(stdout):
|
||||||
|
stdout.writeLine(linesToPrint.join("\p"))
|
||||||
|
else:
|
||||||
|
stdout.writeLine(stripAnsi(linesToPrint.join("\p")))
|
||||||
|
|
||||||
stdout.writeLine("")
|
|
||||||
|
|
||||||
proc formatProjectIssue(
|
proc formatProjectIssue(
|
||||||
ctx: CliContext,
|
ctx: CliContext,
|
||||||
@@ -306,22 +321,23 @@ proc joinColumns(columns: seq[seq[string]], columnWidth: int): seq[string] =
|
|||||||
|
|
||||||
proc showProject*(ctx: CliContext, project: Project) =
|
proc showProject*(ctx: CliContext, project: Project) =
|
||||||
|
|
||||||
|
var linesToPrint = newSeq[string]()
|
||||||
let fullWidth = terminalWidth() - 1
|
let fullWidth = terminalWidth() - 1
|
||||||
let columnWidth = 80
|
let columnWidth = 80
|
||||||
let numColumns = (fullWidth - 4) div (columnWidth + 2)
|
let numColumns = (fullWidth - 4) div (columnWidth + 2)
|
||||||
|
|
||||||
stdout.writeLine("")
|
linesToPrint.add("")
|
||||||
stdout.writeLine(withColor(
|
linesToPrint.add(withColor(
|
||||||
"┌" & "─".repeat(project.name.len + 2) &
|
"┌" & "─".repeat(project.name.runeLen + 2) &
|
||||||
"┬" & "─".repeat(fullWidth - project.name.len - 4) & "┐",
|
"┬" & "─".repeat(fullWidth - project.name.runeLen - 4) & "┐",
|
||||||
fgBlue, bold=true))
|
fgBlue, bold=true))
|
||||||
stdout.writeLine(
|
linesToPrint.add(
|
||||||
withColor("│ ", fgBlue, bold=true) &
|
withColor("│ ", fgBlue, bold=true) &
|
||||||
withColor(project.name, fgBlue, bold=true, bright=true) &
|
withColor(project.name, fgBlue, bold=true, bright=true) &
|
||||||
withColor(" │" & " ".repeat(fullWidth - project.name.len - 4) & "│", fgBlue, bold=true))
|
withColor(" │" & " ".repeat(fullWidth - project.name.runeLen - 4) & "│", fgBlue, bold=true))
|
||||||
stdout.writeLine(withColor(
|
linesToPrint.add(withColor(
|
||||||
"├" & "─".repeat(project.name.len + 2) &
|
"├" & "─".repeat(project.name.runeLen + 2) &
|
||||||
"┘" & " ".repeat(fullWidth - project.name.len - 4) & "│",
|
"┘" & " ".repeat(fullWidth - project.name.runeLen - 4) & "│",
|
||||||
fgBlue, bold=true))
|
fgBlue, bold=true))
|
||||||
|
|
||||||
let milestoneTexts: seq[seq[string]] = project.milestoneOrder -->
|
let milestoneTexts: seq[seq[string]] = project.milestoneOrder -->
|
||||||
@@ -338,16 +354,22 @@ proc showProject*(ctx: CliContext, project: Project) =
|
|||||||
|
|
||||||
for line in joinedLines:
|
for line in joinedLines:
|
||||||
let padLen = fullWidth - runeLen(stripAnsi(line)) - 3
|
let padLen = fullWidth - runeLen(stripAnsi(line)) - 3
|
||||||
stdout.writeLine(
|
linesToPrint.add(
|
||||||
withColor("│ ", fgBlue) &
|
withColor("│ ", fgBlue) &
|
||||||
line &
|
line &
|
||||||
" ".repeat(padLen) &
|
" ".repeat(padLen) &
|
||||||
withColor(" │", fgBlue))
|
withColor(" │", fgBlue))
|
||||||
|
|
||||||
stdout.writeLine(withColor(
|
linesToPrint.add(withColor(
|
||||||
"└" & "─".repeat(terminalWidth() - 2) & "┘",
|
"└" & "─".repeat(terminalWidth() - 2) & "┘",
|
||||||
fgBlue, bold=true))
|
fgBlue, bold=true))
|
||||||
|
|
||||||
|
if isatty(stdout):
|
||||||
|
stdout.writeLine(linesToPrint.join("\p"))
|
||||||
|
else:
|
||||||
|
stdout.writeLine(stripAnsi(linesToPrint.join("\p")))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
proc showProjectBoard*(ctx: CliContext, filter = none[IssueFilter]()) =
|
proc showProjectBoard*(ctx: CliContext, filter = none[IssueFilter]()) =
|
||||||
ctx.loadAllIssues()
|
ctx.loadAllIssues()
|
||||||
|
|||||||
Reference in New Issue
Block a user