Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
15a893d99f | |||
a58c7923cb | |||
9de8a39d9e | |||
78480dc61c | |||
fb652eee30 | |||
a1333db427 | |||
af6aa5d520 |
@ -2,7 +2,7 @@
|
||||
## ===================================
|
||||
|
||||
import asyncdispatch, base64, bcrypt, cliutils, docopt, jester, json, logging,
|
||||
ospaths, sequtils, strutils, os, times, uuids
|
||||
sequtils, strutils, os, tables, times, uuids
|
||||
|
||||
import nre except toSeq
|
||||
|
||||
@ -37,10 +37,25 @@ proc loadApiConfig*(json: JsonNode): PtkApiCfg =
|
||||
dataDir: json.getOrFail("dataDir").getStr,
|
||||
users: json.getIfExists("users").getElems(@[]).mapIt(parseUser(it)))
|
||||
|
||||
template halt(code: HttpCode,
|
||||
headers: RawHeaders,
|
||||
content: string) =
|
||||
## Immediately replies with the specified request. This means any further
|
||||
## code will not be executed after calling this template in the current
|
||||
## route.
|
||||
bind TCActionSend, newHttpHeaders
|
||||
result[0] = CallbackAction.TCActionSend
|
||||
result[1] = code
|
||||
result[2] = some(headers)
|
||||
result[3] = content
|
||||
result.matched = true
|
||||
break allRoutes
|
||||
|
||||
|
||||
template checkAuth(cfg: PtkApiCfg) =
|
||||
## Check this request for authentication and authorization information.
|
||||
## If the request is not authorized, this template sets up the 401 response
|
||||
## correctly. The calling context needs only to return from the route.
|
||||
## If the request is not authorized, this template immediately returns a
|
||||
## 401 Unauthotized response
|
||||
|
||||
var authed {.inject.} = false
|
||||
var user {.inject.}: PtkUser = PtkUser()
|
||||
@ -67,13 +82,12 @@ template checkAuth(cfg: PtkApiCfg) =
|
||||
|
||||
except:
|
||||
stderr.writeLine "Auth failed: " & getCurrentExceptionMsg()
|
||||
response.data[0] = CallbackAction.TCActionSend
|
||||
response.data[1] = Http401
|
||||
response.data[2]["WWW_Authenticate"] = "Basic"
|
||||
response.data[2]["Content-Type"] = TXT
|
||||
response.data[3] = getCurrentExceptionMsg()
|
||||
halt(
|
||||
Http401,
|
||||
@{"Content-Type": TXT, "WWW_Authenticate": "Basic" },
|
||||
getCurrentExceptionMsg())
|
||||
|
||||
proc parseAndRun(user: PtkUser, cmd: string, params: StringTableRef): string =
|
||||
proc parseAndRun(user: PtkUser, cmd: string, params: Table[string, string]): string =
|
||||
|
||||
var args = queryParamsToCliArgs(params)
|
||||
args = @[cmd, "--file", user.timelinePath] & args
|
||||
@ -124,25 +138,25 @@ proc start_api*(cfg: PtkApiCfg) =
|
||||
get "/version": resp("ptk v" & PTK_VERSION, TXT)
|
||||
|
||||
get "/marks":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
checkAuth(cfg)
|
||||
|
||||
try: resp(parseAndRun(user, "list", request.params), TXT)
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/continue":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
checkAuth(cfg)
|
||||
|
||||
try: resp(parseAndRun(user, "continue", request.params), TXT)
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/sum-time":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
checkAuth(cfg)
|
||||
|
||||
try: resp(parseAndRun(user, "sum-time", request.params), TXT)
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/mark":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
checkAuth(cfg)
|
||||
|
||||
var newMark: Mark
|
||||
try: newMark = apiParseMark(parseJson(request.body))
|
||||
@ -156,7 +170,7 @@ proc start_api*(cfg: PtkApiCfg) =
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/stop":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
checkAuth(cfg)
|
||||
|
||||
var newMark: Mark
|
||||
try:
|
||||
@ -174,7 +188,7 @@ proc start_api*(cfg: PtkApiCfg) =
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/resume/@id":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
checkAuth(cfg)
|
||||
|
||||
var timeline: Timeline
|
||||
try: timeline = loadTimeline(user.timelinePath)
|
||||
@ -200,7 +214,7 @@ proc start_api*(cfg: PtkApiCfg) =
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/amend/@id":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
checkAuth(cfg)
|
||||
|
||||
try:
|
||||
var timeline = loadTimeline(user.timelinePath)
|
||||
@ -216,7 +230,7 @@ proc start_api*(cfg: PtkApiCfg) =
|
||||
except: resp(Http500, getCurrentExceptionMsg(), TXT)
|
||||
|
||||
post "/users":
|
||||
checkAuth(cfg); if not authed: return true
|
||||
checkAuth(cfg)
|
||||
if not user.isAdmin: resp(Http403, "insufficient permission", TXT)
|
||||
|
||||
var newUser: PtkUser
|
||||
@ -229,7 +243,7 @@ proc start_api*(cfg: PtkApiCfg) =
|
||||
newUser.timelinePath = cfg.dataDir / newUser.username & ".timeline.json"
|
||||
|
||||
try:
|
||||
discard parseAndRun(newUser, "init", newStringTable())
|
||||
discard parseAndRun(newUser, "init", initTable[string,string]())
|
||||
# TODO: save updated config!
|
||||
# cfg.users.add(newUser)
|
||||
resp(Http200, "ok", TXT)
|
||||
|
@ -1 +1 @@
|
||||
const PTK_VERSION* = "1.0.0"
|
||||
const PTK_VERSION* = "1.0.7"
|
70
ptk.nim
70
ptk.nim
@ -3,8 +3,10 @@
|
||||
##
|
||||
## Simple time keeping CLI
|
||||
|
||||
import algorithm, docopt, json, langutils, logging, os, nre, sequtils,
|
||||
sets, strutils, tempfile, terminal, times, timeutils, uuids
|
||||
import algorithm, docopt, json, langutils, logging, os, nre, std/wordwrap,
|
||||
sequtils, sets, strutils, tempfile, terminal, times, uuids
|
||||
|
||||
import timeutils except `-`
|
||||
|
||||
import private/util
|
||||
import private/api
|
||||
@ -18,18 +20,18 @@ proc exitErr(msg: string): void =
|
||||
fatal "ptk: " & msg
|
||||
quit(QuitFailure)
|
||||
|
||||
proc flexFormat(i: TimeInterval): string =
|
||||
proc flexFormat(i: Duration): string =
|
||||
## Pretty-format a time interval.
|
||||
|
||||
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'"
|
||||
if i > initDuration(days = 1): "d'd' H'h' m'm'"
|
||||
elif i >= initDuration(hours = 1): "H'h' m'm'"
|
||||
elif i >= initDuration(minutes = 1): "m'm' s's'"
|
||||
else: "s's'"
|
||||
|
||||
return i.format(fmt)
|
||||
|
||||
type WriteData = tuple[idx: int, mark: Mark, prefixLen: int, interval: TimeInterval]
|
||||
type WriteData = tuple[idx: int, mark: Mark, prefixLen: int, interval: Duration]
|
||||
|
||||
proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): void =
|
||||
## Write a nicely-formatted list of Marks to stdout.
|
||||
@ -46,9 +48,9 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
|
||||
|
||||
let largestInterval = now - marks[idxs.first].time
|
||||
let timeFormat =
|
||||
if largestInterval > 1.years: "yyyy-MM-dd HH:mm"
|
||||
elif largestInterval > 7.days: "MMM dd HH:mm"
|
||||
elif largestInterval > 1.days: "ddd HH:mm"
|
||||
if largestInterval > initDuration(days = 365): "yyyy-MM-dd HH:mm"
|
||||
elif largestInterval > initDuration(days = 7): "MMM dd HH:mm"
|
||||
elif largestInterval > initDuration(days = 1): "ddd HH:mm"
|
||||
else: "HH:mm"
|
||||
|
||||
var toWrite: seq[WriteData] = @[]
|
||||
@ -57,7 +59,7 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
|
||||
|
||||
for i in idxs:
|
||||
let
|
||||
interval: TimeInterval =
|
||||
interval: Duration =
|
||||
if (i == marks.len - 1): now - marks[i].time
|
||||
else: marks[i + 1].time - marks[i].time
|
||||
prefix =
|
||||
@ -96,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 = wordWrap(s = w.mark.notes,
|
||||
let wrappedNotes = wrapWords(s = w.mark.notes,
|
||||
maxLineWidth = colWidth)
|
||||
for line in splitLines(wrappedNotes):
|
||||
writeLine(stdout, spaces(notesPrefixLen) & line)
|
||||
@ -121,10 +123,12 @@ proc doInit(timelineLocation: string): void =
|
||||
|
||||
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
|
||||
## variable "EDITOR"
|
||||
|
||||
result = mark
|
||||
|
||||
var
|
||||
tempFile: File
|
||||
tempFileName: string
|
||||
@ -140,22 +144,26 @@ proc edit(mark: var Mark): void =
|
||||
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
|
||||
|
||||
discard os.execShellCmd "$EDITOR " & tempFileName & " </dev/tty >/dev/tty"
|
||||
|
||||
var markPart = Time
|
||||
var notes: seq[string] = @[]
|
||||
|
||||
for line in lines tempFileName:
|
||||
if strip(line)[0] == '#': continue
|
||||
elif markPart == Time: mark.time = parseTime(line); markPart = Summary
|
||||
elif markPart == Summary: mark.summary = line; markPart = Tags
|
||||
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
|
||||
elif markPart == Tags:
|
||||
mark.tags = line.split({',', ';'});
|
||||
result.tags = line.split({',', ';'});
|
||||
markPart = Notes
|
||||
else: mark.notes &= line & "\x0D\x0A"
|
||||
else: notes.add(line)
|
||||
|
||||
result.notes = notes.join("\n")
|
||||
finally: close(tempFile)
|
||||
|
||||
proc filterMarkIndices(timeline: Timeline, args: Table[string, Value]): seq[int] =
|
||||
@ -164,15 +172,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).toSet
|
||||
let allIndices = sequtils.toSeq(0..<marks.len).filterIt(marks[it].summary != STOP_MSG).toHashSet
|
||||
let union = args["--or"]
|
||||
|
||||
var selected =
|
||||
if union: initSet[int]()
|
||||
if union: initHashSet[int]()
|
||||
else: allIndices
|
||||
|
||||
template filterMarks(curSet: HashSet[int], pred: untyped): untyped =
|
||||
var res: HashSet[int] = initSet[int]()
|
||||
var res: HashSet[int] = initHashSet[int]()
|
||||
if union:
|
||||
for mIdx {.inject.} in allIndices:
|
||||
if pred: res.incl(mIdx)
|
||||
@ -303,11 +311,11 @@ Options:
|
||||
".ptkrc", $getEnv("PTKRC"), $getEnv("HOME") & "/.ptkrc"]
|
||||
|
||||
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 cfgFile: File
|
||||
if not existsFile(ptkrcFilename):
|
||||
if not fileExists(ptkrcFilename):
|
||||
warn "ptk: could not find .ptkrc file."
|
||||
ptkrcFilename = $getEnv("HOME") & "/.ptkrc"
|
||||
try:
|
||||
@ -329,7 +337,7 @@ Options:
|
||||
"ptk.log.json"]
|
||||
|
||||
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
|
||||
if args["init"]:
|
||||
@ -340,7 +348,7 @@ Options:
|
||||
let filesToMerge = args["<timeline>"]
|
||||
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)
|
||||
var merged: Timeline = (
|
||||
name: mergedName,
|
||||
@ -378,7 +386,7 @@ Options:
|
||||
time: if args["--time"]: parseTime($args["--time"]) else: now,
|
||||
summary: STOP_MSG,
|
||||
notes: args["--notes"] ?: "",
|
||||
tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isNilOrWhitespace))
|
||||
tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isEmptyOrWhitespace))
|
||||
|
||||
timeline.marks.add(newMark)
|
||||
|
||||
@ -420,9 +428,9 @@ Options:
|
||||
time: if args["--time"]: parseTime($args["--time"]) else: now,
|
||||
summary: args["<summary>"] ?: "",
|
||||
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()
|
||||
timeline.marks.add(newMark)
|
||||
@ -452,7 +460,7 @@ Options:
|
||||
notes: markToResume.notes,
|
||||
tags: markToResume.tags)
|
||||
|
||||
if args["--edit"]: edit(newMark)
|
||||
if args["--edit"]: newMark = edit(newMark)
|
||||
|
||||
timeline.marks.add(newMark)
|
||||
timeline.writeMarks(
|
||||
@ -489,7 +497,7 @@ Options:
|
||||
except: raise newException(ValueError,
|
||||
"invalid value for --time: " & getCurrentExceptionMsg())
|
||||
|
||||
if args["--edit"]: edit(mark)
|
||||
if args["--edit"]: mark = edit(mark)
|
||||
|
||||
timeline.marks.delete(markIdx)
|
||||
timeline.marks.insert(mark, markIdx)
|
||||
@ -533,7 +541,7 @@ Options:
|
||||
|
||||
if args["sum-time"]:
|
||||
|
||||
var intervals: seq[TimeInterval] = @[]
|
||||
var intervals: seq[Duration] = @[]
|
||||
|
||||
if args["--ids"]:
|
||||
for id in args["<ids>"]:
|
||||
|
19
ptk.nimble
19
ptk.nimble
@ -1,7 +1,6 @@
|
||||
# Package
|
||||
include "private/version.nim"
|
||||
|
||||
version = PTK_VERSION
|
||||
version = "1.0.7"
|
||||
author = "Jonathan Bernard"
|
||||
description = "Personal Time Keeper"
|
||||
license = "MIT"
|
||||
@ -9,5 +8,19 @@ bin = @["ptk"]
|
||||
|
||||
# Dependencies
|
||||
|
||||
requires @["nim >= 0.18.0", "docopt >= 0.6.4", "uuids", "langutils", "tempfile", "timeutils >= 0.2.2", "isaac >= 0.1.2", "bcrypt", "cliutils >= 0.5.0", "jester 0.2.0"]
|
||||
requires @[
|
||||
"nim >= 1.0.0",
|
||||
"docopt >= 0.6.8",
|
||||
"uuids",
|
||||
"tempfile",
|
||||
"isaac >= 0.1.3",
|
||||
"bcrypt",
|
||||
"jester 0.5.0",
|
||||
"https://git.jdb-labs.com/jdb/nim-lang-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/update-nim-package-version"
|
||||
]
|
||||
|
||||
task updateVersion, "Update the version of this package.":
|
||||
exec "update_nim_package_version ptk 'private/version.nim'"
|
Reference in New Issue
Block a user