8 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
2 changed files with 90 additions and 42 deletions

128
ptk.nim
View File

@ -9,7 +9,7 @@ import algorithm, docopt, json, langutils, logging, os, nre, sequtils,
import private/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>...]
@ -348,6 +377,7 @@ Usage:
ptk delete <id> ptk delete <id>
ptk (list | ls) [options] ptk (list | ls) [options]
ptk (list | ls) tags 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)
@ -369,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.9.0") let args = docopt(doc, version = "ptk 0.12.1")
if args["--echo-args"]: echo $args if args["--echo-args"]: echo $args
@ -465,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)
@ -500,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)
@ -526,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 = (
@ -553,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]
@ -601,7 +639,17 @@ Options:
timeline.writeMarks( timeline.writeMarks(
indices = selectedIndices, indices = selectedIndices,
includeNotes = args["--version"]) 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.9.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"]