## Personal Time Keeper ## ==================== ## ## Simple time keeping CLI import algorithm, docopt, json, langutils, logging, os, sequtils, strutils, tempfile, times, uuids type Mark* = tuple[id: UUID, time: TimeInfo, summary: string, notes: string] Timeline* = tuple[name: string, marks: seq[Mark]] let NO_MARK: Mark = ( id: parseUUID("00000000-0000-0000-0000-000000000000"), time: getLocalTime(getTime()), summary: "", notes: "") const ISO_TIME_FORMAT = "yyyy:MM:dd'T'HH:mm:ss" const TIME_FORMATS = @[ "HH:mm", "HH:mm:ss", "HH:mm:ss", "yyyy:MM:dd'T'HH:mm:ss", "yyyy:MM:dd'T'HH:mm"] #proc `$`*(mark: Mark): string = #return (($mark.uuid)[ 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) except: discard nil raise newException(Exception, "unable to interpret as a date: " & timeStr) template `%`(mark: Mark): JsonNode = %* { "id": $(mark.id), "time": mark.time.format(ISO_TIME_FORMAT), "summary": mark.summary, "notes": mark.notes } template `%`(timeline: Timeline): JsonNode = %* { "name": timeline.name, "marks": timeline.marks } proc loadTimeline(filename: string): Timeline = var timelineJson: JsonNode try: timelineJson = parseFile(filename) except: raise newException(ValueError, "unable to parse the timeline file as JSON: " & filename) var timeline: Timeline = (name: $timelineJson["name"], marks: @[]) for markJson in timelineJson["marks"]: timeline.marks.add(( id: parseUUID(markJson["id"].getStr()), time: parse(markJson["time"].getStr(), ISO_TIME_FORMAT), summary: markJson["summary"].getStr(), notes: markJson["notes"].getStr())) return timeline proc saveTimeline(timeline: Timeline, location: string): void = var timelineFile: File try: timelineFile = open(location, fmWrite) timelineFile.writeLine(pretty(%timeline)) except: raise newException(IOError, "unable to save changes to " & location) finally: close(timelineFile) 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 # TODO: pick up here calculating the time between marks let prefix = ($mark.id)[0..<8] & " " & mark.time.format(timeFormat) & " " result = prefix & mark.summary if includeNotes and len(mark.notes.strip()) > 0: let wrappedNotes = wordWrap(s = mark.notes, maxLineWidth = 80 - len(prefix)) for line in splitLines(wrappedNotes): result &= "\x0D\x0A" & spaces(len(prefix)) & line result &= "\x0D\x0A" proc findMarkById(timeline: Timeline, id: string): auto = var idx = 0 for mark in timeline.marks: if startsWith($mark.id, id): return (mark, idx) inc(idx) return (NO_MARK, -1) proc doInit(timelineLocation: string): void = stdout.write "Time log name [New Timeline]: " let name = stdin.readLine() let timeline = %* { "name": if name.len > 0: name else: "New Timeline", "marks": [] } #"createdAt": getLocalTime().format("yyyy-MM-dd'T'HH:mm:ss") } var timelineFile: File try: timelineFile = open(timelineLocation, fmWrite) timelineFile.write($timeline.pretty) finally: close(timelineFile) proc edit(mark: var Mark): void = var tempFile: File tempFileName: string try: (tempFile, tempFileName) = mkstemp("timestamp-mark-", ".txt", "", fmWrite) tempFile.writeLine( """# Edit the time, mark, and notes below. Any lines starting with '#' will be # ignored. When done, save the file and close the editor.""") tempFile.writeLine(mark.time.format(ISO_TIME_FORMAT)) tempFile.writeLine(mark.summary) tempFile.writeLine( """# Everything from the line below to the end of the file will be considered # notes for this timeline mark.""") close(tempFile) discard os.execShellCmd "$EDITOR " & tempFileName & " /dev/tty" var readTime = false readSummary = false for line in lines tempFileName: if strip(line)[0] == '#': continue elif not readTime: mark.time = parseTime(line); readTime = true elif not readSummary: mark.summary = line; readSummary = true else: mark.notes &= line finally: close(tempFile) when isMainModule: try: let doc = """ Usage: ptk init [options] ptk add [options] ptk add [options] ptk list [options] [] [] ptk ammend [options] [] ptk delete ptk (-V | --version) ptk (-h | --help) Options: -f --file Use the given time keeper file. -c --config Use as configuration for the CLI. -t --time