Added stop, continue, and sum-time.

This commit is contained in:
Jonathan Bernard 2016-10-08 00:37:16 -05:00
parent a1d2fa383a
commit a1d43490cf
4 changed files with 208 additions and 52 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
nimcache/
ptk
*.sw?

254
ptk.nim
View File

@ -4,12 +4,15 @@
## Simple time keeping CLI
import algorithm, docopt, json, langutils, logging, os, sequtils, strutils,
tempfile, times, uuids
tempfile, terminal, times, timeutils, uuids
import ptkutil
type
Mark* = tuple[id: UUID, time: TimeInfo, summary: string, notes: string]
Timeline* = tuple[name: string, marks: seq[Mark]]
const STOP_MSG = "STOP"
let NO_MARK: Mark = (
id: parseUUID("00000000-0000-0000-0000-000000000000"),
time: getLocalTime(getTime()),
@ -28,7 +31,6 @@ proc exitErr(msg: string): void =
fatal "ptk: " & msg
quit(QuitFailure)
proc parseTime(timeStr: string): TimeInfo =
for fmt in TIME_FORMATS:
try: return parse(timeStr, fmt)
@ -73,25 +75,80 @@ proc saveTimeline(timeline: Timeline, location: string): void =
except: raise newException(IOError, "unable to save changes to " & location)
finally: close(timelineFile)
proc flexFormat(i: TimeInterval): string =
let fmt =
if i > 1.days: "d'd' H'h' m'm'"
elif i >= 1.hours: "H'h' m'm'"
elif i >= 1.minutes: "m'm' s's'"
else: "s's'"
return i.format(fmt)
proc writeMarks(marks: seq[Mark], includeNotes = false): void =
let now = getLocalTime(getTime())
let timeFormat =
if now - marks.first.time > 1.years: "yyyy-MM-dd HH:mm"
elif now - marks.first.time > 7.days: "MMM dd HH:mm"
elif now - marks.first.time > 1.days: "ddd HH:mm"
else: "HH:mm"
var intervals: seq[TimeInterval] = @[]
for i in 0..<marks.len - 1: intervals.add(marks[i+1].time - marks[i].time)
intervals.add(now - marks.last.time)
var prefixLens: seq[int] = @[]
var longestPrefix = 0
for i in 0..<marks.len:
let
mark = marks[i]
interval = intervals[i]
prefix = ($mark.id)[0..<8] & " " & mark.time.format(timeFormat) & " (" & interval.flexFormat & ")"
prefixLens.add(prefix.len)
if prefix.len > longestPrefix: longestPrefix = prefix.len
for i in 0..<marks.len:
let mark = marks[i]
if mark.summary == STOP_MSG: continue
let duration = intervals[i].flexFormat
setForegroundColor(stdout, fgBlack, true)
write(stdout, ($mark.id)[0..<8])
setForegroundColor(stdout, fgYellow)
write(stdout, " " & mark.time.format(timeFormat))
setForegroundColor(stdout, fgCyan)
write(stdout, " (" & duration & ")")
resetAttributes(stdout)
writeLine(stdout, spaces(longestPrefix - prefixLens[i]) & " -- " & mark.summary)
if includeNotes and len(mark.notes.strip) > 0:
writeLine(stdout, spaces(longestPrefix) & mark.notes)
writeLine(stdout, "")
proc formatMark(mark: Mark, nextMark = NO_MARK, timeFormat = ISO_TIME_FORMAT, includeNotes = false): string =
let nextTime =
if nextMark == NO_MARK: getLocalTime(getTime())
else: mark.time
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) & " "
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 - len(prefix))
let wrappedNotes = wordWrap(s = mark.notes, maxLineWidth = 80 - prefixLen)
for line in splitLines(wrappedNotes):
result &= "\x0D\x0A" & spaces(len(prefix)) & line
result &= "\x0D\x0A" & spaces(prefixLen) & line
result &= "\x0D\x0A"
proc findMarkById(timeline: Timeline, id: string): auto =
proc findById(marks: seq[Mark], id: string): auto =
var idx = 0
for mark in timeline.marks:
for mark in marks:
if startsWith($mark.id, id): return (mark, idx)
inc(idx)
@ -103,7 +160,7 @@ proc doInit(timelineLocation: string): void =
let name = stdin.readLine()
let timeline = %*
{ "name": if name.len > 0: name else: "New Timeline",
{ "name": if name.strip.len > 0: name.strip else: "New Timeline",
"marks": [] }
#"createdAt": getLocalTime().format("yyyy-MM-dd'T'HH:mm:ss") }
@ -152,29 +209,40 @@ Usage:
ptk init [options]
ptk add [options]
ptk add [options] <summary>
ptk list [options] [<start>] [<end>]
ptk ammend [options] <id> [<summary>]
ptk stop [options]
ptk continue
ptk delete <id>
ptk list [options]
ptk sum-time --ids <ids>...
ptk sum-time [options] [<firstId>] [<lastId>]
ptk (-V | --version)
ptk (-h | --help)
Options:
-f --file <file> Use the given time keeper file.
-f --file <file> Use the given timeline file.
-c --config <cfgFile> Use <cfgFile> as configuration for the CLI.
-t --time <time> For add and ammend, use this time instead of the current time.
-n --notes <notes> For add and ammend, set the notes for a time mark.
-V --version Print the tool's version information.
-e --edit Open the mark in an editor.
-a --after <after> Restrict the selection to marks after <after>.
-b --before <before> Restrict the selection to marks after <before>.
-h --help Print this usage information.
-v --verbose Include notes in timeline entry output.
-E --echo-args Echo the program's understanding of it's arguments.
"""
# TODO: add ptk delete [options]
logging.addHandler(newConsoleLogger())
# Parse arguments
let args = docopt(doc, version = "ptk 0.1.0")
if args["--echo-args"]: echo $args
if args["--help"]:
echo doc
quit()
@ -194,7 +262,7 @@ Options:
ptkrcFilename = $getEnv("HOME") & "/.ptkrc"
try:
cfgFile = open(ptkrcFilename, fmWrite)
cfgFile.write("{}")
cfgFile.write("{\"timelineLogFile\": \"timeline.log.json\"}")
except: warn "ptk: could not write default .ptkrc to " & ptkrcFilename
finally: close(cfgFile)
@ -223,43 +291,75 @@ Options:
raise newException(IOError,
"time log file doesn't exist: " & timelineLocation)
var timeline = parseFile(timelineLocation)
var timeline = loadTimeline(timelineLocation)
if args["stop"]:
let newMark = (
id: genUUID(),
time:
if args["--time"]: parseTime($args["--time"])
else: getLocalTime(getTime()),
summary: STOP_MSG,
notes: args["--notes"] ?: "")
timeline.marks.add(newMark)
writeMarks(
marks = timeline.marks[timeline.marks.len - 2..<timeline.marks.len],
includeNotes = args["--verbose"])
echo "stopped timer"
saveTimeline(timeline, timelineLocation)
if args["continue"]:
if timeline.marks.last.summary != STOP_MSG:
echo "There is already something in progress:"
writeMarks(
marks = @[timeline.marks.last],
includeNotes = args["--verbose"])
quit(0)
let prevMark = timeline.marks[timeline.marks.len - 2]
var newMark: Mark = (
id: genUUID(),
time:
if args["--time"]: parseTime($args["--time"])
else: getLocalTime(getTime()),
summary: prevMark.summary,
notes: prevMark.notes)
timeline.marks.add(newMark)
writeMarks(marks = @[newMark], includeNotes = args["--verbose"])
saveTimeline(timeline, timelineLocation)
if args["add"]:
var newMark: Mark = (
id: genUUID(),
time:
if args["--time"]: parseTime($args["<time>"])
if args["--time"]: parseTime($args["--time"])
else: getLocalTime(getTime()),
summary: args["<summary>"] ?: "",
notes: args["--notes"] ?: "")
if args["--edit"]: edit(newMark)
timeline["marks"].add(%newMark)
echo formatMark(
mark = newMark,
timeFormat = "HH:mm",
includeNotes = args["--verbose"])
timeline.marks.add(newMark)
writeMarks(marks = @[newMark], includeNotes = args["--verbose"])
var timelineFile: File
try:
timelineFile = open(timelineLocation, fmWrite)
timelineFile.writeLine(pretty(%timeline))
finally: close(timelineFile)
saveTimeline(timeline, timelineLocation)
if args["ammend"]:
var timeline = loadTimeline(timelineLocation)
# Note, this returns a copy, not a reference to the mark in the seq.
var (mark, markIdx) = timeline.findMarkById($args["<id>"])
var (mark, markIdx) = timeline.marks.findById($args["<id>"])
if args["<summary>"]: mark.summary = $args["<summary>"]
if args["--notes"]: mark.notes = $args["<notes>"]
if args["--time"]:
try: mark.time = parseTime($args["<time>"])
try: mark.time = parseTime($args["--time"])
except: raise newException(ValueError,
"invalid value for --time: " & getCurrentExceptionMsg())
@ -276,41 +376,93 @@ Options:
if args["delete"]:
var timeline = loadTimeline(timelineLocation)
var (mark, markIdx) = timeline.findMarkById($args["<id>"])
var (mark, markIdx) = timeline.marks.findById($args["<id>"])
timeline.marks.delete(markIdx)
saveTimeline(timeline, timelineLocation)
if args["list"]:
let timeline = loadTimeline(timelineLocation)
var marks = timeline.marks
if args["<start>"]:
var startTime: Time
try: startTime = parseTime($args["<start>"]).toTime
if args["--after"]:
var startTime: TimeInfo
try: startTime = parseTime($args["--after"])
except: raise newException(ValueError,
"invalid value for --start: " & getCurrentExceptionMsg())
marks = marks.filter(proc(m: Mark): bool = m.time.toTime > startTime)
"invalid value for --after: " & getCurrentExceptionMsg())
marks = marks.filter(proc(m: Mark): bool = m.time > startTime)
if args["<end>"]:
var endTime: Time
try: endTime = parseTime($args["<end>"]).toTime
if args["--before"]:
var endTime: TimeInfo
try: endTime = parseTime($args["--before"])
except: raise newException(ValueError,
"invalid value for --end: " & getCurrentExceptionMsg())
marks = marks.filter(proc(m: Mark): bool = m.time.toTime < endTime)
"invalid value for --before: " & getCurrentExceptionMsg())
marks = marks.filter(proc(m: Mark): bool = m.time < endTime)
marks = marks.sorted(proc(a, b: Mark): int = cast[int](a.time.toTime - b.time.toTime))
marks = marks.sorted(proc(a, b: Mark): int = cmp(a.time, b.time))
writeMarks(marks = marks, includeNotes = args["--version"])
if args["sum-time"]:
var intervals: seq[TimeInterval] = @[]
if args["--ids"]:
for id in args["<ids>"]:
let (mark, markIdx) = timeline.marks.findById(id)
if mark == NO_MARK:
warn "ptk: could not find mark for id " & id
elif markIdx == timeline.marks.len - 1:
intervals.add(getLocalTime(getTime()) - mark.time)
else:
intervals.add(timeline.marks[markIdx + 1].time - mark.time)
else:
var startIdx = 0
var endIdx = timeline.marks.len - 1
if args["<firstId>"]:
startIdx = max(timeline.marks.findById($args["<firstId>"])[1], 0)
if args["<lastId>"]:
let idx = timeline.marks.findById($args["<firstId>"])[1]
if (idx > 0): endIdx = idx
if args["--after"]:
var startTime: TimeInfo
try: startTime = parseTime($args["--after"])
except: raise newException(ValueError,
"invalid value for --after: " & getCurrentExceptionMsg())
let marks = timeline.marks.filter(proc(m: Mark): bool = m.time > startTime)
let (mark, idx) = timeline.marks.findById($marks.first.id)
if idx > startIdx: startIdx = idx
if args["--before"]:
var endTime: TimeInfo
try: endTime = parseTime($args["--before"])
except: raise newException(ValueError,
"invalid value for --after: " & getCurrentExceptionMsg())
let marks = timeline.marks.filter(proc(m: Mark): bool = m.time < endTime)
let (mark, idx) = timeline.marks.findById($marks.last.id)
if idx < endIdx: endIdx = idx
for idx in startIdx..<min(endIdx, timeline.marks.len - 1):
if timeline.marks[idx].summary == STOP_MSG: continue # don't count stops
intervals.add(timeline.marks[idx + 1].time - timeline.marks[idx].time)
if endIdx == timeline.marks.len - 1 and
timeline.marks.last.summary != STOP_MSG:
intervals.add(getLocalTime(getTime()) - timeline.marks.last.time)
if intervals.len == 0:
echo "ptk: no marks found"
else:
let total = foldl(intervals, a + b)
echo total.flexFormat
#for i in 0..<marks.high-1:
# echo formatMark(
for mark in marks:
echo formatMark(
mark = mark,
timeFormat = "HH:mm",
includeNotes = args["--verbose"])
except:
fatal "ptk: " & getCurrentExceptionMsg()
quit(QuitFailure)

View File

@ -8,5 +8,5 @@ bin = @["ptk"]
# Dependencies
requires @["nim >= 0.15.0", "docopt >= 0.6.4", "uuids", "langutils", "tempfile"]
requires @["nim >= 0.15.0", "docopt >= 0.6.4", "uuids", "langutils", "tempfile", "timeutils"]

3
ptkutil.nim Normal file
View File

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