Init
This commit is contained in:
9
.copilot/used-promts/01.Md
Executable file
9
.copilot/used-promts/01.Md
Executable 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.
|
||||
13
.copilot/used-promts/02.md
Normal file
13
.copilot/used-promts/02.md
Normal 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
47
.gitignore
vendored
Normal 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
56
README.md
Executable 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
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
19
luxtools.nimble
Executable 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
119
src/luxtools.nim
Executable 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
57
templates/index.html
Executable 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
20
tests/test_rendering.nim
Executable 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)
|
||||
Reference in New Issue
Block a user