7 Commits
1.0.2 ... 1.0.8

5 changed files with 68 additions and 38 deletions

View File

@ -2,7 +2,7 @@
## =================================== ## ===================================
import asyncdispatch, base64, bcrypt, cliutils, docopt, jester, json, logging, import asyncdispatch, base64, bcrypt, cliutils, docopt, jester, json, logging,
ospaths, sequtils, strutils, os, tables, times, uuids sequtils, strutils, os, tables, times, uuids
import nre except toSeq import nre except toSeq
@ -39,7 +39,7 @@ proc loadApiConfig*(json: JsonNode): PtkApiCfg =
template halt(code: HttpCode, template halt(code: HttpCode,
headers: RawHeaders, headers: RawHeaders,
content: string): typed = content: string) =
## Immediately replies with the specified request. This means any further ## Immediately replies with the specified request. This means any further
## code will not be executed after calling this template in the current ## code will not be executed after calling this template in the current
## route. ## route.
@ -55,7 +55,7 @@ template halt(code: HttpCode,
template checkAuth(cfg: PtkApiCfg) = template checkAuth(cfg: PtkApiCfg) =
## Check this request for authentication and authorization information. ## Check this request for authentication and authorization information.
## If the request is not authorized, this template immediately returns a ## If the request is not authorized, this template immediately returns a
## 401 Unauthotized response ## 401 Unauthotized response
var authed {.inject.} = false var authed {.inject.} = false
var user {.inject.}: PtkUser = PtkUser() var user {.inject.}: PtkUser = PtkUser()

View File

@ -9,6 +9,8 @@ type
Timeline* = tuple[name: string, marks: seq[Mark]] Timeline* = tuple[name: string, marks: seq[Mark]]
## Representation of a timeline: a name and sequence of Marks. ## Representation of a timeline: a name and sequence of Marks.
OffsetFrom = enum Year, Month, Day, None
const STOP_MSG* = "STOP" const STOP_MSG* = "STOP"
let NO_MARK*: Mark = ( let NO_MARK*: Mark = (
@ -20,11 +22,18 @@ const ISO_TIME_FORMAT* = "yyyy-MM-dd'T'HH:mm:ss"
## The canonical time format used by PTK. ## The canonical time format used by PTK.
const TIME_FORMATS* = @[ const TIME_FORMATS* = @[
"yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd HH:mm:ss", (fmtStr: "yyyy-MM-dd'T'HH:mm:ss", offsetFrom: OffsetFrom.None),
"yyyy-MM-dd'T'HH:mm", "yyyy-MM-dd HH:mm", (fmtStr: "yyyy-MM-dd HH:mm:ss", offsetFrom: OffsetFrom.None),
"MM-dd'T'HH:mm:ss", "MM-dd HH:mm:ss", (fmtStr: "yyyy-MM-dd'T'HH:mm", offsetFrom: OffsetFrom.None),
"MM-dd'T'HH:mm", "MM-dd HH:mm", (fmtStr: "yyyy-MM-dd HH:mm", offsetFrom: OffsetFrom.None),
"HH:mm:ss", "H:mm:ss", "H:mm", "HH:mm" ] (fmtStr: "MM-dd'T'HH:mm:ss", offsetFrom: OffsetFrom.Year),
(fmtStr: "MM-dd HH:mm:ss", offsetFrom: OffsetFrom.Year),
(fmtStr: "MM-dd'T'HH:mm", offsetFrom: OffsetFrom.Year),
(fmtStr: "MM-dd HH:mm", offsetFrom: OffsetFrom.Year),
(fmtStr: "HH:mm:ss", offsetFrom: OffsetFrom.Day),
(fmtStr: "H:mm:ss", offsetFrom: OffsetFrom.Day),
(fmtStr: "H:mm", offsetFrom: OffsetFrom.Day),
(fmtStr: "HH:mm", offsetFrom: OffsetFrom.Day) ]
## Other time formats that PTK will accept as input. ## Other time formats that PTK will accept as input.
proc getOrFail*(n: JsonNode, key: string, objName: string = ""): JsonNode = proc getOrFail*(n: JsonNode, key: string, objName: string = ""): JsonNode =
@ -40,7 +49,23 @@ proc getIfExists*(n: JsonNode, key: string): JsonNode =
proc parseTime*(timeStr: string): DateTime = 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:
let now = now()
let parsed = parse(timeStr, fmt.fmtStr)
case fmt.offsetFrom:
of OffsetFrom.None:
return parsed
of OffsetFrom.Year:
return initDateTime(parsed.monthday, parsed.month, now.year,
parsed.hour, parsed.minute, parsed.second, parsed.nanosecond,
now.timezone)
of OffsetFrom.Month:
return initDateTime(parsed.monthday, now.month, now.year,
parsed.hour, parsed.minute, parsed.second, parsed.nanosecond,
now.timezone)
of OffsetFrom.Day:
return initDateTime(now.monthday, now.month, now.year, parsed.hour,
parsed.minute, parsed.second, parsed.nanosecond, now.timezone)
except: discard nil except: discard nil
raise newException(Exception, "unable to interpret as a date: " & timeStr) raise newException(Exception, "unable to interpret as a date: " & timeStr)
@ -112,5 +137,3 @@ proc getLastIndex*(marks: seq[Mark]): int =
while idx >= 0 and marks[idx].summary == STOP_MSG: idx -= 1 while idx >= 0 and marks[idx].summary == STOP_MSG: idx -= 1
if idx < 0: result = -1 if idx < 0: result = -1
else: result = idx else: result = idx

View File

@ -1 +1 @@
const PTK_VERSION* = "1.0.2" const PTK_VERSION* = "1.0.8"

53
ptk.nim
View File

@ -3,10 +3,10 @@
## ##
## Simple time keeping CLI ## Simple time keeping CLI
import algorithm, docopt, json, langutils, logging, os, nre, sequtils, import algorithm, docopt, json, langutils, logging, os, nre, std/wordwrap,
sets, strutils, tempfile, terminal, times, uuids sequtils, sets, strutils, tempfile, terminal, times, uuids
import timeutils except `-`; import timeutils except `-`
import private/util import private/util
import private/api import private/api
@ -98,7 +98,7 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
if includeNotes and len(w.mark.notes.strip) > 0: if includeNotes and len(w.mark.notes.strip) > 0:
writeLine(stdout, "") writeLine(stdout, "")
let wrappedNotes = wordWrap(s = w.mark.notes, let wrappedNotes = wrapWords(s = w.mark.notes,
maxLineWidth = colWidth) maxLineWidth = colWidth)
for line in splitLines(wrappedNotes): for line in splitLines(wrappedNotes):
writeLine(stdout, spaces(notesPrefixLen) & line) writeLine(stdout, spaces(notesPrefixLen) & line)
@ -123,10 +123,12 @@ proc doInit(timelineLocation: string): void =
type ExpectedMarkPart = enum Time, Summary, Tags, Notes type ExpectedMarkPart = enum Time, Summary, Tags, Notes
proc edit(mark: var Mark): void = proc edit(mark: Mark): Mark =
## Interactively edit a mark using the editor named in the environment ## Interactively edit a mark using the editor named in the environment
## variable "EDITOR" ## variable "EDITOR"
result = mark
var var
tempFile: File tempFile: File
tempFileName: string tempFileName: string
@ -142,22 +144,27 @@ proc edit(mark: var Mark): void =
tempFile.writeLine( tempFile.writeLine(
"""# Everything from the line below to the end of the file will be considered """# Everything from the line below to the end of the file will be considered
# notes for this timeline mark.""") # notes for this timeline mark.""")
tempFile.write(mark.notes)
close(tempFile) close(tempFile)
tempFile = nil
discard os.execShellCmd "$EDITOR " & tempFileName & " </dev/tty >/dev/tty" let editor = getEnv("EDITOR", "vim")
discard os.execShellCmd editor & " " & tempFileName & " </dev/tty >/dev/tty"
var markPart = Time var markPart = Time
var notes: seq[string] = @[]
for line in lines tempFileName: for line in lines tempFileName:
if strip(line)[0] == '#': continue if strip(line).len > 0 and strip(line)[0] == '#': continue
elif markPart == Time: mark.time = parseTime(line); markPart = Summary elif markPart == Time: result.time = parseTime(line); markPart = Summary
elif markPart == Summary: mark.summary = line; markPart = Tags elif markPart == Summary: result.summary = line; markPart = Tags
elif markPart == Tags: elif markPart == Tags:
mark.tags = line.split({',', ';'}); result.tags = line.split({',', ';'});
markPart = Notes markPart = Notes
else: mark.notes &= line & "\x0D\x0A" else: notes.add(line)
result.notes = notes.join("\n")
finally: close(tempFile) finally: close(tempFile)
proc filterMarkIndices(timeline: Timeline, args: Table[string, Value]): seq[int] = proc filterMarkIndices(timeline: Timeline, args: Table[string, Value]): seq[int] =
@ -166,15 +173,15 @@ proc filterMarkIndices(timeline: Timeline, args: Table[string, Value]): seq[int]
let marks = timeline.marks let marks = timeline.marks
let now = getTime().local let now = getTime().local
let allIndices = sequtils.toSeq(0..<marks.len).filterIt(marks[it].summary != STOP_MSG).toSet let allIndices = sequtils.toSeq(0..<marks.len).filterIt(marks[it].summary != STOP_MSG).toHashSet
let union = args["--or"] let union = args["--or"]
var selected = var selected =
if union: initSet[int]() if union: initHashSet[int]()
else: allIndices else: allIndices
template filterMarks(curSet: HashSet[int], pred: untyped): untyped = template filterMarks(curSet: HashSet[int], pred: untyped): untyped =
var res: HashSet[int] = initSet[int]() var res: HashSet[int] = initHashSet[int]()
if union: if union:
for mIdx {.inject.} in allIndices: for mIdx {.inject.} in allIndices:
if pred: res.incl(mIdx) if pred: res.incl(mIdx)
@ -305,11 +312,11 @@ Options:
".ptkrc", $getEnv("PTKRC"), $getEnv("HOME") & "/.ptkrc"] ".ptkrc", $getEnv("PTKRC"), $getEnv("HOME") & "/.ptkrc"]
var ptkrcFilename: string = var ptkrcFilename: string =
foldl(ptkrcLocations, if len(a) > 0: a elif existsFile(b): b else: "") foldl(ptkrcLocations, if len(a) > 0: a elif fileExists(b): b else: "")
var cfg: JsonNode var cfg: JsonNode
var cfgFile: File var cfgFile: File
if not existsFile(ptkrcFilename): if not fileExists(ptkrcFilename):
warn "ptk: could not find .ptkrc file." warn "ptk: could not find .ptkrc file."
ptkrcFilename = $getEnv("HOME") & "/.ptkrc" ptkrcFilename = $getEnv("HOME") & "/.ptkrc"
try: try:
@ -331,7 +338,7 @@ Options:
"ptk.log.json"] "ptk.log.json"]
var timelineLocation = var timelineLocation =
foldl(timelineLocations, if len(a) > 0: a elif existsFile(b): b else: "") foldl(timelineLocations, if len(a) > 0: a elif fileExists(b): b else: "")
# Execute commands # Execute commands
if args["init"]: if args["init"]:
@ -342,7 +349,7 @@ Options:
let filesToMerge = args["<timeline>"] let filesToMerge = args["<timeline>"]
let timelines = filesToMerge.mapIt(loadTimeline(it)) let timelines = filesToMerge.mapIt(loadTimeline(it))
let names = timelines.mapIt(it.name).toSet let names = timelines.mapIt(it.name).toHashSet
let mergedName = sequtils.toSeq(names.items).foldl(a & " + " & b) let mergedName = sequtils.toSeq(names.items).foldl(a & " + " & b)
var merged: Timeline = ( var merged: Timeline = (
name: mergedName, name: mergedName,
@ -380,7 +387,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({',', ';'}).filterIt(not it.isNilOrWhitespace)) tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isEmptyOrWhitespace))
timeline.marks.add(newMark) timeline.marks.add(newMark)
@ -422,9 +429,9 @@ Options:
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({',', ';'}).filterIt(not it.isNilOrWhitespace)) tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isEmptyOrWhitespace))
if args["--edit"]: edit(newMark) if args["--edit"]: newMark = edit(newMark)
let prevLastIdx = timeline.marks.getLastIndex() let prevLastIdx = timeline.marks.getLastIndex()
timeline.marks.add(newMark) timeline.marks.add(newMark)
@ -454,7 +461,7 @@ Options:
notes: markToResume.notes, notes: markToResume.notes,
tags: markToResume.tags) tags: markToResume.tags)
if args["--edit"]: edit(newMark) if args["--edit"]: newMark = edit(newMark)
timeline.marks.add(newMark) timeline.marks.add(newMark)
timeline.writeMarks( timeline.writeMarks(
@ -491,7 +498,7 @@ Options:
except: raise newException(ValueError, except: raise newException(ValueError,
"invalid value for --time: " & getCurrentExceptionMsg()) "invalid value for --time: " & getCurrentExceptionMsg())
if args["--edit"]: edit(mark) if args["--edit"]: mark = edit(mark)
timeline.marks.delete(markIdx) timeline.marks.delete(markIdx)
timeline.marks.insert(mark, markIdx) timeline.marks.insert(mark, markIdx)

View File

@ -1,6 +1,6 @@
# Package # Package
version = "1.0.2" version = "1.0.8"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Personal Time Keeper" description = "Personal Time Keeper"
license = "MIT" license = "MIT"
@ -15,9 +15,9 @@ requires @[
"tempfile", "tempfile",
"isaac >= 0.1.3", "isaac >= 0.1.3",
"bcrypt", "bcrypt",
"jester 0.4.1", "jester 0.5.0",
"https://git.jdb-labs.com/jdb/nim-lang-utils.git", "https://git.jdb-labs.com/jdb/nim-lang-utils.git",
"https://git.jdb-labs.com/jdb/nim-cli-utils.git", "https://git.jdb-labs.com/jdb/nim-cli-utils.git >= 0.6.5",
"https://git.jdb-labs.com/jdb/nim-time-utils.git >= 0.5.2", "https://git.jdb-labs.com/jdb/nim-time-utils.git >= 0.5.2",
"https://git.jdb-labs.com/jdb/update-nim-package-version" "https://git.jdb-labs.com/jdb/update-nim-package-version"
] ]