This commit is contained in:
2025-10-01 12:07:25 +02:00
commit 34127035c5
9 changed files with 341 additions and 0 deletions

119
src/luxtools.nim Executable file
View File

@@ -0,0 +1,119 @@
import std/[asynchttpserver, asyncdispatch, os, strformat, strutils, times]
const
indexPage = staticRead("../templates/index.html")
var clickCount = 0
proc htmlHeaders(): HttpHeaders =
result = newHttpHeaders()
result.add("Content-Type", "text/html; charset=utf-8")
proc assetHeaders(contentType: string): HttpHeaders =
result = newHttpHeaders()
result.add("Content-Type", contentType)
proc renderCounter*(count: int): string =
&"""
<div id="counter" class="tui-panel tui-panel-inline">
<p><strong>Clicks:</strong> {count}</p>
<button class="tui-button" hx-post="/counter" hx-target="#counter" hx-swap="outerHTML">
Increment
</button>
</div>
"""
proc renderTime(): string =
let now = now().format("yyyy-MM-dd HH:mm:ss")
&"""
<div id="server-time" class="tui-panel tui-panel-inline">
<p><strong>Server time:</strong> {now}</p>
</div>
"""
proc sendHtml(req: Request, body: string) {.async, gcsafe.} =
await req.respond(Http200, body, htmlHeaders())
proc sendHtml(req: Request, code: HttpCode, body: string) {.async, gcsafe.} =
await req.respond(code, body, htmlHeaders())
proc sendAsset(req: Request, body: string, contentType: string) {.async, gcsafe.} =
await req.respond(Http200, body, assetHeaders(contentType))
proc guessContentType(path: string): string =
let ext = splitFile(path).ext.toLowerAscii()
case ext
of ".js":
result = "application/javascript"
of ".css":
result = "text/css"
of ".html":
result = "text/html; charset=utf-8"
of ".json":
result = "application/json"
of ".png":
result = "image/png"
of ".jpg", ".jpeg":
result = "image/jpeg"
of ".svg":
result = "image/svg+xml"
of ".gif":
result = "image/gif"
of ".ico":
result = "image/x-icon"
else:
result = "application/octet-stream"
proc trySendAsset(req: Request): Future[bool] {.async, gcsafe.} =
if req.reqMethod notin {HttpGet, HttpHead}:
return false
let path = req.url.path
if path.len <= 1 or path[0] != '/':
return false
let relative = path[1..^1]
if relative.len == 0 or relative.contains("..") or relative.contains('\\'):
return false
let assetPath = joinPath(absolutePath(joinPath(getAppDir(), "..", "assets")), relative)
if not fileExists(assetPath):
return false
let contentType = guessContentType(assetPath)
if req.reqMethod == HttpHead:
await req.respond(Http200, "", assetHeaders(contentType))
else:
await sendAsset(req, readFile(assetPath), contentType)
return true
proc handleRequest(req: Request) {.async, gcsafe.} =
case req.url.path
of "/":
await sendHtml(req, indexPage)
of "/counter":
if req.reqMethod in {HttpPost, HttpPut}:
inc clickCount
await sendHtml(req, renderCounter(clickCount))
of "/time":
await sendHtml(req, renderTime())
else:
if await trySendAsset(req):
return
await sendHtml(req, Http404, """
<div class="tui-window">
<fieldset class="tui-fieldset">
<legend>Error</legend>
<p>Route """ & req.url.path & """ not found.</p>
</fieldset>
</div>
""")
proc runServer() {.async.} =
let server = newAsyncHttpServer()
let port = Port(5000)
echo &"Luxtools web server running on http://127.0.0.1:{port.int}"
await server.serve(port, handleRequest)
when isMainModule:
waitFor runServer()