# 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 ```bash # 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 1. **main.go** loads `examples/display.jsx` and creates a `Display` 2. **transform** converts JSX to JS using esbuild with automatic JSX runtime 3. **display** creates a goja runtime, registers modules (fetch, cache, console, url) 4. **JSX components** execute and return a tree structure (via jsx-runtime.js) 5. **tree.CalculateLayout** parses the tree into typed nodes (FlexNode, TextNode, etc.) 6. **Layout engine** calculates bounds using flexbox algorithm with weights/sizes 7. **draw.go** renders nodes to `image.Paletted` (black/white palette) 8. **Output** is BMP with TRMNL-specific header patches ## JSX/Component System ### Built-in Node Types (from jsx-runtime.js) ```jsx // Flex container - arranges children horizontally or vertically // Text node - content from children, supports fontSize Hello World // Fill - solid color block // Image - from embedded assets ``` ### Style Properties (all nodes) - `weight` - Flex grow factor (default: 1) - `size` - Fixed pixel size (overrides weight) - `margin` - Outer margin in pixels - `padding` - Inner padding in pixels - `cornerRadius` - For rounded corners (via mask) - `fontSize` - Inherited font size for text (default: 32) ### Writing Modules Modules are JSX files in `internal/module/scrn/`: ```jsx // 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 {temp}°C } ``` ### Key Rules - Components **must** export default a function - The function receives `width`, `height`, and any props from parent - `fetch()` is synchronous (blocking) in this runtime - `cache` is 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 1. Define `NodeType` constant in `tree/tree.go` 2. Create struct in `tree/newtype.go` with: - Type-specific fields - `Type() NodeType` method - `Bounds() image.Rectangle` method - `GetStyle() NodeStyle` method 3. Add case to `node.go`'s `GetNode()` switch 4. Add renderer in `draw.go`'s `drawNode()` switch ### Adding a Built-in Module 1. Create `.jsx` file in `internal/module/scrn/` 2. Run `go generate ./...` in `internal/module` to compile 3. 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: ```go 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 1. **JSX Compilation**: JSX files are transformed at build time via `go generate`. The generated `.js` files are embedded and ignored in `.gitignore`. 2. **Synchronous fetch**: The `fetch()` implementation is blocking/synchronous, not Promise-based like browser fetch. 3. **BMP Header Patching**: The BMP output has hardcoded header patches for TRMNL compatibility (bytes 46, 57, 61). 4. **Font Size Inheritance**: `fontSize` is inherited from parent nodes. The default is 32px. 5. **Flex Layout Algorithm**: - If `size` is set, it's used as fixed pixels - Otherwise `weight` determines proportional sizing of remaining space - Weights are normalized across siblings 6. **Module Resolution**: Modules are loaded from `node_modules/scrn/*` path in the goja runtime, mapped to embedded files. 7. **Goja Runtime**: Each Display has its own isolated JS runtime. Modules share the same runtime within a Display. 8. **Image Assets**: Only images in `internal/module/scrn/icons/` are available. They're embedded at build time via `//go:embed`. 9. **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 info - `GET /api/display` - Returns display metadata with image URL - `GET /api/image.bmp` - Returns rendered BMP image - `POST /api/log` - Receives device logs The server mimics TRMNL's cloud API for local e-ink display testing.