Compare commits

..

2 Commits

Author SHA1 Message Date
3d1dc7512a Allow including or excluding properties by name only. 2025-12-01 14:04:19 -06:00
1d18be9d1b Include issues without project or milestone on boards.
In order to help organize issues, show issues on boards even if they
don't have an assigned project or milestone.

Refactor the issue hiding feature (using the `hide-until` property) to
be an option to IssueFilter rather than a separate, special-case. This
means that the CLI always filters by default.

Hide issues in the Done state on project boards unless the new
`--show-done` arg is passed.
2025-12-01 13:50:13 -06:00
6 changed files with 72 additions and 51 deletions

View File

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

View File

@@ -19,18 +19,16 @@ proc parsePropertiesOption(propsOpt: string): TableRef[string, string] =
result = newTable[string, string]()
for propText in propsOpt.split(";"):
let pair = propText.split(":", 1)
if pair.len == 1: result[pair[0]] = "true"
if pair.len == 1: result[pair[0]] = MATCH_ANY
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]
if not result.hasKey(pair[0]): result[pair[0]] = @[]
if pair.len == 2: result[pair[0]].add(pair[1])
proc reorder(ctx: CliContext, state: IssueState) =
@@ -39,6 +37,7 @@ proc reorder(ctx: CliContext, state: IssueState) =
ctx.loadIssues(state)
discard os.execShellCmd(EDITOR & " '" & (ctx.cfg.tasksDir / $state / "order.txt") & "' </dev/tty >/dev/tty")
proc addIssue(
ctx: CliContext,
args: Table[string, Value],
@@ -172,8 +171,6 @@ when isMainModule:
var exclTagsOption = none(seq[string])
let filter = initFilter()
var filterOption = none(IssueFilter)
if args["--properties"] or args["--context"]:
@@ -207,43 +204,37 @@ when isMainModule:
# Initialize filter with properties (if given)
if propertiesOption.isSome:
filter.properties = propertiesOption.get
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 args["--match"]:
filter.summaryMatch = some(re("(?i)" & $args["--match"]))
filterOption = some(filter)
if args["--match-all"]:
filter.fullMatch = some(re("(?i)" & $args["--match-all"]))
filterOption = some(filter)
# If no "context" property is given, use the default (if we have one)
if ctx.defaultContext.isSome and not filter.properties.hasKey("context"):
stderr.writeLine("Limiting to default context: " & ctx.defaultContext.get)
filter.properties["context"] = ctx.defaultContext.get
filterOption = some(filter)
if tagsOption.isSome:
filter.hasTags = tagsOption.get
filterOption = some(filter)
if exclTagsOption.isSome:
filter.exclTags = exclTagsOption.get
filterOption = some(filter)
if args["--today"]:
filter.inclStates.add(@[Current, TodoToday, Pending])
filterOption = some(filter)
if args["--future"]:
filter.inclStates.add(@[Pending, Todo])
filterOption = some(filter)
if args["--show-hidden"]:
filter.exclHidden = false
# Finally, if the "context" is "all", don't filter on context
if filter.properties.hasKey("context") and
@@ -433,7 +424,7 @@ when isMainModule:
for state in statesOption.get: ctx.loadIssues(state)
else: ctx.loadAllIssues()
if filterOption.isSome: ctx.filterIssues(filterOption.get)
ctx.filterIssues(filter)
for state, issueList in ctx.issues:
for issue in issueList:
@@ -449,27 +440,27 @@ when isMainModule:
stdout.writeLine formatIssue(issue)
# List projects
elif listProjects: ctx.listProjects(filterOption)
elif listProjects: ctx.listProjects(some(filter))
# List milestones
elif listMilestones: ctx.listMilestones(filterOption)
elif listMilestones: ctx.listMilestones(some(filter))
# List all issues
else:
trace "listing all issues"
let showBoth = args["--today"] == args["--future"]
ctx.list(
filter = filterOption,
filter = some(filter),
states = statesOption,
showToday = showBoth or args["--today"],
showFuture = showBoth or args["--future"],
showHidden = args["--show-hidden"],
verbose = ctx.verbose)
elif args["show"]:
if args["project-board"]:
ctx.showProjectBoard(filterOption)
if not args["--show-done"]: filter.exclStates.add(Done)
ctx.showProjectBoard(some(filter))
discard
elif args["dupes"]:

View File

@@ -1,4 +1,4 @@
const PIT_VERSION* = "4.31.0"
const PIT_VERSION* = "4.31.1"
const USAGE* = """Usage:
pit ( new | add) <summary> [<state>] [options]
@@ -18,7 +18,7 @@ const USAGE* = """Usage:
pit add-binary-property <id> <propName> <propSource> [options]
pit get-binary-property <id> <propName> <propDest> [options]
pit show dupes [options]
pit show project-board [options]
pit show project-board [--show-done] [options]
pit show <id> [options]
pit sync [<syncTarget>...] [options]
pit help [options]
@@ -33,12 +33,20 @@ Options:
a filter to the issues listed, only allowing those
which have all of the given properties.
If a propert name is provided without a value, this
will allow all issues which have any value defined
for the named property.
-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"
If no value is provided for a property, this will
filter out all issues with *any* value for that
property.
-c, --context <ctx> Shorthand for '-p context:<ctx>'
-C, --excl-context <ctx> Don't show issues from the given context(s).
@@ -239,4 +247,4 @@ Issue Properties:
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

@@ -173,7 +173,6 @@ proc list*(
states: Option[seq[IssueState]],
showToday = false,
showFuture = false,
showHidden = false,
verbose: bool) =
if states.isSome:
@@ -219,10 +218,7 @@ proc list*(
for s in [Current, TodoToday, Pending]:
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
let visibleIssues = ctx.issues[s].filterIt(
showHidden or
not (it.hasProp("hide-until") and
it.getDateTime("hide-until") > getTime().local))
let visibleIssues = ctx.issues[s]
if isatty(stdout):
stdout.write ctx.formatSection(visibleIssues, s, indent, verbose)
@@ -242,10 +238,7 @@ proc list*(
for s in futureCategories:
if ctx.issues.hasKey(s) and ctx.issues[s].len > 0:
let visibleIssues = ctx.issues[s].filterIt(
showHidden or
not (it.hasProp("hide-until") and
it.getDateTime("hide-until") > getTime().local))
let visibleIssues = ctx.issues[s]
if isatty(stdout):
stdout.write ctx.formatSection(visibleIssues, s, indent, verbose)

View File

@@ -30,6 +30,7 @@ type
exclStates*: seq[IssueState]
hasTags*: seq[string]
exclTags*: seq[string]
exclHidden*: bool
properties*: TableRef[string, string]
exclProperties*: TableRef[string, seq[string]]
@@ -56,6 +57,7 @@ type
isFromCompletion*: bool
const MATCH_ANY* = "<match-any>"
const DONE_FOLDER_FORMAT* = "yyyy-MM"
const ISO8601_MS = "yyyy-MM-dd'T'HH:mm:ss'.'fffzzz"
@@ -151,6 +153,7 @@ proc initFilter*(): IssueFilter =
summaryMatch: none(Regex),
inclStates: @[],
exclStates: @[],
exclHidden: true,
hasTags: @[],
exclTags: @[],
properties: newTable[string, string](),
@@ -184,6 +187,10 @@ proc stateFilter*(states: seq[IssueState]): IssueFilter =
result = initFilter()
result.inclStates = states
proc showHiddenFilter*(): IssueFilter =
result = initFilter()
result.exclHidden = false
proc groupBy*(issues: seq[Issue], propertyKey: string): TableRef[string, seq[Issue]] =
result = newTable[string, seq[Issue]]()
for i in issues:
@@ -416,11 +423,23 @@ proc nextRecurrence*(tasksDir: string, rec: Recurrence, defaultIssue: Issue): Is
proc filter*(issues: seq[Issue], filter: IssueFilter): seq[Issue] =
var f: seq[Issue] = issues
if filter.exclHidden:
let now = getTime().local
f = f --> filter(
not it.hasProp("hide-until") or
it.getDateTime("hide-until") <= now)
for k,v in filter.properties:
f = f --> filter(it.hasProp(k) and it[k] == v)
if v == MATCH_ANY:
f = f --> filter(it.hasProp(k))
else:
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 v.len == 0:
f = f --> filter(not (it.hasProp(k)))
else:
f = f --> filter(not (it.hasProp(k) and v.contains(it[k])))
if filter.completedRange.isSome:
let range = filter.completedRange.get

View File

@@ -4,6 +4,10 @@ from std/sequtils import repeat, toSeq
import cliutils, uuids, zero_functional
import ./[formatting, libpit]
const NO_PROJECT* = "<no-project>"
const NO_MILESTONE* = "<no-project>"
const NO_CONTEXT* = "<no-project>"
type
ProjectCfg* = ref object of RootObj
name: string
@@ -62,15 +66,18 @@ proc buildDb*(ctx: CliContext, cfg: ProjectsConfiguration): ProjectsDatabase =
# Now populate the database with issues
for (state, issues) in pairs(ctx.issues):
for issue in issues:
if not issue.hasProp("project") or
not issue.hasProp("milestone"):
continue
let projectName = issue["project"]
let milestone = issue["milestone"]
let projectName =
if issue.hasProp("project"): issue["project"]
else: NO_PROJECT
let milestone =
if issue.hasProp("milestone"): issue["milestone"]
else: NO_MILESTONE
let context =
if issue.hasProp("context"): issue["context"]
else: "<no-context>"
else: NO_CONTEXT
# Make sure we have entries for this context and project
if not result.hasKey(context): result[context] = @[]
@@ -128,15 +135,18 @@ proc listProjects*(ctx: CliContext, filter = none[IssueFilter]()) =
for (state, issues) in pairs(ctx.issues):
for issue in issues:
if issue.hasProp("project"):
let context =
if issue.hasProp("context"): issue["context"]
else: "<no-context>"
let context =
if issue.hasProp("context"): issue["context"]
else: NO_CONTEXT
if not projectsByContext.hasKey(context):
projectsByContext[context] = newCountTable[string]()
let projectName =
if issue.hasProp("project"): issue["project"]
else: NO_PROJECT
projectsByContext[context].inc(issue["project"])
if not projectsByContext.hasKey(context):
projectsByContext[context] = newCountTable[string]()
projectsByContext[context].inc(projectName)
for (context, projects) in pairs(projectsByContext):