WIP trmnl BYOS
at main 221 lines 8.2 kB view raw view rendered
1# AGENTS.md - Working with the scrn codebase 2 3This 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. 4 5## Project Overview 6 7- **Language**: Go 1.23.3 8- **Module**: `tangled.org/cdbrdr.com/scrn` 9- **Purpose**: TRMNL-compatible display server that renders JSX layouts to BMP images 10- **Display**: 800x480, 1-bit color (black/white), outputs BMP format 11 12## Essential Commands 13 14```bash 15# Build the binary 16go build -o scrn 17 18# Run the server (uses examples/display.jsx by default) 19go run . 20 21# Run tests 22go test ./... 23go test ./internal/tree/... # specific package 24 25# Regenerate pre-compiled JS modules (run after modifying JSX files) 26cd internal/module 27go generate ./... 28 29# Download dependencies 30go mod download 31go mod tidy 32``` 33 34## Project Structure 35 36``` 37. 38├── main.go # Entry point: loads display.jsx, starts HTTP server on :8081 39├── internal/ 40│ ├── display/ 41│ │ ├── display.go # Display struct: manages goja runtime, renders BMP 42│ │ └── draw.go # Rendering: draws nodes to image (uses gofont, masks) 43│ ├── handler/ 44│ │ └── api.go # HTTP handlers: /api/setup, /api/display, /api/image.bmp 45│ ├── tree/ 46│ │ ├── tree.go # Node interface, CalculateLayout, style parsing 47│ │ ├── node.go # parseNode: converts JSX output to internal nodes 48│ │ ├── module.go # Module nodes: calls JSX functions recursively 49│ │ ├── flex.go # FlexNode: flexbox layout (horizontal/vertical) 50│ │ ├── text.go # TextNode: text rendering with font size inheritance 51│ │ ├── fill.go # FillNode: solid color fills (black/white/gray) 52│ │ ├── img.go # ImgNode: embedded image rendering 53│ │ └── tree_test.go # Tests using deep.Equal for struct comparison 54│ ├── module/ 55│ │ ├── module.go # go:embed for JSX files, module loader 56│ │ ├── jsx/ 57│ │ │ └── jsx-runtime.js # JSX transform runtime (h/jsx/jsxs functions) 58│ │ ├── scrn/ 59│ │ │ ├── *.jsx # Built-in modules (weather, tracker, utils) 60│ │ │ └── icons/ # PNG/JPG assets for weather icons 61│ │ ├── fetch/ 62│ │ │ └── fetch.go # fetch() polyfill for goja (HTTP GET) 63│ │ └── cache/ 64│ │ └── cache.go # Simple in-memory cache for goja 65│ └── transform/ 66│ └── transform.go # esbuild wrapper: transforms JSX -> JS (IIFE/CJS) 67└── examples/ 68 └── display.jsx # Example main component 69``` 70 71## Architecture Flow 72 731. **main.go** loads `examples/display.jsx` and creates a `Display` 742. **transform** converts JSX to JS using esbuild with automatic JSX runtime 753. **display** creates a goja runtime, registers modules (fetch, cache, console, url) 764. **JSX components** execute and return a tree structure (via jsx-runtime.js) 775. **tree.CalculateLayout** parses the tree into typed nodes (FlexNode, TextNode, etc.) 786. **Layout engine** calculates bounds using flexbox algorithm with weights/sizes 797. **draw.go** renders nodes to `image.Paletted` (black/white palette) 808. **Output** is BMP with TRMNL-specific header patches 81 82## JSX/Component System 83 84### Built-in Node Types (from jsx-runtime.js) 85 86```jsx 87// Flex container - arranges children horizontally or vertically 88<flex direction="horizontal|vertical" separator="none|solid|dashed" justify="start|end|center" gap={10}> 89 90// Text node - content from children, supports fontSize 91<text fontSize={32}>Hello World</text> 92 93// Fill - solid color block 94<fill color="white|black|gray" /> 95 96// Image - from embedded assets 97<img src="scrn/icons/weather/day/clear.png" /> 98``` 99 100### Style Properties (all nodes) 101 102- `weight` - Flex grow factor (default: 1) 103- `size` - Fixed pixel size (overrides weight) 104- `margin` - Outer margin in pixels 105- `padding` - Inner padding in pixels 106- `cornerRadius` - For rounded corners (via mask) 107- `fontSize` - Inherited font size for text (default: 32) 108 109### Writing Modules 110 111Modules are JSX files in `internal/module/scrn/`: 112 113```jsx 114// Import other modules 115import { ProgressBar } from "scrn/utils" 116 117// Export default function - receives props including width/height 118export default function Weather({width, height, location}) { 119 // Use fetch() to get data 120 const response = fetch(`https://api.example.com/data`) 121 122 // Use cache for persistence between renders 123 cache.set("key", value) 124 const cached = cache.get("key") 125 126 // Return JSX tree 127 return <flex direction="vertical"> 128 <text fontSize={60}>{temp}°C</text> 129 </flex> 130} 131``` 132 133### Key Rules 134 135- Components **must** export default a function 136- The function receives `width`, `height`, and any props from parent 137- `fetch()` is synchronous (blocking) in this runtime 138- `cache` is in-memory only (resets on server restart) 139- Images must be in `scrn/icons/` path (embedded at build time) 140 141## Code Patterns 142 143### Adding a New Node Type 144 1451. Define `NodeType` constant in `tree/tree.go` 1462. Create struct in `tree/newtype.go` with: 147 - Type-specific fields 148 - `Type() NodeType` method 149 - `Bounds() image.Rectangle` method 150 - `GetStyle() NodeStyle` method 1513. Add case to `node.go`'s `GetNode()` switch 1524. Add renderer in `draw.go`'s `drawNode()` switch 153 154### Adding a Built-in Module 155 1561. Create `.jsx` file in `internal/module/scrn/` 1572. Run `go generate ./...` in `internal/module` to compile 1583. Import in display files as `import X from "scrn/filename"` 159 160### Error Handling 161 162- Go errors are defined as vars: `ErrNoDefaultExport = errors.New("...")` 163- Goja (JS) errors use `runtime.NewGoError(err)` 164- HTTP handlers log errors and return 500 status 165 166## Testing 167 168Tests use `github.com/go-test/deep` for struct comparison: 169 170```go 171func TestSomething(t *testing.T) { 172 result, err := CalculateLayout(input, rect, nil) 173 expected := &FlexNode{...} 174 175 if diff := deep.Equal(result, expected); diff != nil { 176 t.Error(diff) 177 } 178} 179``` 180 181Run with: `go test ./internal/tree/...` 182 183## Important Gotchas 184 1851. **JSX Compilation**: JSX files are transformed at build time via `go generate`. The generated `.js` files are embedded and ignored in `.gitignore`. 186 1872. **Synchronous fetch**: The `fetch()` implementation is blocking/synchronous, not Promise-based like browser fetch. 188 1893. **BMP Header Patching**: The BMP output has hardcoded header patches for TRMNL compatibility (bytes 46, 57, 61). 190 1914. **Font Size Inheritance**: `fontSize` is inherited from parent nodes. The default is 32px. 192 1935. **Flex Layout Algorithm**: 194 - If `size` is set, it's used as fixed pixels 195 - Otherwise `weight` determines proportional sizing of remaining space 196 - Weights are normalized across siblings 197 1986. **Module Resolution**: Modules are loaded from `node_modules/scrn/*` path in the goja runtime, mapped to embedded files. 199 2007. **Goja Runtime**: Each Display has its own isolated JS runtime. Modules share the same runtime within a Display. 201 2028. **Image Assets**: Only images in `internal/module/scrn/icons/` are available. They're embedded at build time via `//go:embed`. 203 2049. **Color Palette**: Only black, white, and gray. Gray is rendered as a checkerboard pattern. 205 206## Dependencies to Know 207 208- **goja**: JavaScript runtime for Go (ECMAScript 5.1+) 209- **esbuild**: Bundles/transforms JSX (used as Go library) 210- **gin**: HTTP web framework 211- **gofont**: Font rendering for Go images 212- **mapstructure**: Decodes map[string]any into structs (for JSX props) 213 214## API Endpoints (TRMNL-compatible) 215 216- `GET /api/setup` - Returns device registration info 217- `GET /api/display` - Returns display metadata with image URL 218- `GET /api/image.bmp` - Returns rendered BMP image 219- `POST /api/log` - Receives device logs 220 221The server mimics TRMNL's cloud API for local e-ink display testing.