WIP trmnl BYOS
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.