AGENTS.md - Working with the scrn codebase#
This is a Go-based server that renders JSX components to BMP images for e-ink displays (TRMNL devices). It executes JSX components in a JavaScript runtime (goja), calculates layouts using a custom flexbox-like system, and renders to 1-bit BMP images.
Project Overview#
- Language: Go 1.23.3
- Module:
tangled.org/cdbrdr.com/scrn - Purpose: TRMNL-compatible display server that renders JSX layouts to BMP images
- Display: 800x480, 1-bit color (black/white), outputs BMP format
Essential Commands#
# Build the binary
go build -o scrn
# Run the server (uses examples/display.jsx by default)
go run .
# Run tests
go test ./...
go test ./internal/tree/... # specific package
# Regenerate pre-compiled JS modules (run after modifying JSX files)
cd internal/module
go generate ./...
# Download dependencies
go mod download
go mod tidy
Project Structure#
.
├── main.go # Entry point: loads display.jsx, starts HTTP server on :8081
├── internal/
│ ├── display/
│ │ ├── display.go # Display struct: manages goja runtime, renders BMP
│ │ └── draw.go # Rendering: draws nodes to image (uses gofont, masks)
│ ├── handler/
│ │ └── api.go # HTTP handlers: /api/setup, /api/display, /api/image.bmp
│ ├── tree/
│ │ ├── tree.go # Node interface, CalculateLayout, style parsing
│ │ ├── node.go # parseNode: converts JSX output to internal nodes
│ │ ├── module.go # Module nodes: calls JSX functions recursively
│ │ ├── flex.go # FlexNode: flexbox layout (horizontal/vertical)
│ │ ├── text.go # TextNode: text rendering with font size inheritance
│ │ ├── fill.go # FillNode: solid color fills (black/white/gray)
│ │ ├── img.go # ImgNode: embedded image rendering
│ │ └── tree_test.go # Tests using deep.Equal for struct comparison
│ ├── module/
│ │ ├── module.go # go:embed for JSX files, module loader
│ │ ├── jsx/
│ │ │ └── jsx-runtime.js # JSX transform runtime (h/jsx/jsxs functions)
│ │ ├── scrn/
│ │ │ ├── *.jsx # Built-in modules (weather, tracker, utils)
│ │ │ └── icons/ # PNG/JPG assets for weather icons
│ │ ├── fetch/
│ │ │ └── fetch.go # fetch() polyfill for goja (HTTP GET)
│ │ └── cache/
│ │ └── cache.go # Simple in-memory cache for goja
│ └── transform/
│ └── transform.go # esbuild wrapper: transforms JSX -> JS (IIFE/CJS)
└── examples/
└── display.jsx # Example main component
Architecture Flow#
- main.go loads
examples/display.jsxand creates aDisplay - transform converts JSX to JS using esbuild with automatic JSX runtime
- display creates a goja runtime, registers modules (fetch, cache, console, url)
- JSX components execute and return a tree structure (via jsx-runtime.js)
- tree.CalculateLayout parses the tree into typed nodes (FlexNode, TextNode, etc.)
- Layout engine calculates bounds using flexbox algorithm with weights/sizes
- draw.go renders nodes to
image.Paletted(black/white palette) - Output is BMP with TRMNL-specific header patches
JSX/Component System#
Built-in Node Types (from jsx-runtime.js)#
// Flex container - arranges children horizontally or vertically
<flex direction="horizontal|vertical" separator="none|solid|dashed" justify="start|end|center" gap={10}>
// Text node - content from children, supports fontSize
<text fontSize={32}>Hello World</text>
// Fill - solid color block
<fill color="white|black|gray" />
// Image - from embedded assets
<img src="scrn/icons/weather/day/clear.png" />
Style Properties (all nodes)#
weight- Flex grow factor (default: 1)size- Fixed pixel size (overrides weight)margin- Outer margin in pixelspadding- Inner padding in pixelscornerRadius- For rounded corners (via mask)fontSize- Inherited font size for text (default: 32)
Writing Modules#
Modules are JSX files in internal/module/scrn/:
// Import other modules
import { ProgressBar } from "scrn/utils"
// Export default function - receives props including width/height
export default function Weather({width, height, location}) {
// Use fetch() to get data
const response = fetch(`https://api.example.com/data`)
// Use cache for persistence between renders
cache.set("key", value)
const cached = cache.get("key")
// Return JSX tree
return <flex direction="vertical">
<text fontSize={60}>{temp}°C</text>
</flex>
}
Key Rules#
- Components must export default a function
- The function receives
width,height, and any props from parent fetch()is synchronous (blocking) in this runtimecacheis in-memory only (resets on server restart)- Images must be in
scrn/icons/path (embedded at build time)
Code Patterns#
Adding a New Node Type#
- Define
NodeTypeconstant intree/tree.go - Create struct in
tree/newtype.gowith:- Type-specific fields
Type() NodeTypemethodBounds() image.RectanglemethodGetStyle() NodeStylemethod
- Add case to
node.go'sGetNode()switch - Add renderer in
draw.go'sdrawNode()switch
Adding a Built-in Module#
- Create
.jsxfile ininternal/module/scrn/ - Run
go generate ./...ininternal/moduleto compile - Import in display files as
import X from "scrn/filename"
Error Handling#
- Go errors are defined as vars:
ErrNoDefaultExport = errors.New("...") - Goja (JS) errors use
runtime.NewGoError(err) - HTTP handlers log errors and return 500 status
Testing#
Tests use github.com/go-test/deep for struct comparison:
func TestSomething(t *testing.T) {
result, err := CalculateLayout(input, rect, nil)
expected := &FlexNode{...}
if diff := deep.Equal(result, expected); diff != nil {
t.Error(diff)
}
}
Run with: go test ./internal/tree/...
Important Gotchas#
-
JSX Compilation: JSX files are transformed at build time via
go generate. The generated.jsfiles are embedded and ignored in.gitignore. -
Synchronous fetch: The
fetch()implementation is blocking/synchronous, not Promise-based like browser fetch. -
BMP Header Patching: The BMP output has hardcoded header patches for TRMNL compatibility (bytes 46, 57, 61).
-
Font Size Inheritance:
fontSizeis inherited from parent nodes. The default is 32px. -
Flex Layout Algorithm:
- If
sizeis set, it's used as fixed pixels - Otherwise
weightdetermines proportional sizing of remaining space - Weights are normalized across siblings
- If
-
Module Resolution: Modules are loaded from
node_modules/scrn/*path in the goja runtime, mapped to embedded files. -
Goja Runtime: Each Display has its own isolated JS runtime. Modules share the same runtime within a Display.
-
Image Assets: Only images in
internal/module/scrn/icons/are available. They're embedded at build time via//go:embed. -
Color Palette: Only black, white, and gray. Gray is rendered as a checkerboard pattern.
Dependencies to Know#
- goja: JavaScript runtime for Go (ECMAScript 5.1+)
- esbuild: Bundles/transforms JSX (used as Go library)
- gin: HTTP web framework
- gofont: Font rendering for Go images
- mapstructure: Decodes map[string]any into structs (for JSX props)
API Endpoints (TRMNL-compatible)#
GET /api/setup- Returns device registration infoGET /api/display- Returns display metadata with image URLGET /api/image.bmp- Returns rendered BMP imagePOST /api/log- Receives device logs
The server mimics TRMNL's cloud API for local e-ink display testing.