Compare commits

..

12 Commits
1.0.4 ... main

Author SHA1 Message Date
c00be8c1fc Add asdf tool-versions definitions pinning this to Nim 1.6 2024-05-10 11:59:58 -05:00
96ee649bf6 Change the logic around how we load our config.
The previous logic that was intended to find the first non-empty
filename that represented a valid file location was wrong. Replaced with
a simpler filter-based version.

Additionally, if the user provides a specific configuration filename, we
no longer fall back to the defaults if it is not found. Instead we just
err out and inform them that the file the specified was not found.
2022-04-02 08:45:30 -05:00
69177ffa17 Log locations when unable to load config.
When we can't find a config file, log all the locations we looked and
don't create a new file in $HOME/.ptkrc.

There is currently an intermittent bug in the config loading logic that
is leading to the default config file in $HOME/.ptkrc not being found
and then being overwritten with the default config. This is step one in
fixing it.
2022-03-19 08:49:26 -05:00
8b6405441a Support additional date and time formats (particularly YYYY-MM-DD). 2022-01-03 08:57:43 -06:00
aff927b4f4 Fix for Nim 1.6.2 2021-12-20 17:42:55 -06:00
7c7695b891 Update for migration from jdb-labs -> jdb-software. 2021-06-08 18:49:03 -05:00
6ab23c7c84 Remove tags option actually removes tags now. 2020-11-12 14:44:52 -06:00
136e0ade02 Edit functionality no loger relies on sub-process receiving parent ENV. 2020-11-12 10:42:00 -06:00
3a4905ff6c Fix relative time input following Nim stdlib changes. 2020-11-12 10:41:05 -06:00
15a893d99f Update to reflect stdlib changes in Nim 1.4. 2020-11-12 04:30:36 -06:00
a58c7923cb Update to reflect stdlib changes in Nim 1.2 2020-07-06 11:17:56 -05:00
9de8a39d9e Fix an error parsing empty lines when editing a mark. 2020-03-16 16:02:16 -05:00
6 changed files with 84 additions and 56 deletions

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
nim 1.6.20

View File

@ -1,8 +1,8 @@
## Personal Time Keeping API Interface ## Personal Time Keeping API Interface
## =================================== ## ===================================
import asyncdispatch, base64, bcrypt, cliutils, docopt, jester, json, logging, import asyncdispatch, base64, bcrypt, cliutils, docopt, httpcore, 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,16 +55,16 @@ 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()
try: try:
if not request.headers.hasKey("Authorization"): if not headers(request).hasKey("Authorization"):
raiseEx "No auth token." raiseEx "No auth token."
let headerVal = request.headers["Authorization"] let headerVal = headers(request)["Authorization"]
if not headerVal.startsWith("Basic "): if not headerVal.startsWith("Basic "):
raiseEx "Invalid Authorization type (only 'Basic' is supported)." raiseEx "Invalid Authorization type (only 'Basic' is supported)."

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,26 @@ 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:sszzz", offsetFrom: OffsetFrom.None),
"yyyy-MM-dd'T'HH:mm", "yyyy-MM-dd HH:mm", (fmtStr: "yyyy-MM-dd HH:mm:sszzz", offsetFrom: OffsetFrom.None),
"MM-dd'T'HH:mm:ss", "MM-dd HH:mm:ss", (fmtStr: "yyyy-MM-dd'T'HH:mm:sszz", offsetFrom: OffsetFrom.None),
"MM-dd'T'HH:mm", "MM-dd HH:mm", (fmtStr: "yyyy-MM-dd HH:mm:sszz", offsetFrom: OffsetFrom.None),
"HH:mm:ss", "H:mm:ss", "H:mm", "HH:mm" ] (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) ]
## 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 =
@ -39,8 +56,25 @@ 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.
let now = now()
for fmt in TIME_FORMATS: for fmt in TIME_FORMATS:
try: return parse(timeStr, fmt) 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)
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 +146,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.4" const PTK_VERSION* = "1.0.14"

65
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, sugar, tempfile, terminal, times, uuids
import timeutils except `-`; import timeutils except `-`
import private/util import private/util
import private/api import private/api
@ -43,8 +43,7 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
writeLine(stdout, "No marks match the given criteria.") writeLine(stdout, "No marks match the given criteria.")
return return
var idxs = indices.sorted( var idxs = indices.sorted((a, b) => cmp(marks[a].time, marks[b].time))
proc(a, b: int): int = cmp(marks[a].time, marks[b].time))
let largestInterval = now - marks[idxs.first].time let largestInterval = now - marks[idxs.first].time
let timeFormat = let timeFormat =
@ -98,7 +97,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)
@ -149,20 +148,22 @@ proc edit(mark: Mark): Mark =
close(tempFile) close(tempFile)
tempFile = nil 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: result.time = parseTime(line); markPart = Summary elif markPart == Time: result.time = parseTime(line); markPart = Summary
elif markPart == Summary: result.summary = line; markPart = Tags elif markPart == Summary: result.summary = line; markPart = Tags
elif markPart == Tags: elif markPart == Tags:
result.tags = line.split({',', ';'}); result.tags = line.split({',', ';'});
result.notes = ""
markPart = Notes markPart = Notes
else: result.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] =
@ -171,15 +172,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)
@ -277,8 +278,9 @@ Options:
-e --edit Open the mark in an editor. -e --edit Open the mark in an editor.
-f --file <file> Use the given timeline file. -f --file <file> Use the given timeline file.
-g --tags <tags> Add the given tags (comma-separated) to the selected marks. -g --tags <tags> Add the given tags (comma-separated) to the selected marks.
-G --remove-tags <tagx> Remove the given tag from the selected marks. -G --remove-tags <tags> Remove the given tag from the selected marks.
-h --help Print this usage information. -h --help Print this usage information.
-m --matching <pattern> Restric the selection to marks matching <pattern>. -m --matching <pattern> Restric the selection to marks matching <pattern>.
-n --notes <notes> For add and amend, set the notes for a time mark. -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. -t --time <time> For add and amend, use this time instead of the current time.
@ -305,27 +307,21 @@ Options:
quit() quit()
# Find and parse the .ptkrc file # Find and parse the .ptkrc file
let ptkrcLocations = @[ let ptkrcLocations =
if args["--config"]: $args["--config"] else:"", if args["--config"]: @[$args["--config"]]
".ptkrc", $getEnv("PTKRC"), $getEnv("HOME") & "/.ptkrc"] else: @[".ptkrc", $getEnv("PTKRC"), $getEnv("HOME") & "/.ptkrc"]
var ptkrcFilename: string = let foundPtkrcLocations =
foldl(ptkrcLocations, if len(a) > 0: a elif existsFile(b): b else: "") ptkrcLocations.filterIt(it.len > 0 and fileExists(it))
var cfg: JsonNode var cfg: JsonNode
var cfgFile: File if foundPtkrcLocations.len < 1:
if not existsFile(ptkrcFilename):
warn "ptk: could not find .ptkrc file." warn "ptk: could not find .ptkrc file."
ptkrcFilename = $getEnv("HOME") & "/.ptkrc" debug "ptk: considered the following locations:\n\t" & ptkrcLocations.join("\n\t")
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(ptkrcFilename) try: cfg = parseFile(foundPtkrcLocations[0])
except: raise newException(IOError, except: raise newException(IOError,
"unable to read config file: " & ptkrcFilename & "unable to read config file: " & foundPtkrcLocations[0] &
"\x0D\x0A" & getCurrentExceptionMsg()) "\x0D\x0A" & getCurrentExceptionMsg())
# Find the time log file # Find the time log file
@ -336,7 +332,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"]:
@ -347,7 +343,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,
@ -385,7 +381,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)
@ -427,7 +423,7 @@ 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"]: newMark = edit(newMark) if args["--edit"]: newMark = edit(newMark)
@ -489,8 +485,7 @@ Options:
mark.tags = mark.tags.deduplicate mark.tags = mark.tags.deduplicate
if args["--remove-tags"]: if args["--remove-tags"]:
let tagsToRemove = (args["--remove-tags"] ?: "").split({',', ';'}) let tagsToRemove = (args["--remove-tags"] ?: "").split({',', ';'})
mark.tags = mark.tags.filter(proc (t: string): bool = mark.tags = mark.tags.filter((t) => not anyIt(tagsToRemove, it == t))
anyIt(tagsToRemove, it == t))
if args["--time"]: if args["--time"]:
try: mark.time = parseTime($args["--time"]) try: mark.time = parseTime($args["--time"])
except: raise newException(ValueError, except: raise newException(ValueError,

View File

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