|
|
|
@ -4,27 +4,34 @@
|
|
|
|
|
## Simple time keeping CLI
|
|
|
|
|
|
|
|
|
|
import algorithm, docopt, json, langutils, logging, os, nre, sequtils,
|
|
|
|
|
strutils, tempfile, terminal, times, timeutils, uuids
|
|
|
|
|
sets, strutils, tempfile, terminal, times, timeutils, uuids
|
|
|
|
|
|
|
|
|
|
import ptkutil
|
|
|
|
|
import private/ptkutil
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
Timeline* = tuple[name: string, marks: seq[Mark]]
|
|
|
|
|
## Representation of a timeline: a name and sequence of Marks.
|
|
|
|
|
|
|
|
|
|
const STOP_MSG = "STOP"
|
|
|
|
|
|
|
|
|
|
let NO_MARK: Mark = (
|
|
|
|
|
id: parseUUID("00000000-0000-0000-0000-000000000000"),
|
|
|
|
|
time: fromSeconds(0).getLocalTime,
|
|
|
|
|
time: fromUnix(0).local,
|
|
|
|
|
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"
|
|
|
|
|
## The canonical time format used by PTK.
|
|
|
|
|
|
|
|
|
|
const TIME_FORMATS = @[
|
|
|
|
|
"H:mm", "HH:mm", "H:mm:ss", "HH:mm:ss",
|
|
|
|
|
"yyyy:MM:dd'T'HH:mm:ss", "yyyy:MM:dd'T'HH:mm",
|
|
|
|
|
"yyyy:MM:dd HH:mm:ss", "yyyy:MM:dd HH:mm"]
|
|
|
|
|
"yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd HH:mm:ss",
|
|
|
|
|
"yyyy-MM-dd'T'HH:mm", "yyyy-MM-dd HH:mm",
|
|
|
|
|
"MM-dd'T'HH:mm:ss", "MM-dd HH:mm:ss",
|
|
|
|
|
"MM-dd'T'HH:mm", "MM-dd HH:mm",
|
|
|
|
|
"HH:mm:ss", "H:mm:ss", "H:mm", "HH:mm" ]
|
|
|
|
|
## Other time formats that PTK will accept as input.
|
|
|
|
|
|
|
|
|
|
#proc `$`*(mark: Mark): string =
|
|
|
|
|
#return (($mark.uuid)[
|
|
|
|
@ -33,19 +40,14 @@ proc exitErr(msg: string): void =
|
|
|
|
|
fatal "ptk: " & msg
|
|
|
|
|
quit(QuitFailure)
|
|
|
|
|
|
|
|
|
|
proc parseTime(timeStr: string): TimeInfo =
|
|
|
|
|
proc parseTime(timeStr: string): DateTime =
|
|
|
|
|
## Helper to parse time strings trying multiple known formats.
|
|
|
|
|
for fmt in TIME_FORMATS:
|
|
|
|
|
try: return parse(timeStr, fmt)
|
|
|
|
|
except: discard nil
|
|
|
|
|
|
|
|
|
|
raise newException(Exception, "unable to interpret as a date: " & timeStr)
|
|
|
|
|
|
|
|
|
|
proc startOfDay(ti: TimeInfo): TimeInfo =
|
|
|
|
|
result = ti
|
|
|
|
|
result.hour = 0
|
|
|
|
|
result.minute = 0
|
|
|
|
|
result.second = 0
|
|
|
|
|
|
|
|
|
|
template `%`(mark: Mark): JsonNode =
|
|
|
|
|
%* {
|
|
|
|
|
"id": $(mark.id),
|
|
|
|
@ -59,6 +61,10 @@ template `%`(timeline: Timeline): JsonNode =
|
|
|
|
|
%* { "name": timeline.name, "marks": timeline.marks }
|
|
|
|
|
|
|
|
|
|
proc loadTimeline(filename: string): Timeline =
|
|
|
|
|
## Load a timeline from a file. Expects a path to a file (can be relative or
|
|
|
|
|
## absolute) and returns a Timeline. The marks in the timeline are guaranteed
|
|
|
|
|
## to be ordered by time.
|
|
|
|
|
|
|
|
|
|
var timelineJson: JsonNode
|
|
|
|
|
try: timelineJson = parseFile(filename)
|
|
|
|
|
except:
|
|
|
|
@ -68,9 +74,17 @@ proc loadTimeline(filename: string): Timeline =
|
|
|
|
|
var timeline: Timeline = (name: timelineJson["name"].getStr(), marks: @[])
|
|
|
|
|
|
|
|
|
|
for markJson in timelineJson["marks"]:
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
# out the correct format (so they can be used to convert older timelines).
|
|
|
|
|
var time: DateTime
|
|
|
|
|
try: time = parse(markJson["time"].getStr(), ISO_TIME_FORMAT)
|
|
|
|
|
except: time = parse(markJson["time"].getStr(), "yyyy:MM:dd'T'HH:mm:ss")
|
|
|
|
|
|
|
|
|
|
timeline.marks.add((
|
|
|
|
|
id: parseUUID(markJson["id"].getStr()),
|
|
|
|
|
time: parse(markJson["time"].getStr(), ISO_TIME_FORMAT),
|
|
|
|
|
time: time, #parse(markJson["time"].getStr(), ISO_TIME_FORMAT),
|
|
|
|
|
summary: markJson["summary"].getStr(),
|
|
|
|
|
notes: markJson["notes"].getStr(),
|
|
|
|
|
tags: markJson["tags"].getElems(@[]).map(proc (t: JsonNode): string = t.getStr())))
|
|
|
|
@ -81,6 +95,8 @@ proc loadTimeline(filename: string): Timeline =
|
|
|
|
|
return timeline
|
|
|
|
|
|
|
|
|
|
proc saveTimeline(timeline: Timeline, location: string): void =
|
|
|
|
|
## Write the timeline to disk at the file location given.
|
|
|
|
|
|
|
|
|
|
var timelineFile: File
|
|
|
|
|
try:
|
|
|
|
|
timelineFile = open(location, fmWrite)
|
|
|
|
@ -89,6 +105,8 @@ proc saveTimeline(timeline: Timeline, location: string): void =
|
|
|
|
|
finally: close(timelineFile)
|
|
|
|
|
|
|
|
|
|
proc flexFormat(i: TimeInterval): string =
|
|
|
|
|
## Pretty-format a time interval.
|
|
|
|
|
|
|
|
|
|
let fmt =
|
|
|
|
|
if i > 1.days: "d'd' H'h' m'm'"
|
|
|
|
|
elif i >= 1.hours: "H'h' m'm'"
|
|
|
|
@ -100,8 +118,14 @@ proc flexFormat(i: TimeInterval): string =
|
|
|
|
|
type WriteData = tuple[idx: int, mark: Mark, prefixLen: int, interval: TimeInterval]
|
|
|
|
|
|
|
|
|
|
proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): void =
|
|
|
|
|
## Write a nicely-formatted list of Marks to stdout.
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
|
|
proc(a, b: int): int = cmp(marks[a].time, marks[b].time))
|
|
|
|
@ -134,6 +158,9 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
|
|
|
|
|
|
|
|
|
|
if prefix.len > longestPrefix: longestPrefix = prefix.len
|
|
|
|
|
|
|
|
|
|
let colWidth = 80
|
|
|
|
|
let notesPrefixLen = 4
|
|
|
|
|
|
|
|
|
|
for w in toWrite:
|
|
|
|
|
if w.mark.summary == STOP_MSG: continue
|
|
|
|
|
|
|
|
|
@ -154,26 +181,32 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
|
|
|
|
|
writeLine(stdout, "")
|
|
|
|
|
|
|
|
|
|
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, "")
|
|
|
|
|
|
|
|
|
|
proc formatMark(mark: Mark, nextMark = NO_MARK, timeFormat = ISO_TIME_FORMAT, includeNotes = false): string =
|
|
|
|
|
## Pretty-format a Mark, optionally taking the next Mark in the timeline (to
|
|
|
|
|
## compute duration) and a time format string, and conditionally including
|
|
|
|
|
## the Mark's notes in the output.
|
|
|
|
|
|
|
|
|
|
let nextTime =
|
|
|
|
|
if nextMark == NO_MARK: getLocalTime(getTime())
|
|
|
|
|
if nextMark == NO_MARK: getTime().local
|
|
|
|
|
else: nextMark.time
|
|
|
|
|
|
|
|
|
|
let duration = (nextTime - mark.time).flexFormat
|
|
|
|
|
# TODO: pick up here calculating the time between marks
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
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):
|
|
|
|
|
result &= "\x0D\x0A" & spaces(prefixLen) & line
|
|
|
|
|
result &= "\x0D\x0A" & spaces(prefix.len) & line
|
|
|
|
|
result &= "\x0D\x0A"
|
|
|
|
|
|
|
|
|
|
proc findById(marks: seq[Mark], id: string): int =
|
|
|
|
@ -184,7 +217,17 @@ proc findById(marks: seq[Mark], id: string): int =
|
|
|
|
|
|
|
|
|
|
return -1
|
|
|
|
|
|
|
|
|
|
proc getLastIndex(marks: seq[Mark]): int =
|
|
|
|
|
## Find and return the index of the last Mark that was not a STOP mark.
|
|
|
|
|
## Returns -1 if there is no such last mark.
|
|
|
|
|
|
|
|
|
|
var idx = marks.len - 1
|
|
|
|
|
while idx >= 0 and marks[idx].summary == STOP_MSG: idx -= 1
|
|
|
|
|
if idx < 0: result = -1
|
|
|
|
|
else: result = idx
|
|
|
|
|
|
|
|
|
|
proc doInit(timelineLocation: string): void =
|
|
|
|
|
## Interactively initialize a new timeline at the given file path.
|
|
|
|
|
|
|
|
|
|
stdout.write "Time log name [New Timeline]: "
|
|
|
|
|
let name = stdin.readLine()
|
|
|
|
@ -203,6 +246,9 @@ proc doInit(timelineLocation: string): void =
|
|
|
|
|
type ExpectedMarkPart = enum Time, Summary, Tags, Notes
|
|
|
|
|
|
|
|
|
|
proc edit(mark: var Mark): void =
|
|
|
|
|
## Interactively edit a mark using the editor named in the environment
|
|
|
|
|
## variable "EDITOR"
|
|
|
|
|
|
|
|
|
|
var
|
|
|
|
|
tempFile: File
|
|
|
|
|
tempFileName: string
|
|
|
|
@ -237,63 +283,101 @@ proc edit(mark: var Mark): void =
|
|
|
|
|
finally: close(tempFile)
|
|
|
|
|
|
|
|
|
|
proc filterMarkIndices(timeline: Timeline, args: Table[string, Value]): seq[int] =
|
|
|
|
|
## Filter down a set of marks according to options provided in command line
|
|
|
|
|
## arguments.
|
|
|
|
|
|
|
|
|
|
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>"]:
|
|
|
|
|
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>"]:
|
|
|
|
|
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"]:
|
|
|
|
|
var startTime: TimeInfo
|
|
|
|
|
var startTime: DateTime
|
|
|
|
|
try: startTime = parseTime($args["--after"])
|
|
|
|
|
except: raise newException(ValueError,
|
|
|
|
|
"invalid value for --after: " & getCurrentExceptionMsg())
|
|
|
|
|
result = result.filterIt(marks[it].time > startTime)
|
|
|
|
|
selected = selected.filterMarks(marks[mIdx].time > startTime)
|
|
|
|
|
|
|
|
|
|
if args["--before"]:
|
|
|
|
|
var endTime: TimeInfo
|
|
|
|
|
var endTime: DateTime
|
|
|
|
|
try: endTime = parseTime($args["--before"])
|
|
|
|
|
except: raise newException(ValueError,
|
|
|
|
|
"invalid value for --before: " & getCurrentExceptionMsg())
|
|
|
|
|
result = result.filterIt(marks[it].time < endTime)
|
|
|
|
|
selected = selected.filterMarks(marks[mIdx].time < endTime)
|
|
|
|
|
|
|
|
|
|
if args["--today"]:
|
|
|
|
|
let now = getLocalTime(getTime())
|
|
|
|
|
let b = now.startOfDay
|
|
|
|
|
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"]:
|
|
|
|
|
let b = now.startOfWeek(dSun)
|
|
|
|
|
let e = b + 7.days
|
|
|
|
|
selected = selected.filterMarks(marks[mIdx].time >= b and marks[mIdx].time < e)
|
|
|
|
|
|
|
|
|
|
if args["--last-week"]:
|
|
|
|
|
let e = now.startOfWeek(dSun)
|
|
|
|
|
let b = e - 7.days
|
|
|
|
|
selected = selected.filterMarks(marks[mIdx].time >= b and marks[mIdx].time < e)
|
|
|
|
|
|
|
|
|
|
if args["--tags"]:
|
|
|
|
|
let tags = (args["--tags"] ?: "").split({',', ';'})
|
|
|
|
|
result = result.filter(proc (i: int): bool =
|
|
|
|
|
tags.allIt(marks[i].tags.contains(it)))
|
|
|
|
|
selected = selected.filterMarks(tags.allIt(marks[mIdx].tags.contains(it)))
|
|
|
|
|
|
|
|
|
|
if args["--remove-tags"]:
|
|
|
|
|
let tags = (args["--remove-tags"] ?: "").split({',', ';'})
|
|
|
|
|
result = result.filter(proc (i: int): bool =
|
|
|
|
|
not tags.allIt(marks[i].tags.contains(it)))
|
|
|
|
|
selected = selected.filterMarks(not tags.allIt(marks[mIdx].tags.contains(it)))
|
|
|
|
|
|
|
|
|
|
if args["--matching"]:
|
|
|
|
|
let pattern = re(args["--matching"] ?: "")
|
|
|
|
|
result = result.filterIt(marks[it].summary.find(pattern).isSome)
|
|
|
|
|
let pattern = re("(?i)" & $(args["--matching"] ?: ""))
|
|
|
|
|
selected = selected.filterMarks(marks[mIdx].summary.find(pattern).isSome)
|
|
|
|
|
|
|
|
|
|
return sequtils.toSeq(selected.items).sorted(system.cmp)
|
|
|
|
|
|
|
|
|
|
when isMainModule:
|
|
|
|
|
try:
|
|
|
|
|
let doc = """
|
|
|
|
|
Usage:
|
|
|
|
|
ptk init [options]
|
|
|
|
|
ptk add [options]
|
|
|
|
|
ptk add [options] <summary>
|
|
|
|
|
ptk amend [options] <id> [<summary>]
|
|
|
|
|
ptk (add | start) [options]
|
|
|
|
|
ptk (add | start) [options] <summary>
|
|
|
|
|
ptk resume [options] [<id>]
|
|
|
|
|
ptk amend [options] [<id>] [<summary>]
|
|
|
|
|
ptk merge <timeline> [<timeline>...]
|
|
|
|
|
ptk stop [options]
|
|
|
|
|
ptk continue
|
|
|
|
|
ptk delete <id>
|
|
|
|
|
ptk (list | ls) [options]
|
|
|
|
|
ptk (list | ls) tags
|
|
|
|
|
ptk current
|
|
|
|
|
ptk sum-time --ids <ids>...
|
|
|
|
|
ptk sum-time [options] [<firstId>] [<lastId>]
|
|
|
|
|
ptk (-V | --version)
|
|
|
|
@ -314,17 +398,22 @@ Options:
|
|
|
|
|
-m --matching <pattern> Restric the selection to marks matching <pattern>.
|
|
|
|
|
-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 --today Restrict the seelction 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 --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.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# TODO: add ptk delete [options]
|
|
|
|
|
|
|
|
|
|
logging.addHandler(newConsoleLogger())
|
|
|
|
|
let now = getLocalTime(getTime())
|
|
|
|
|
let now = getTime().local
|
|
|
|
|
|
|
|
|
|
# Parse arguments
|
|
|
|
|
let args = docopt(doc, version = "ptk 0.4.1")
|
|
|
|
|
let args = docopt(doc, version = "ptk 0.12.1")
|
|
|
|
|
|
|
|
|
|
if args["--echo-args"]: echo $args
|
|
|
|
|
|
|
|
|
@ -370,6 +459,30 @@ Options:
|
|
|
|
|
if args["init"]:
|
|
|
|
|
doInit(foldl(timelineLocations, if len(a) > 0: a else: b))
|
|
|
|
|
|
|
|
|
|
elif args["merge"]:
|
|
|
|
|
|
|
|
|
|
let filesToMerge = args["<timeline>"]
|
|
|
|
|
let timelines = filesToMerge.mapIt(loadTimeline(it))
|
|
|
|
|
|
|
|
|
|
let names = timelines.mapIt(it.name).toSet
|
|
|
|
|
let mergedName = sequtils.toSeq(names.items).foldl(a & " + " & b)
|
|
|
|
|
var merged: Timeline = (
|
|
|
|
|
name: mergedName,
|
|
|
|
|
marks: @[])
|
|
|
|
|
|
|
|
|
|
for timeline in timelines:
|
|
|
|
|
for mark in timeline.marks:
|
|
|
|
|
var existingMarkIdx = merged.marks.findById($mark.id)
|
|
|
|
|
if existingMarkIdx >= 0:
|
|
|
|
|
if merged.marks[existingMarkIdx].summary != mark.summary:
|
|
|
|
|
merged.marks[existingMarkIdx].summary &= " | " & mark.summary
|
|
|
|
|
if merged.marks[existingMarkIdx].notes != mark.notes:
|
|
|
|
|
merged.marks[existingMarkIdx].notes &= "\r\n--------\r\b" & mark.notes
|
|
|
|
|
|
|
|
|
|
else: merged.marks.add(mark)
|
|
|
|
|
|
|
|
|
|
writeLine(stdout, pretty(%merged))
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
|
|
if not fileExists(timelineLocation):
|
|
|
|
@ -385,7 +498,7 @@ Options:
|
|
|
|
|
time: if args["--time"]: parseTime($args["--time"]) else: now,
|
|
|
|
|
summary: STOP_MSG,
|
|
|
|
|
notes: args["--notes"] ?: "",
|
|
|
|
|
tags: (args["--tags"] ?: "").split({',', ';'}))
|
|
|
|
|
tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isNilOrWhitespace))
|
|
|
|
|
|
|
|
|
|
timeline.marks.add(newMark)
|
|
|
|
|
|
|
|
|
@ -420,20 +533,50 @@ Options:
|
|
|
|
|
|
|
|
|
|
saveTimeline(timeline, timelineLocation)
|
|
|
|
|
|
|
|
|
|
if args["add"]:
|
|
|
|
|
if args["add"] or args["start"]:
|
|
|
|
|
|
|
|
|
|
var newMark: Mark = (
|
|
|
|
|
id: genUUID(),
|
|
|
|
|
time: if args["--time"]: parseTime($args["--time"]) else: now,
|
|
|
|
|
summary: args["<summary>"] ?: "",
|
|
|
|
|
notes: args["--notes"] ?: "",
|
|
|
|
|
tags: (args["--tags"] ?: "").split({',', ';'}))
|
|
|
|
|
tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isNilOrWhitespace))
|
|
|
|
|
|
|
|
|
|
if args["--edit"]: edit(newMark)
|
|
|
|
|
|
|
|
|
|
let prevLastIdx = timeline.marks.getLastIndex()
|
|
|
|
|
timeline.marks.add(newMark)
|
|
|
|
|
timeline.writeMarks(
|
|
|
|
|
indices = if prevLastIdx < 0: @[0]
|
|
|
|
|
else: @[prevLastIdx, timeline.marks.len - 1],
|
|
|
|
|
includeNotes = args["--verbose"])
|
|
|
|
|
|
|
|
|
|
saveTimeline(timeline, timelineLocation)
|
|
|
|
|
|
|
|
|
|
if args["resume"]:
|
|
|
|
|
|
|
|
|
|
var markToResumeIdx: int
|
|
|
|
|
|
|
|
|
|
if args["<id>"]:
|
|
|
|
|
markToResumeIdx = timeline.marks.findById($args["<id>"])
|
|
|
|
|
if markToResumeIdx == -1: exitErr "Cannot find a mark matching " & $args["<id>"]
|
|
|
|
|
else:
|
|
|
|
|
markToResumeIdx = timeline.marks.getLastIndex()
|
|
|
|
|
if markToResumeIdx < 0: exitErr "No mark to resume."
|
|
|
|
|
var markToResume = timeline.marks[markToResumeIdx]
|
|
|
|
|
|
|
|
|
|
var newMark: Mark = (
|
|
|
|
|
id: genUUID(),
|
|
|
|
|
time: if args["--time"]: parseTime($args["--time"]) else: now,
|
|
|
|
|
summary: markToResume.summary,
|
|
|
|
|
notes: markToResume.notes,
|
|
|
|
|
tags: markToResume.tags)
|
|
|
|
|
|
|
|
|
|
if args["--edit"]: edit(newMark)
|
|
|
|
|
|
|
|
|
|
timeline.marks.add(newMark)
|
|
|
|
|
timeline.writeMarks(
|
|
|
|
|
indices = @[timeline.marks.len - 1],
|
|
|
|
|
indices = sequtils.toSeq(markToResumeIdx..<timeline.marks.len),
|
|
|
|
|
includeNotes = args["--verbose"])
|
|
|
|
|
|
|
|
|
|
saveTimeline(timeline, timelineLocation)
|
|
|
|
@ -441,7 +584,15 @@ Options:
|
|
|
|
|
if args["amend"]:
|
|
|
|
|
|
|
|
|
|
# Note, this returns a copy, not a reference to the mark in the seq.
|
|
|
|
|
let markIdx = timeline.marks.findById($args["<id>"])
|
|
|
|
|
var markIdx: int
|
|
|
|
|
|
|
|
|
|
if args["<id>"]:
|
|
|
|
|
markIdx = timeline.marks.findById($args["<id>"])
|
|
|
|
|
if markIdx == -1: exitErr "Cannot find a mark matching " & $args["<id>"]
|
|
|
|
|
else:
|
|
|
|
|
markIdx = timeline.marks.getLastIndex()
|
|
|
|
|
if markIdx < 0: exitErr "No mark to amend."
|
|
|
|
|
|
|
|
|
|
var mark = timeline.marks[markIdx]
|
|
|
|
|
|
|
|
|
|
if args["<summary>"]: mark.summary = $args["<summary>"]
|
|
|
|
@ -478,11 +629,27 @@ Options:
|
|
|
|
|
|
|
|
|
|
if args["list"] or args["ls"]:
|
|
|
|
|
|
|
|
|
|
var selectedIndices = timeline.filterMarkIndices(args)
|
|
|
|
|
if args["tags"]:
|
|
|
|
|
|
|
|
|
|
timeline.writeMarks(
|
|
|
|
|
indices = selectedIndices,
|
|
|
|
|
includeNotes = args["--version"])
|
|
|
|
|
echo $(timeline.marks.mapIt(it.tags)
|
|
|
|
|
.flatten().deduplicate().sorted(system.cmp).join("\n"))
|
|
|
|
|
|
|
|
|
|
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"]:
|
|
|
|
|
|
|
|
|
|