9 Commits

Author SHA1 Message Date
75f74c3a0a Make -m case insensitive. 2018-04-30 11:05:46 -05:00
e5c6e6187c Add --yesterday and --or filter criteria.
* `--yesterday` is like `--today`, but for yesterday 😄.
* `--or` changes the behavior of filter criteria to return a union of
  events matching an of the criteria than an intersection of events
  matching all the criteria. Example:

      ptk list --today --yesterday

  Will never return results (because the criteria are mutually
  exclusive).

      ptk list --today --or --yesterday

  Will return the marks for both today and yesterday.
2018-04-27 13:22:06 -05:00
81d326c5c8 Filter out empty whitespace when considering new tags. 2018-04-16 04:29:19 -05:00
72c332fa45 Add start as an alias for add. 2018-04-09 09:53:00 -05:00
4a878026d8 Bump library version to compile under Nim 0.18 2018-04-02 14:44:26 -05:00
ee733957c6 Add current command. Reformat output of notes. 2017-09-19 10:37:28 -05:00
d75f5607c2 Fix edge case when no marks exist yet. 2017-05-16 12:32:59 -05:00
15708cebdf Add explicit dependency on isaac >= 0.1.2
Something depends on isaac 0.1.0, but that version doesn't compile on Nim 0.16
and above. Until the transitive dependancy is updated, ask for at least 0.1.2.
2017-02-21 11:18:12 -06:00
78a35b6478 Added list tags command. 2017-02-09 10:57:51 -06:00
4 changed files with 109 additions and 49 deletions

8
private/ptkutil.nim Normal file
View File

@ -0,0 +1,8 @@
template first*(a: openarray): auto = a[0]
template last*(a: openarray): auto = a[len(a)-1]
proc flatten*[T](a: seq[seq[T]]): seq[T] =
result = @[]
for subseq in a:
result.add(subseq)

143
ptk.nim
View File

@ -6,10 +6,10 @@
import algorithm, docopt, json, langutils, logging, os, nre, sequtils, import algorithm, docopt, json, langutils, logging, os, nre, sequtils,
sets, strutils, tempfile, terminal, times, timeutils, uuids sets, strutils, tempfile, terminal, times, timeutils, uuids
import ptkutil import private/ptkutil
type type
Mark* = tuple[id: UUID, time: TimeInfo, summary: string, notes: string, tags: seq[string]] Mark* = tuple[id: UUID, time: DateTime, summary: string, notes: string, tags: seq[string]]
## Representation of a single mark on the timeline. ## Representation of a single mark on the timeline.
Timeline* = tuple[name: string, marks: seq[Mark]] Timeline* = tuple[name: string, marks: seq[Mark]]
@ -19,7 +19,7 @@ const STOP_MSG = "STOP"
let NO_MARK: Mark = ( let NO_MARK: Mark = (
id: parseUUID("00000000-0000-0000-0000-000000000000"), id: parseUUID("00000000-0000-0000-0000-000000000000"),
time: fromSeconds(0).getLocalTime, time: fromUnix(0).local,
summary: "", notes: "", tags: @[]) summary: "", notes: "", tags: @[])
const ISO_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" const ISO_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
@ -40,7 +40,7 @@ proc exitErr(msg: string): void =
fatal "ptk: " & msg fatal "ptk: " & msg
quit(QuitFailure) quit(QuitFailure)
proc parseTime(timeStr: string): TimeInfo = proc parseTime(timeStr: string): DateTime =
## Helper to parse time strings trying multiple known formats. ## Helper to parse time strings trying multiple known formats.
for fmt in TIME_FORMATS: for fmt in TIME_FORMATS:
try: return parse(timeStr, fmt) try: return parse(timeStr, fmt)
@ -78,7 +78,7 @@ proc loadTimeline(filename: string): Timeline =
# TODO: an incorrect time format that was used in version 0.6 and prior. # TODO: an incorrect time format that was used in version 0.6 and prior.
# Version 0.7 between 1.0 support this format on read only and will write # Version 0.7 between 1.0 support this format on read only and will write
# out the correct format (so they can be used to convert older timelines). # out the correct format (so they can be used to convert older timelines).
var time: TimeInfo var time: DateTime
try: time = parse(markJson["time"].getStr(), ISO_TIME_FORMAT) try: time = parse(markJson["time"].getStr(), ISO_TIME_FORMAT)
except: time = parse(markJson["time"].getStr(), "yyyy:MM:dd'T'HH:mm:ss") except: time = parse(markJson["time"].getStr(), "yyyy:MM:dd'T'HH:mm:ss")
@ -121,7 +121,11 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
## Write a nicely-formatted list of Marks to stdout. ## Write a nicely-formatted list of Marks to stdout.
let marks = timeline.marks let marks = timeline.marks
let now = getLocalTime(getTime()) let now = getTime().local
if indices.len == 0:
writeLine(stdout, "No marks match the given criteria.")
return
var idxs = indices.sorted( var idxs = indices.sorted(
proc(a, b: int): int = cmp(marks[a].time, marks[b].time)) proc(a, b: int): int = cmp(marks[a].time, marks[b].time))
@ -154,6 +158,9 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
if prefix.len > longestPrefix: longestPrefix = prefix.len if prefix.len > longestPrefix: longestPrefix = prefix.len
let colWidth = 80
let notesPrefixLen = 4
for w in toWrite: for w in toWrite:
if w.mark.summary == STOP_MSG: continue if w.mark.summary == STOP_MSG: continue
@ -174,7 +181,11 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
writeLine(stdout, "") writeLine(stdout, "")
if includeNotes and len(w.mark.notes.strip) > 0: if includeNotes and len(w.mark.notes.strip) > 0:
writeLine(stdout, spaces(longestPrefix) & w.mark.notes) writeLine(stdout, "")
let wrappedNotes = wordWrap(s = w.mark.notes,
maxLineWidth = colWidth)
for line in splitLines(wrappedNotes):
writeLine(stdout, spaces(notesPrefixLen) & line)
writeLine(stdout, "") writeLine(stdout, "")
proc formatMark(mark: Mark, nextMark = NO_MARK, timeFormat = ISO_TIME_FORMAT, includeNotes = false): string = proc formatMark(mark: Mark, nextMark = NO_MARK, timeFormat = ISO_TIME_FORMAT, includeNotes = false): string =
@ -183,20 +194,19 @@ proc formatMark(mark: Mark, nextMark = NO_MARK, timeFormat = ISO_TIME_FORMAT, in
## the Mark's notes in the output. ## the Mark's notes in the output.
let nextTime = let nextTime =
if nextMark == NO_MARK: getLocalTime(getTime()) if nextMark == NO_MARK: getTime().local
else: nextMark.time else: nextMark.time
let duration = (nextTime - mark.time).flexFormat let duration = (nextTime - mark.time).flexFormat
# TODO: pick up here calculating the time between marks # TODO: pick up here calculating the time between marks
let prefix = ($mark.id)[0..<8] & " " & mark.time.format(timeFormat) & " (" & duration & ") -- " let prefix = ($mark.id)[0..<8] & " " & mark.time.format(timeFormat) & " (" & duration & ") -- "
let prefixLen = len(($mark.id)[0..<8] & " " & mark.time.format(timeFormat) & " (" & duration & ") -- ")
result = prefix & mark.summary result = prefix & mark.summary
if includeNotes and len(mark.notes.strip()) > 0: if includeNotes and len(mark.notes.strip()) > 0:
let wrappedNotes = wordWrap(s = mark.notes, maxLineWidth = 80 - prefixLen) let wrappedNotes = wordWrap(s = mark.notes, maxLineWidth = 80 - prefix.len)
for line in splitLines(wrappedNotes): for line in splitLines(wrappedNotes):
result &= "\x0D\x0A" & spaces(prefixLen) & line result &= "\x0D\x0A" & spaces(prefix.len) & line
result &= "\x0D\x0A" result &= "\x0D\x0A"
proc findById(marks: seq[Mark], id: string): int = proc findById(marks: seq[Mark], id: string): int =
@ -277,69 +287,88 @@ proc filterMarkIndices(timeline: Timeline, args: Table[string, Value]): seq[int]
## arguments. ## arguments.
let marks = timeline.marks let marks = timeline.marks
result = sequtils.toSeq(0..<marks.len).filterIt(marks[it].summary != STOP_MSG) let now = getTime().local
let allIndices = sequtils.toSeq(0..<marks.len).filterIt(marks[it].summary != STOP_MSG).toSet
let union = args["--or"]
var selected =
if union: initSet[int]()
else: allIndices
template filterMarks(curSet: HashSet[int], pred: untyped): untyped =
var res: HashSet[int] = initSet[int]()
if union:
for mIdx {.inject.} in allIndices:
if pred: res.incl(mIdx)
res = res + curSet
else:
for mIdx {.inject.} in curSet:
if pred: res.incl(mIdx)
res
if args["<firstId>"]: if args["<firstId>"]:
let idx = marks.findById($args["<firstId>"]) let idx = marks.findById($args["<firstId>"])
if idx > 0: result = result.filterIt(it >= idx) if idx > 0: selected = selected.filterMarks(mIdx >= idx)
if args["<lastId>"]: if args["<lastId>"]:
let idx = marks.findById($args["<lastId>"]) let idx = marks.findById($args["<lastId>"])
if (idx > 0): result = result.filterIt(it <= idx) if (idx > 0): selected = selected.filterMarks(mIdx <= idx)
if args["--after"]: if args["--after"]:
var startTime: TimeInfo var startTime: DateTime
try: startTime = parseTime($args["--after"]) try: startTime = parseTime($args["--after"])
except: raise newException(ValueError, except: raise newException(ValueError,
"invalid value for --after: " & getCurrentExceptionMsg()) "invalid value for --after: " & getCurrentExceptionMsg())
result = result.filterIt(marks[it].time > startTime) selected = selected.filterMarks(marks[mIdx].time > startTime)
if args["--before"]: if args["--before"]:
var endTime: TimeInfo var endTime: DateTime
try: endTime = parseTime($args["--before"]) try: endTime = parseTime($args["--before"])
except: raise newException(ValueError, except: raise newException(ValueError,
"invalid value for --before: " & getCurrentExceptionMsg()) "invalid value for --before: " & getCurrentExceptionMsg())
result = result.filterIt(marks[it].time < endTime) selected = selected.filterMarks(marks[mIdx].time < endTime)
if args["--today"]: if args["--today"]:
let now = getLocalTime(getTime())
let b = now.startOfDay let b = now.startOfDay
let e = b + 1.days let e = b + 1.days
result = result.filterIt(marks[it].time >= b and marks[it].time < e) selected = selected.filterMarks(marks[mIdx].time >= b and marks[mIdx].time < e)
if args["--yesterday"]:
let e = now.startOfDay
let b = e - 1.days
selected = selected.filterMarks(marks[mIdx].time >= b and marks[mIdx].time < e)
if args["--this-week"]: if args["--this-week"]:
let now = getLocalTime(getTime())
let b = now.startOfWeek(dSun) let b = now.startOfWeek(dSun)
let e = b + 7.days let e = b + 7.days
result = result.filterIt(marks[it].time >= b and marks[it].time < e) selected = selected.filterMarks(marks[mIdx].time >= b and marks[mIdx].time < e)
if args["--last-week"]: if args["--last-week"]:
let now = getLocalTime(getTime())
let e = now.startOfWeek(dSun) let e = now.startOfWeek(dSun)
let b = e - 7.days let b = e - 7.days
result = result.filterIt(marks[it].time >= b and marks[it].time < e) selected = selected.filterMarks(marks[mIdx].time >= b and marks[mIdx].time < e)
if args["--tags"]: if args["--tags"]:
let tags = (args["--tags"] ?: "").split({',', ';'}) let tags = (args["--tags"] ?: "").split({',', ';'})
result = result.filter(proc (i: int): bool = selected = selected.filterMarks(tags.allIt(marks[mIdx].tags.contains(it)))
tags.allIt(marks[i].tags.contains(it)))
if args["--remove-tags"]: if args["--remove-tags"]:
let tags = (args["--remove-tags"] ?: "").split({',', ';'}) let tags = (args["--remove-tags"] ?: "").split({',', ';'})
result = result.filter(proc (i: int): bool = selected = selected.filterMarks(not tags.allIt(marks[mIdx].tags.contains(it)))
not tags.allIt(marks[i].tags.contains(it)))
if args["--matching"]: if args["--matching"]:
let pattern = re(args["--matching"] ?: "") let pattern = re("(?i)" & $(args["--matching"] ?: ""))
result = result.filterIt(marks[it].summary.find(pattern).isSome) selected = selected.filterMarks(marks[mIdx].summary.find(pattern).isSome)
return sequtils.toSeq(selected.items).sorted(system.cmp)
when isMainModule: when isMainModule:
try: try:
let doc = """ let doc = """
Usage: Usage:
ptk init [options] ptk init [options]
ptk add [options] ptk (add | start) [options]
ptk add [options] <summary> ptk (add | start) [options] <summary>
ptk resume [options] [<id>] ptk resume [options] [<id>]
ptk amend [options] [<id>] [<summary>] ptk amend [options] [<id>] [<summary>]
ptk merge <timeline> [<timeline>...] ptk merge <timeline> [<timeline>...]
@ -347,6 +376,8 @@ Usage:
ptk continue ptk continue
ptk delete <id> ptk delete <id>
ptk (list | ls) [options] ptk (list | ls) [options]
ptk (list | ls) tags
ptk current
ptk sum-time --ids <ids>... ptk sum-time --ids <ids>...
ptk sum-time [options] [<firstId>] [<lastId>] ptk sum-time [options] [<firstId>] [<lastId>]
ptk (-V | --version) ptk (-V | --version)
@ -368,18 +399,21 @@ Options:
-n --notes <notes> For add and amend, set the notes for a time mark. -n --notes <notes> For add and amend, set the notes for a time mark.
-t --time <time> For add and amend, use this time instead of the current time. -t --time <time> For add and amend, use this time instead of the current time.
-T --today Restrict the selection to marks during today. -T --today Restrict the selection to marks during today.
-Y --yesterday Restrict the selection to marks during yesterday.
-w --this-week Restrict the selection to marks during this week. -w --this-week Restrict the selection to marks during this week.
-W --last-week Restrict the selection to marks during the last week. -W --last-week Restrict the selection to marks during the last week.
-O --or Create a union from the time conditionals, not an intersection
(e.g. --today --or --yesterday)
-v --verbose Include notes in timeline entry output. -v --verbose Include notes in timeline entry output.
""" """
# TODO: add ptk delete [options] # TODO: add ptk delete [options]
logging.addHandler(newConsoleLogger()) logging.addHandler(newConsoleLogger())
let now = getLocalTime(getTime()) let now = getTime().local
# Parse arguments # Parse arguments
let args = docopt(doc, version = "ptk 0.8.0") let args = docopt(doc, version = "ptk 0.12.1")
if args["--echo-args"]: echo $args if args["--echo-args"]: echo $args
@ -464,7 +498,7 @@ Options:
time: if args["--time"]: parseTime($args["--time"]) else: now, time: if args["--time"]: parseTime($args["--time"]) else: now,
summary: STOP_MSG, summary: STOP_MSG,
notes: args["--notes"] ?: "", notes: args["--notes"] ?: "",
tags: (args["--tags"] ?: "").split({',', ';'})) tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isNilOrWhitespace))
timeline.marks.add(newMark) timeline.marks.add(newMark)
@ -499,21 +533,22 @@ Options:
saveTimeline(timeline, timelineLocation) saveTimeline(timeline, timelineLocation)
if args["add"]: if args["add"] or args["start"]:
var newMark: Mark = ( var newMark: Mark = (
id: genUUID(), id: genUUID(),
time: if args["--time"]: parseTime($args["--time"]) else: now, time: if args["--time"]: parseTime($args["--time"]) else: now,
summary: args["<summary>"] ?: "", summary: args["<summary>"] ?: "",
notes: args["--notes"] ?: "", notes: args["--notes"] ?: "",
tags: (args["--tags"] ?: "").split({',', ';'})) tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isNilOrWhitespace))
if args["--edit"]: edit(newMark) if args["--edit"]: edit(newMark)
let prevLastIdx = timeline.marks.getLastIndex() let prevLastIdx = timeline.marks.getLastIndex()
timeline.marks.add(newMark) timeline.marks.add(newMark)
timeline.writeMarks( timeline.writeMarks(
indices = @[prevLastIdx, timeline.marks.len - 1], indices = if prevLastIdx < 0: @[0]
else: @[prevLastIdx, timeline.marks.len - 1],
includeNotes = args["--verbose"]) includeNotes = args["--verbose"])
saveTimeline(timeline, timelineLocation) saveTimeline(timeline, timelineLocation)
@ -525,7 +560,9 @@ Options:
if args["<id>"]: if args["<id>"]:
markToResumeIdx = timeline.marks.findById($args["<id>"]) markToResumeIdx = timeline.marks.findById($args["<id>"])
if markToResumeIdx == -1: exitErr "Cannot find a mark matching " & $args["<id>"] if markToResumeIdx == -1: exitErr "Cannot find a mark matching " & $args["<id>"]
else: markToResumeIdx = timeline.marks.getLastIndex() else:
markToResumeIdx = timeline.marks.getLastIndex()
if markToResumeIdx < 0: exitErr "No mark to resume."
var markToResume = timeline.marks[markToResumeIdx] var markToResume = timeline.marks[markToResumeIdx]
var newMark: Mark = ( var newMark: Mark = (
@ -552,7 +589,9 @@ Options:
if args["<id>"]: if args["<id>"]:
markIdx = timeline.marks.findById($args["<id>"]) markIdx = timeline.marks.findById($args["<id>"])
if markIdx == -1: exitErr "Cannot find a mark matching " & $args["<id>"] if markIdx == -1: exitErr "Cannot find a mark matching " & $args["<id>"]
else: markIdx = timeline.marks.getLastIndex() else:
markIdx = timeline.marks.getLastIndex()
if markIdx < 0: exitErr "No mark to amend."
var mark = timeline.marks[markIdx] var mark = timeline.marks[markIdx]
@ -590,11 +629,27 @@ Options:
if args["list"] or args["ls"]: if args["list"] or args["ls"]:
var selectedIndices = timeline.filterMarkIndices(args) if args["tags"]:
timeline.writeMarks( echo $(timeline.marks.mapIt(it.tags)
indices = selectedIndices, .flatten().deduplicate().sorted(system.cmp).join("\n"))
includeNotes = args["--version"])
else:
var selectedIndices = timeline.filterMarkIndices(args)
timeline.writeMarks(
indices = selectedIndices,
includeNotes = args["--verbose"])
if args["current"]:
let idx = timeline.marks.len - 1
if timeline.marks[idx].summary == STOP_MSG:
echo "ptk: no current task"
else:
timeline.writeMarks(
indices = @[idx],
includeNotes = true)
if args["sum-time"]: if args["sum-time"]:

View File

@ -1,6 +1,6 @@
# Package # Package
version = "0.8.0" version = "0.12.1"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Personal Time Keeper" description = "Personal Time Keeper"
license = "MIT" license = "MIT"
@ -8,5 +8,5 @@ bin = @["ptk"]
# Dependencies # Dependencies
requires @["nim >= 0.15.0", "docopt >= 0.6.4", "uuids", "langutils", "tempfile", "timeutils >= 0.2.0"] requires @["nim >= 0.18.0", "docopt >= 0.6.4", "uuids", "langutils", "tempfile", "timeutils >= 0.2.2", "isaac >= 0.1.2"]

View File

@ -1,3 +0,0 @@
template first*(a: openarray): auto = a[0]
template last*(a: openarray): auto = a[len(a)-1]