Template for building interesting things in Nim. Build Nim terminal apps and games without installing Nim. See them working live on the web via GitHub Pages.
How the template works:
- When commits are made to the repo, GitHub Action will automatically compiles prehistorie.nim for both web(wasm) and linux.
- Compiled binaries made avalailable as releases.
- Makes compiled web(wasm) version available to GitHub Pages.
So basically, any changes(git commits) made to prehistorie.nim results in compilation and results are quickly made available directly on GitHub Pages.
- ✨ Zero dependencies - Pure Nim
- 🎮 Plugin system - Modular, extensible architecture
- 🎨 TrueColor support - RGB colors with 8/256-color fallback
- 🌐 Web + Native - Single codebase for terminal and browser
- 📦 Double-buffered - Smooth rendering with diff-based updates
- 🔤 Full Unicode - UTF-8 character support
- 📐 Layout utilities - Text wrapping, alignment, clipping
- 🎯 Resize handling - Automatic terminal size detection
Simply create a repo from this template, customize prehistorie.nim and it will automatically compile for web. Enable GitHub Pages and it serves the results.
# Install Nim if you haven't already
curl https://nim-lang.org/choosenim/init.sh -sSf | sh
# Build and run
nim c -r prehistorie.nim# Install Emscripten
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
cd ..
# Build
./build-simple.sh
# Serve
python3 -m http.server 8000
# Visit http://localhost:8000The codebase uses conditional compilation to handle native vs web targets:
when defined(emscripten):
# Web-specific code
proc emInit(width, height: int) {.exportc.} = ...
proc emUpdate(deltaMs: float) {.exportc.} = ...
else:
# Native-specific code
import posix, termios
setupRawMode()Color- RGB color with utilities for named colors and palette conversionStyle- Text styling (fg/bg colors, bold, italic, underline, dim)TermBuffer- Character grid with clipping and offset supportAppState- Global state container for plugins and rendering
Plugins are modules that hook into the engine lifecycle:
type PluginModule* = object
name*: string
initProc*: proc(state: var AppState)
updateProc*: proc(state: var AppState, dt: float)
renderProc*: proc(state: var AppState)
handleInputProc*: proc(state: var AppState, key: char): bool
shutdownProc*: proc(state: var AppState)Example plugin structure:
# plugins/my_plugin.nim
import ../prehistorie
proc myInit(state: var AppState) =
echo "Plugin initialized!"
proc myUpdate(state: var AppState, dt: float) =
# Update game logic
proc myRender(state: var AppState) =
state.currentBuffer.writeText(0, 0, "Hello!", state.styles["default"])
proc createMyPlugin*(): PluginModule =
result.name = "MyPlugin"
result.initProc = myInit
result.updateProc = myUpdate
result.renderProc = myRenderThe web version exports these functions via {.exportc.}:
emInit(width, height)- Initialize engine with terminal dimensionsemUpdate(deltaMs)- Update game state (called each frame)emResize(width, height)- Handle window resizeemGetWidth/emGetHeight()- Get current buffer dimensionsemGetCell(x, y)- Get character at positionemGetCellStyle(x, y, component)- Get style component at positionemHandleKey(key)- Handle keyboard input
setupRawMode()- Configure terminal for raw inputgetTermSize()- Query terminal dimensions via ioctl- Main loop:
getKey()- Non-blocking input checkupdatePlugins()- Update game staterenderPlugins()- Render to bufferdisplay()- Diff buffers and output ANSI codes
restoreTerminal()- Clean up on exit
- JavaScript loads
prehistorie.wasm.js Module.onRuntimeInitialized→ callemInit(80, 24)requestAnimationFrameloop:- Call
emUpdate(deltaTime) - Iterate buffer via
emGetCell/emGetCellStyle - Render to HTML5 Canvas with proper fonts/colors
- Call
- Event listeners pipe keyboard/resize events to Wasm
var buf = newTermBuffer(80, 24)
let style = Style(fg: green(), bg: black(), bold: true)
# Write text
buf.writeText(0, 0, "Hello, World!", style)
# Draw box
buf.drawBox(10, 5, 30, 10, style)
# Fill rectangle
buf.fillRect(5, 5, 10, 3, "█", style)
# Draw line
buf.drawLine(0, 0, 20, 0, "─", style)# Text wrapping
let lines = wrapText("Long text that needs wrapping...", 40)
for i, line in lines:
buf.writeText(0, i, line, style)
# Alignment
buf.writeAligned(5, "Centered!", 80, AlignCenter, style)# Clip to region
buf.setClip(10, 10, 40, 20)
buf.writeText(0, 0, "This will be clipped", style)
buf.clearClip()
# Scroll offset
buf.setOffset(-10, -5) # Shift everything
buf.writeText(10, 5, "Appears at 0,0", style)# Named colors
let c1 = red()
let c2 = blue()
# RGB
let c3 = rgb(128, 255, 64)
# Grayscale
let c4 = gray(128)
# Style with colors
let style = Style(
fg: rgb(255, 100, 50),
bg: black(),
bold: true,
italic: false
)The engine automatically converts to the best available color format:
- TrueColor (16.7M) - Direct RGB via ANSI 38;2;R;G;B
- 256-color - Converts to 6x6x6 color cube
- 8-color - Converts to basic ANSI colors
nim c prehistorie.nim # Debug build
nim c -d:release prehistorie.nim # Optimized
nim c -d:danger prehistorie.nim # Maximum optimizationMethod 1: Using build script
./build-simple.shMethod 2: Manual compilation
nim c -d:emscripten \
--os:linux --cpu:wasm32 \
--cc:clang --clang.exe:emcc --clang.linkerexe:emcc \
--passL:"-s WASM=1 -s EXPORTED_FUNCTIONS='[...]'" \
-o:prehistorie.wasm.js \
prehistorie.nimJust distribute the compiled binary. It has no runtime dependencies.
- Build the WebAssembly version
- Create a repository with:
index.htmlprehistorie.jsprehistorie.wasm.jsprehistorie.wasm
- Enable GitHub Pages in repo settings
- Visit
https://yourusername.github.io/yourrepo/
- Build WebAssembly version
- Create a
.zipwith all web files - Upload to itch.io as "HTML5" project
- Set viewport to 1200x800 (or adjust to your needs)
- ~30 FPS cap (configurable)
- Non-blocking input via
select() - Diff-based rendering (only changed cells update)
- Zero allocations in hot path after init
- 60 FPS via
requestAnimationFrame - Canvas-based rendering with text measurement
- Full Unicode support via browser fonts
- ~1-2MB Wasm binary (can be gzipped)
Install Emscripten SDK:
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh # Add to .bashrc for persistenceCheck browser console. Common issues:
- Missing
prehistorie.wasmfile - CORS errors (must serve via HTTP, not
file://) - Wrong paths in
prehistorie.js
Solution: Always use a local server:
python3 -m http.server 8000
# or
npx http-server -p 8000Check TERM environment variable:
echo $TERM # Should be xterm-256color or similar
export TERM=xterm-256colorFor TrueColor support:
export COLORTERM=truecolorSee the built-in demo (runs when no plugins loaded):
- Centered box with borders
- FPS counter
- Terminal dimensions
- Color capability detection
- Styled text examples
MIT License - See code for details
- Fork the repository
- Create a plugin or feature
- Test on both native and web
- Submit a pull request
Built with ❤️ using Nim