Added stop, continue, and sum-time.
This commit is contained in:
parent
a1d2fa383a
commit
a1d43490cf
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
nimcache/
|
||||
ptk
|
||||
*.sw?
|
||||
|
254
ptk.nim
254
ptk.nim
@ -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))
|
||||
|
||||
#for i in 0..<marks.high-1:
|
||||
# echo formatMark(
|
||||
for mark in marks:
|
||||
echo formatMark(
|
||||
mark = mark,
|
||||
timeFormat = "HH:mm",
|
||||
includeNotes = args["--verbose"])
|
||||
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
|
||||
|
||||
except:
|
||||
fatal "ptk: " & getCurrentExceptionMsg()
|
||||
quit(QuitFailure)
|
||||
|
@ -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
3
ptkutil.nim
Normal file
@ -0,0 +1,3 @@
|
||||
template first*(s: seq): auto = s[0]
|
||||
|
||||
template last*(s: seq): auto = s[len(s)-1]
|
Loading…
x
Reference in New Issue
Block a user