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

9
.copilot/used-promts/01.Md Executable file
View File

@@ -0,0 +1,9 @@
# A web application
Create a simple web application using these Requirements:
- nim programming language
- single binary output
- HTMX for frontend interactivity
- TUI.CSS css framework for a retro look
create a scaffolding for the project and explain your choices.

View File

@@ -0,0 +1,13 @@
Change the asset handling to the following requirements:
- 2 Kinds of static assets
- Compiled into the binary (for example, images, css, js) called "static"
- Served from a defined folder (for example, user uploaded files) called "assets"
- statics are compiled into the binary and are served from a single handler
- Assets are served from a defined folder are served from a different handler
- The static handler uses a table to store all the static files
- The assets handler serves files from a defined folder on the filesystem
Additional requirements:
- make the htmx lib a static asset
- add a www folder for the assets
- add a demo asset file to the www folder

47
.gitignore vendored Normal file
View File

@@ -0,0 +1,47 @@
# Nim compiler cache
nimcache/
nimcache_*/
nimcache.*
# Nimble package manager cache and build metadata
nimblecache/
nimbledeps/
*.nimbledir/
*.nimblecache
*.nims.lock
*.nimsbak
# Build outputs
bin/
build/
htmldocs/
jsondocs/
# Compiled binaries and objects
*.exe
*.dll
*.lib
*.obj
*.o
*.so
*.dylib
*.a
*.pdb
*.ilk
*.map
# Logs and temporary files
*.log
*.out
*.err
*.tmp
*.temp
# Editor and tool artifacts
*.swp
*~
.DS_Store
Thumbs.db
.idea/
.vscode/
project.nimcache/

56
README.md Executable file
View File

@@ -0,0 +1,56 @@
# Luxtools Web Application Scaffold
Luxtools is a minimal-but-complete scaffold for a retro-styled web application built with the Nim programming language. It demonstrates how to deliver an interactive HTMX front end backed by a Nim HTTP server that compiles down to a single distributable binary.
## Why these choices?
- **Nim**: Nim's ahead-of-time compilation and small standard library make it ideal for producing a single self-contained binary. Its async HTTP primitives provide great performance with minimal boilerplate.
- **HTMX**: HTMX keeps the frontend simple by embracing hypermedia. We can ship almost entirely server-rendered HTML snippets, allowing Nim to stay in control without a bulky SPA build step.
- **TUI.CSS**: This lightweight CSS framework offers a retro terminal aesthetic with zero JavaScript and a tiny footprint—perfect for a nostalgic interface while remaining easy to customize.
- **Single binary delivery**: HTML templates are embedded at compile time via `staticRead`, so the server ships with everything it needs. Drop the compiled executable onto any host with the correct architecture and you're ready to run.
## Project layout
```
luxtools/
├── bin/ # Output directory for release binaries
├── assets/ # Static files served directly by the HTTP handler
├── src/
│ └── luxtools.nim # Main server entrypoint
├── templates/
│ └── index.html # HTMX-powered landing page (compiled into the binary)
├── tests/
│ └── test_rendering.nim# Lightweight regression tests for HTML helpers
├── luxtools.nimble # Nimble manifest with build/test tasks
└── README.md # You're here
```
## Running the app
```text
nimble run
```
This command compiles the server (in debug mode) and starts it at http://127.0.0.1:5000. The console output confirms the address. Open the URL in a browser to see the TUI.CSS interface. The counter widget uses `hx-post` to mutate state on the server, and the clock panel uses `hx-get` with a timed trigger.
## Building the single binary
```text
nimble build
```
A release-optimized executable is emitted to `bin/luxtools` (or `bin/luxtools.exe` on Windows). Because the HTML is embedded at compile time, shipping this one file is enough.
## Testing
```text
nimble test
```
The test suite checks the generated HTMX markup to catch regressions in the HTML helpers without spinning up the HTTP server.
## Next steps (optional)
- Add persistence by wiring Nim's database libraries or a lightweight KV store.
- Serve additional HTMX endpoints (e.g., todo lists, metrics dashboards).
- Extend the styling by composing more TUI.CSS components or creating custom themes.

1
assets/lib/htmx.2.0.7.min.js vendored Normal file

File diff suppressed because one or more lines are too long

19
luxtools.nimble Executable file
View File

@@ -0,0 +1,19 @@
# Nimble package definition for the Luxtools demo web application
version = "0.1.0"
author = "Luxtools Contributors"
description = "Scaffolding for a Nim + HTMX + TUI.CSS single-binary web application"
license = "MIT"
srcDir = "src"
bin = @["luxtools"]
requires "nim >= 1.6.0"
task build, "Build the optimized standalone binary":
exec "nim c -d:release --opt:speed -o:bin/luxtools src/luxtools.nim"
task run, "Start the development server with hot rebuild":
exec "nim c -r src/luxtools.nim"
task test, "Execute the lightweight test suite":
exec "nim c -r --path:src tests/test_rendering.nim"

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()

57
templates/index.html Executable file
View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Luxtools · Nim + HTMX demo</title>
<link
rel="stylesheet"
href="https://unpkg.com/tui-css@2.3.1/dist/tui.css"
/>
<script src="/lib/htmx.2.0.7.min.js" defer></script>
</head>
<body class="tui-bg-black">
<main class="tui-container tui-window" style="margin-top: 2rem;">
<fieldset class="tui-fieldset" style="padding: 2rem;">
<legend>Luxtools control panel</legend>
<p class="tui-text-white">
Luxtools demonstrates a Nim backend compiled into a single binary. The UI
uses <strong>TUI.CSS</strong> for a retro feel and <strong>HTMX</strong> for
partial page updates without JavaScript glue code.
</p>
<section style="margin-top: 1.5rem;">
<h2 class="tui-text-green">Interactive counter</h2>
<p class="tui-text-silver">
Click the button to trigger an <code>hx-post</code> request. The response
replaces only the counter panel.
</p>
<div
id="counter"
hx-get="/counter"
hx-trigger="load"
hx-swap="outerHTML"
>
<div class="tui-panel tui-panel-inline">Loading…</div>
</div>
</section>
<section style="margin-top: 1.5rem;">
<h2 class="tui-text-green">Server time</h2>
<p class="tui-text-silver">
A periodic <code>hx-get</code> refresh keeps this panel in sync with the
server clock.
</p>
<div
id="server-time"
hx-get="/time"
hx-trigger="load, every 5s"
hx-swap="outerHTML"
>
<div class="tui-panel tui-panel-inline">Loading…</div>
</div>
</section>
</fieldset>
</main>
</body>
</html>

20
tests/test_rendering.nim Executable file
View File

@@ -0,0 +1,20 @@
import std/[os, strutils, unittest]
import luxtools
const indexTemplate = staticRead("../templates/index.html")
suite "rendering helpers":
test "counter markup references hx attributes":
let html = renderCounter(3)
check html.contains("hx-post=\"/counter\"")
check html.contains("id=\"counter\"")
check html.contains("3")
test "index template uses vendored htmx build":
check indexTemplate.contains("/lib/htmx.2.0.7.min.js")
check indexTemplate.contains("<script src=\"/lib/htmx.2.0.7.min.js\" defer></script>")
check not indexTemplate.contains("unpkg.com/htmx")
test "vendored htmx asset lives in assets directory":
let assetPath = joinPath(getAppDir(), "..", "assets", "lib", "htmx.2.0.7.min.js")
check fileExists(assetPath)