Compare commits

..

No commits in common. "main" and "1.0.2" have entirely different histories.
main ... 1.0.2

6 changed files with 62 additions and 95 deletions

View File

@ -1 +0,0 @@
nim 1.6.20

View File

@ -1,8 +1,8 @@
## Personal Time Keeping API Interface
## ===================================
import asyncdispatch, base64, bcrypt, cliutils, docopt, httpcore, jester, json, logging,
sequtils, strutils, os, tables, times, uuids
import asyncdispatch, base64, bcrypt, cliutils, docopt, jester, json, logging,
ospaths, sequtils, strutils, os, tables, times, uuids
import nre except toSeq
@ -39,7 +39,7 @@ proc loadApiConfig*(json: JsonNode): PtkApiCfg =
template halt(code: HttpCode,
headers: RawHeaders,
content: string) =
content: string): typed =
## Immediately replies with the specified request. This means any further
## code will not be executed after calling this template in the current
## route.
@ -55,16 +55,16 @@ template halt(code: HttpCode,
template checkAuth(cfg: PtkApiCfg) =
## Check this request for authentication and authorization information.
## If the request is not authorized, this template immediately returns a
## 401 Unauthotized response
## 401 Unauthotized response
var authed {.inject.} = false
var user {.inject.}: PtkUser = PtkUser()
try:
if not headers(request).hasKey("Authorization"):
if not request.headers.hasKey("Authorization"):
raiseEx "No auth token."
let headerVal = headers(request)["Authorization"]
let headerVal = request.headers["Authorization"]
if not headerVal.startsWith("Basic "):
raiseEx "Invalid Authorization type (only 'Basic' is supported)."

View File

@ -9,8 +9,6 @@ type
Timeline* = tuple[name: string, marks: seq[Mark]]
## Representation of a timeline: a name and sequence of Marks.
OffsetFrom = enum Year, Month, Day, None
const STOP_MSG* = "STOP"
let NO_MARK*: Mark = (
@ -22,26 +20,11 @@ const ISO_TIME_FORMAT* = "yyyy-MM-dd'T'HH:mm:ss"
## The canonical time format used by PTK.
const TIME_FORMATS* = @[
(fmtStr: "yyyy-MM-dd'T'HH:mm:sszzz", offsetFrom: OffsetFrom.None),
(fmtStr: "yyyy-MM-dd HH:mm:sszzz", offsetFrom: OffsetFrom.None),
(fmtStr: "yyyy-MM-dd'T'HH:mm:sszz", offsetFrom: OffsetFrom.None),
(fmtStr: "yyyy-MM-dd HH:mm:sszz", offsetFrom: OffsetFrom.None),
(fmtStr: "yyyy-MM-dd'T'HH:mm:ssz", offsetFrom: OffsetFrom.None),
(fmtStr: "yyyy-MM-dd HH:mm:ssz", offsetFrom: OffsetFrom.None),
(fmtStr: "yyyy-MM-dd'T'HH:mm:ss", offsetFrom: OffsetFrom.None),
(fmtStr: "yyyy-MM-dd HH:mm:ss", offsetFrom: OffsetFrom.None),
(fmtStr: "yyyy-MM-dd'T'HH:mm", offsetFrom: OffsetFrom.None),
(fmtStr: "yyyy-MM-dd HH:mm", offsetFrom: OffsetFrom.None),
(fmtStr: "yyyy-MM-dd", offsetFrom: OffsetFrom.None),
(fmtStr: "yyyy-MM", offsetFrom: OffsetFrom.None),
(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) ]
"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 getOrFail*(n: JsonNode, key: string, objName: string = ""): JsonNode =
@ -56,25 +39,8 @@ proc getIfExists*(n: JsonNode, key: string): JsonNode =
proc parseTime*(timeStr: string): DateTime =
## Helper to parse time strings trying multiple known formats.
let now = now()
for fmt in TIME_FORMATS:
try:
let parsed = parse(timeStr, fmt.fmtStr)
case fmt.offsetFrom:
of OffsetFrom.None:
return parsed
of OffsetFrom.Year:
return dateTime(now.year, parsed.month, parsed.monthday,
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)
try: return parse(timeStr, fmt)
except: discard nil
raise newException(Exception, "unable to interpret as a date: " & timeStr)
@ -146,3 +112,5 @@ proc getLastIndex*(marks: seq[Mark]): int =
while idx >= 0 and marks[idx].summary == STOP_MSG: idx -= 1
if idx < 0: result = -1
else: result = idx

View File

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

82
ptk.nim
View File

@ -3,10 +3,10 @@
##
## Simple time keeping CLI
import algorithm, docopt, json, langutils, logging, os, nre, std/wordwrap,
sequtils, sets, strutils, sugar, tempfile, terminal, times, uuids
import algorithm, docopt, json, langutils, logging, os, nre, sequtils,
sets, strutils, tempfile, terminal, times, uuids
import timeutils except `-`
import timeutils except `-`;
import private/util
import private/api
@ -43,7 +43,8 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
writeLine(stdout, "No marks match the given criteria.")
return
var idxs = indices.sorted((a, b) => cmp(marks[a].time, marks[b].time))
var idxs = indices.sorted(
proc(a, b: int): int = cmp(marks[a].time, marks[b].time))
let largestInterval = now - marks[idxs.first].time
let timeFormat =
@ -97,7 +98,7 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
if includeNotes and len(w.mark.notes.strip) > 0:
writeLine(stdout, "")
let wrappedNotes = wrapWords(s = w.mark.notes,
let wrappedNotes = wordWrap(s = w.mark.notes,
maxLineWidth = colWidth)
for line in splitLines(wrappedNotes):
writeLine(stdout, spaces(notesPrefixLen) & line)
@ -122,12 +123,10 @@ proc doInit(timelineLocation: string): void =
type ExpectedMarkPart = enum Time, Summary, Tags, Notes
proc edit(mark: Mark): Mark =
proc edit(mark: var Mark): void =
## Interactively edit a mark using the editor named in the environment
## variable "EDITOR"
result = mark
var
tempFile: File
tempFileName: string
@ -143,27 +142,22 @@ proc edit(mark: Mark): Mark =
tempFile.writeLine(
"""# Everything from the line below to the end of the file will be considered
# notes for this timeline mark.""")
tempFile.write(mark.notes)
close(tempFile)
tempFile = nil
let editor = getEnv("EDITOR", "vim")
discard os.execShellCmd editor & " " & tempFileName & " </dev/tty >/dev/tty"
discard os.execShellCmd "$EDITOR " & tempFileName & " </dev/tty >/dev/tty"
var markPart = Time
var notes: seq[string] = @[]
for line in lines tempFileName:
if strip(line).len > 0 and strip(line)[0] == '#': continue
elif markPart == Time: result.time = parseTime(line); markPart = Summary
elif markPart == Summary: result.summary = line; markPart = Tags
if strip(line)[0] == '#': continue
elif markPart == Time: mark.time = parseTime(line); markPart = Summary
elif markPart == Summary: mark.summary = line; markPart = Tags
elif markPart == Tags:
result.tags = line.split({',', ';'});
mark.tags = line.split({',', ';'});
markPart = Notes
else: notes.add(line)
else: mark.notes &= line & "\x0D\x0A"
result.notes = notes.join("\n")
finally: close(tempFile)
proc filterMarkIndices(timeline: Timeline, args: Table[string, Value]): seq[int] =
@ -172,15 +166,15 @@ proc filterMarkIndices(timeline: Timeline, args: Table[string, Value]): seq[int]
let marks = timeline.marks
let now = getTime().local
let allIndices = sequtils.toSeq(0..<marks.len).filterIt(marks[it].summary != STOP_MSG).toHashSet
let allIndices = sequtils.toSeq(0..<marks.len).filterIt(marks[it].summary != STOP_MSG).toSet
let union = args["--or"]
var selected =
if union: initHashSet[int]()
if union: initSet[int]()
else: allIndices
template filterMarks(curSet: HashSet[int], pred: untyped): untyped =
var res: HashSet[int] = initHashSet[int]()
var res: HashSet[int] = initSet[int]()
if union:
for mIdx {.inject.} in allIndices:
if pred: res.incl(mIdx)
@ -278,9 +272,8 @@ Options:
-e --edit Open the mark in an editor.
-f --file <file> Use the given timeline file.
-g --tags <tags> Add the given tags (comma-separated) to the selected marks.
-G --remove-tags <tags> Remove the given tag from the selected marks.
-G --remove-tags <tagx> Remove the given tag from the selected marks.
-h --help Print this usage information.
-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.
@ -307,21 +300,27 @@ Options:
quit()
# Find and parse the .ptkrc file
let ptkrcLocations =
if args["--config"]: @[$args["--config"]]
else: @[".ptkrc", $getEnv("PTKRC"), $getEnv("HOME") & "/.ptkrc"]
let ptkrcLocations = @[
if args["--config"]: $args["--config"] else:"",
".ptkrc", $getEnv("PTKRC"), $getEnv("HOME") & "/.ptkrc"]
let foundPtkrcLocations =
ptkrcLocations.filterIt(it.len > 0 and fileExists(it))
var ptkrcFilename: string =
foldl(ptkrcLocations, if len(a) > 0: a elif existsFile(b): b else: "")
var cfg: JsonNode
if foundPtkrcLocations.len < 1:
var cfgFile: File
if not existsFile(ptkrcFilename):
warn "ptk: could not find .ptkrc file."
debug "ptk: considered the following locations:\n\t" & ptkrcLocations.join("\n\t")
ptkrcFilename = $getEnv("HOME") & "/.ptkrc"
try:
cfgFile = open(ptkrcFilename, fmWrite)
cfgFile.write("{\"timelineLogFile\": \"timeline.log.json\"}")
except: warn "ptk: could not write default .ptkrc to " & ptkrcFilename
finally: close(cfgFile)
try: cfg = parseFile(foundPtkrcLocations[0])
try: cfg = parseFile(ptkrcFilename)
except: raise newException(IOError,
"unable to read config file: " & foundPtkrcLocations[0] &
"unable to read config file: " & ptkrcFilename &
"\x0D\x0A" & getCurrentExceptionMsg())
# Find the time log file
@ -332,7 +331,7 @@ Options:
"ptk.log.json"]
var timelineLocation =
foldl(timelineLocations, if len(a) > 0: a elif fileExists(b): b else: "")
foldl(timelineLocations, if len(a) > 0: a elif existsFile(b): b else: "")
# Execute commands
if args["init"]:
@ -343,7 +342,7 @@ Options:
let filesToMerge = args["<timeline>"]
let timelines = filesToMerge.mapIt(loadTimeline(it))
let names = timelines.mapIt(it.name).toHashSet
let names = timelines.mapIt(it.name).toSet
let mergedName = sequtils.toSeq(names.items).foldl(a & " + " & b)
var merged: Timeline = (
name: mergedName,
@ -381,7 +380,7 @@ Options:
time: if args["--time"]: parseTime($args["--time"]) else: now,
summary: STOP_MSG,
notes: args["--notes"] ?: "",
tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isEmptyOrWhitespace))
tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isNilOrWhitespace))
timeline.marks.add(newMark)
@ -423,9 +422,9 @@ Options:
time: if args["--time"]: parseTime($args["--time"]) else: now,
summary: args["<summary>"] ?: "",
notes: args["--notes"] ?: "",
tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isEmptyOrWhitespace))
tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isNilOrWhitespace))
if args["--edit"]: newMark = edit(newMark)
if args["--edit"]: edit(newMark)
let prevLastIdx = timeline.marks.getLastIndex()
timeline.marks.add(newMark)
@ -455,7 +454,7 @@ Options:
notes: markToResume.notes,
tags: markToResume.tags)
if args["--edit"]: newMark = edit(newMark)
if args["--edit"]: edit(newMark)
timeline.marks.add(newMark)
timeline.writeMarks(
@ -485,13 +484,14 @@ Options:
mark.tags = mark.tags.deduplicate
if args["--remove-tags"]:
let tagsToRemove = (args["--remove-tags"] ?: "").split({',', ';'})
mark.tags = mark.tags.filter((t) => not anyIt(tagsToRemove, it == t))
mark.tags = mark.tags.filter(proc (t: string): bool =
anyIt(tagsToRemove, it == t))
if args["--time"]:
try: mark.time = parseTime($args["--time"])
except: raise newException(ValueError,
"invalid value for --time: " & getCurrentExceptionMsg())
if args["--edit"]: mark = edit(mark)
if args["--edit"]: edit(mark)
timeline.marks.delete(markIdx)
timeline.marks.insert(mark, markIdx)

View File

@ -1,6 +1,6 @@
# Package
version = "1.0.14"
version = "1.0.2"
author = "Jonathan Bernard"
description = "Personal Time Keeper"
license = "MIT"
@ -15,11 +15,11 @@ requires @[
"tempfile",
"isaac >= 0.1.3",
"bcrypt",
"jester 0.5.0",
"https://git.jdb-software.com/jdb/nim-lang-utils.git",
"https://git.jdb-software.com/jdb/nim-cli-utils.git >= 0.6.5",
"https://git.jdb-software.com/jdb/nim-time-utils.git >= 0.5.2",
"https://git.jdb-software.com/jdb/update-nim-package-version"
"jester 0.4.1",
"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-time-utils.git >= 0.5.2",
"https://git.jdb-labs.com/jdb/update-nim-package-version"
]
task updateVersion, "Update the version of this package.":