tangled
alpha
login
or
join now
cdbrdr.com
/
scrn
0
fork
atom
WIP trmnl BYOS
0
fork
atom
overview
issues
pulls
pipelines
feat: extending modules
cdbrdr.com
1 month ago
ef5d481a
180e2b8a
+1275
-7
11 changed files
expand all
collapse all
unified
split
.crush.json
.skills
writing-modules
SKILL.md
AGENTS.md
examples
display.jsx
go.mod
go.sum
internal
display
draw.go
module
scrn
github.jsx
tracker.jsx
weather.jsx
main.go
+8
.crush.json
···
1
1
+
{
2
2
+
"options": {
3
3
+
"skills_paths": [
4
4
+
"~/.config/crush/skills",
5
5
+
"./.skills"
6
6
+
]
7
7
+
}
8
8
+
}
+796
.skills/writing-modules/SKILL.md
···
1
1
+
---
2
2
+
name: writing-modules
3
3
+
description: Guide for creating modules for scrn. This skill should be used when users want to create a new module (or update an existing module) that extends scrn's capabilities.
4
4
+
---
5
5
+
# Writing Modules for scrn
6
6
+
7
7
+
A comprehensive guide for designing and implementing JSX modules for the scrn e-ink display system.
8
8
+
9
9
+
## Overview
10
10
+
11
11
+
Modules are JSX components that render layouts to 800x480 1-bit BMP images for e-ink displays. They can fetch data, use caching, and compose other modules to create rich display interfaces.
12
12
+
13
13
+
## Module Structure
14
14
+
15
15
+
### Basic Template
16
16
+
17
17
+
Every module follows this pattern:
18
18
+
19
19
+
```jsx
20
20
+
export default function ModuleName({width, height, propName}) {
21
21
+
// 1. Fetch or compute data
22
22
+
// 2. Build JSX tree
23
23
+
// 3. Return layout
24
24
+
25
25
+
return <flex direction="vertical">
26
26
+
{/* Your layout here */}
27
27
+
</flex>
28
28
+
}
29
29
+
```
30
30
+
31
31
+
### Key Principles
32
32
+
33
33
+
1. **Export default function** - The entry point must be a default export
34
34
+
2. **Receive props** - Always destructure `{width, height, ...customProps}`
35
35
+
3. **Synchronous execution** - All code runs synchronously, including fetch()
36
36
+
4. **Pure JSX return** - Return a single JSX tree (usually `<flex>` as root)
37
37
+
38
38
+
## Available Props
39
39
+
40
40
+
Every module receives these standard props:
41
41
+
42
42
+
- `width` - Allocated width in pixels
43
43
+
- `height` - Allocated height in pixels
44
44
+
- Any custom props passed from parent components
45
45
+
46
46
+
```jsx
47
47
+
// In display.jsx
48
48
+
<Weather weight={60} padding={20} location="Vienna, Austria" />
49
49
+
50
50
+
// In weather.jsx - receives location plus standard width/height
51
51
+
export default function Weather({width, height, location}) {
52
52
+
// ...
53
53
+
}
54
54
+
```
55
55
+
56
56
+
## Built-in Node Types
57
57
+
58
58
+
### `<flex>` - Layout Container
59
59
+
60
60
+
The primary layout primitive. Arranges children horizontally or vertically.
61
61
+
62
62
+
```jsx
63
63
+
<flex
64
64
+
direction="horizontal|vertical" // Layout direction (default: horizontal)
65
65
+
separator="none|solid|dashed" // Visual separator between children
66
66
+
justify="start|end|center" // Alignment along main axis
67
67
+
gap={10} // Spacing between children in pixels
68
68
+
weight={1} // Flex grow factor (relative sizing)
69
69
+
size={200} // Fixed size in pixels (overrides weight)
70
70
+
margin={10} // Outer spacing
71
71
+
padding={20} // Inner spacing
72
72
+
cornerRadius={6} // Rounded corners
73
73
+
>
74
74
+
{children}
75
75
+
</flex>
76
76
+
```
77
77
+
78
78
+
**Examples:**
79
79
+
80
80
+
```jsx
81
81
+
// Horizontal split (60/40)
82
82
+
<flex direction="horizontal">
83
83
+
<Weather weight={60} />
84
84
+
<Tracker weight={40} />
85
85
+
</flex>
86
86
+
```
87
87
+
88
88
+
```jsx
89
89
+
// Vertical stack with fixed header
90
90
+
<flex direction="vertical">
91
91
+
<flex size={100}>Header</flex>
92
92
+
<flex weight={1}>Content</flex>
93
93
+
</flex>
94
94
+
```
95
95
+
96
96
+
```jsx
97
97
+
// Center content with gap
98
98
+
<flex gap={20} justify="center" direction="vertical">
99
99
+
<text>Line 1</text>
100
100
+
<text>Line 2</text>
101
101
+
</flex>
102
102
+
```
103
103
+
104
104
+
### `<text>` - Text Display
105
105
+
106
106
+
Renders text content with font size control.
107
107
+
108
108
+
```jsx
109
109
+
<text
110
110
+
fontSize={32} // Font size in pixels (default: 32, inherited)
111
111
+
weight={1} // Flex sizing
112
112
+
size={100} // Fixed height
113
113
+
>
114
114
+
Your text here
115
115
+
</text>
116
116
+
```
117
117
+
118
118
+
**Examples:**
119
119
+
120
120
+
```jsx
121
121
+
// Large temperature display
122
122
+
<text fontSize={60}>23°C</text>
123
123
+
```
124
124
+
125
125
+
```jsx
126
126
+
// Small label
127
127
+
<text fontSize={22}>Days Left</text>
128
128
+
```
129
129
+
130
130
+
```jsx
131
131
+
// Dynamic content
132
132
+
<text fontSize={120}>{week}</text>
133
133
+
```
134
134
+
135
135
+
### `<fill>` - Solid Color Block
136
136
+
137
137
+
Renders a solid color rectangle.
138
138
+
139
139
+
```jsx
140
140
+
<fill
141
141
+
color="white|black|gray" // Color (gray = checkerboard pattern)
142
142
+
weight={1} // Flex sizing
143
143
+
size={10} // Fixed size
144
144
+
/>
145
145
+
```
146
146
+
147
147
+
**Examples:**
148
148
+
149
149
+
```jsx
150
150
+
// Progress bar (black filled, gray remainder)
151
151
+
<flex direction="horizontal">
152
152
+
<fill weight={70} color="black" />
153
153
+
<fill weight={30} color="gray" />
154
154
+
</flex>
155
155
+
```
156
156
+
157
157
+
```jsx
158
158
+
// Separator line
159
159
+
<fill size={2} color="black" />
160
160
+
```
161
161
+
162
162
+
```jsx
163
163
+
// Spacer
164
164
+
<fill color="white" />
165
165
+
```
166
166
+
167
167
+
### `<img>` - Image Display
168
168
+
169
169
+
Renders embedded images from the icons directory.
170
170
+
171
171
+
```jsx
172
172
+
<img
173
173
+
src="scrn/icons/path/to/image.png" // Path relative to scrn/icons/
174
174
+
size={256} // Fixed size (width=height)
175
175
+
/>
176
176
+
```
177
177
+
178
178
+
**Examples:**
179
179
+
180
180
+
```jsx
181
181
+
// Weather icon
182
182
+
<img size={256} src="scrn/icons/weather/day/clear.png" />
183
183
+
184
184
+
// Must be in internal/module/scrn/icons/ directory
185
185
+
```
186
186
+
187
187
+
## Data Fetching
188
188
+
189
189
+
### fetch() API
190
190
+
191
191
+
Synchronous HTTP GET using the standard fetch interface:
192
192
+
193
193
+
```jsx
194
194
+
const url = new URL("/v1/search", "https://api.example.com/")
195
195
+
url.searchParams.set("name", location)
196
196
+
url.searchParams.set("count", "10")
197
197
+
198
198
+
const response = fetch(url.toString())
199
199
+
200
200
+
if (response.status === 200) {
201
201
+
const data = response.json() // Parse JSON response
202
202
+
// Use data...
203
203
+
}
204
204
+
```
205
205
+
206
206
+
**Key differences from browser fetch:**
207
207
+
- **Synchronous** - Blocks until response received (no Promises)
208
208
+
- **GET only** - Only HTTP GET requests supported
209
209
+
- `response.json()` returns parsed object directly
210
210
+
- `response.status` for status code checking
211
211
+
212
212
+
### URL Construction
213
213
+
214
214
+
Use the `URL` class for clean query string building:
215
215
+
216
216
+
```jsx
217
217
+
const url = new URL("/v1/forecast", "https://api.open-meteo.com/")
218
218
+
url.searchParams.set("latitude", coords.latitude)
219
219
+
url.searchParams.set("longitude", coords.longitude)
220
220
+
url.searchParams.set("current", "temperature_2m")
221
221
+
222
222
+
fetch(url.toString()) // Converts to full URL string
223
223
+
```
224
224
+
225
225
+
## Caching
226
226
+
227
227
+
### cache API
228
228
+
229
229
+
Simple key-value in-memory cache (resets on server restart):
230
230
+
231
231
+
```jsx
232
232
+
// Check cache first
233
233
+
const cached = cache.get("weather.coordinates")
234
234
+
if (cached) {
235
235
+
return cached
236
236
+
}
237
237
+
238
238
+
// Fetch fresh data
239
239
+
const data = fetchFromAPI()
240
240
+
241
241
+
// Store in cache
242
242
+
cache.set("weather.coordinates", data)
243
243
+
```
244
244
+
245
245
+
**Best practices:**
246
246
+
- Use namespaced keys: `"weather.coordinates"`, `"tracker.data"`
247
247
+
- Cache API responses to reduce network calls
248
248
+
- Store processed/transformed data for reuse
249
249
+
- Cache is shared across renders but not persistent
250
250
+
251
251
+
## Responsive Layout
252
252
+
253
253
+
Modules receive `width` and `height` - use them for responsive behavior:
254
254
+
255
255
+
```jsx
256
256
+
export function PregnancyTracker({width, due}) {
257
257
+
// Default layout for wide displays
258
258
+
let inner = <flex direction="horizontal">
259
259
+
<Meta>
260
260
+
<text fontSize={30}>Week</text>
261
261
+
<text fontSize={120}>{week}</text>
262
262
+
</Meta>
263
263
+
<Meta>
264
264
+
<text fontSize={30}>Days Left</text>
265
265
+
<text fontSize={120}>{left}</text>
266
266
+
</Meta>
267
267
+
</flex>
268
268
+
269
269
+
// Compact layout for narrow displays
270
270
+
if (width < 500) {
271
271
+
inner = <flex direction="vertical" gap={20}>
272
272
+
<Meta size={140}>
273
273
+
<text fontSize={30}>Week</text>
274
274
+
<text fontSize={120}>{week}</text>
275
275
+
</Meta>
276
276
+
<Meta size={100}>
277
277
+
<text fontSize={26}>Days Left</text>
278
278
+
<text fontSize={60}>{left}</text>
279
279
+
</Meta>
280
280
+
</flex>
281
281
+
}
282
282
+
283
283
+
return <flex direction="vertical">
284
284
+
{inner}
285
285
+
<ProgressBar percentage={percentage} />
286
286
+
</flex>
287
287
+
}
288
288
+
```
289
289
+
290
290
+
## Importing and Composing
291
291
+
292
292
+
### Import Built-in Modules
293
293
+
294
294
+
```jsx
295
295
+
// Default import
296
296
+
import Weather from "scrn/weather"
297
297
+
298
298
+
// Named exports
299
299
+
import { ProgressBar, Meta } from "scrn/utils"
300
300
+
import { PregnancyTracker } from "scrn/tracker"
301
301
+
```
302
302
+
303
303
+
### Using Imported Components
304
304
+
305
305
+
Pass props like any JSX component:
306
306
+
307
307
+
```jsx
308
308
+
<ProgressBar percentage={0.75} size={60} cornerRadius={6} />
309
309
+
310
310
+
<Meta>
311
311
+
<text fontSize={30}>Label</text>
312
312
+
<text fontSize={120}>Value</text>
313
313
+
</Meta>
314
314
+
```
315
315
+
316
316
+
### Creating Reusable Components
317
317
+
318
318
+
Export utilities for other modules to use:
319
319
+
320
320
+
```jsx
321
321
+
// In scrn/utils.jsx
322
322
+
export function ProgressBar({percentage}) {
323
323
+
const filled = parseInt(percentage * 100, 10)
324
324
+
const empty = 100 - filled
325
325
+
326
326
+
return <flex direction="horizontal">
327
327
+
<fill weight={filled} color="black" />
328
328
+
<fill weight={empty} color="gray" />
329
329
+
</flex>
330
330
+
}
331
331
+
332
332
+
export function Meta({children}) {
333
333
+
return <flex direction="horizontal">
334
334
+
<fill size={10} color="gray" />
335
335
+
<flex padding={10} justify="center" direction="vertical">
336
336
+
{children}
337
337
+
</flex>
338
338
+
</flex>
339
339
+
}
340
340
+
```
341
341
+
342
342
+
## Common Patterns
343
343
+
344
344
+
### Data Fetching with Cache
345
345
+
346
346
+
```jsx
347
347
+
function getData(key, apiUrl) {
348
348
+
const cached = cache.get(key)
349
349
+
if (cached) {
350
350
+
return cached
351
351
+
}
352
352
+
353
353
+
const response = fetch(apiUrl)
354
354
+
if (response.status === 200) {
355
355
+
const data = response.json()
356
356
+
cache.set(key, data)
357
357
+
return data
358
358
+
}
359
359
+
360
360
+
return null
361
361
+
}
362
362
+
363
363
+
export default function MyModule({location}) {
364
364
+
const data = getData("mymodule.data", "https://api.example.com/data")
365
365
+
366
366
+
if (!data) {
367
367
+
return <text>Data not available</text>
368
368
+
}
369
369
+
370
370
+
return <flex>
371
371
+
{/* Render data */}
372
372
+
</flex>
373
373
+
}
374
374
+
```
375
375
+
376
376
+
### Error States
377
377
+
378
378
+
Handle missing data gracefully:
379
379
+
380
380
+
```jsx
381
381
+
export default function Weather({location}) {
382
382
+
const coords = getCoordinates(location)
383
383
+
384
384
+
let inner = <text>Coordinates not found!</text>
385
385
+
386
386
+
if (coords) {
387
387
+
const weather = getWeather(coords)
388
388
+
389
389
+
if (weather) {
390
390
+
inner = <text fontSize={60}>{weather.temp}°C</text>
391
391
+
} else {
392
392
+
inner = <text>Weather data not found!</text>
393
393
+
}
394
394
+
}
395
395
+
396
396
+
return <flex>{inner}</flex>
397
397
+
}
398
398
+
```
399
399
+
400
400
+
### Calculations and Formatting
401
401
+
402
402
+
```jsx
403
403
+
function toDays(millis) {
404
404
+
return parseInt(millis / 1000 / 60 / 60 / 24, 10)
405
405
+
}
406
406
+
407
407
+
export default function Tracker({start, end}) {
408
408
+
const s = Date.parse(start)
409
409
+
const e = Date.parse(end)
410
410
+
const n = Date.now()
411
411
+
412
412
+
const left = toDays(e - n)
413
413
+
const passed = toDays(n - s)
414
414
+
const percentage = (n - s) / (e - s)
415
415
+
416
416
+
return <flex>
417
417
+
<text>{left} days left</text>
418
418
+
<ProgressBar percentage={percentage} />
419
419
+
</flex>
420
420
+
}
421
421
+
```
422
422
+
423
423
+
### Layout Composition
424
424
+
425
425
+
```jsx
426
426
+
export default function Dashboard() {
427
427
+
return <flex direction="horizontal" separator="dashed">
428
428
+
{/* Left side: 60% width */}
429
429
+
<flex weight={60} padding={20} direction="vertical">
430
430
+
<flex size={256}>
431
431
+
<img size={256} src="scrn/icons/weather/day/clear.png" />
432
432
+
</flex>
433
433
+
<flex>
434
434
+
<text fontSize={60}>23°C</text>
435
435
+
</flex>
436
436
+
</flex>
437
437
+
438
438
+
{/* Right side: 40% width */}
439
439
+
<flex weight={40} padding={20} direction="vertical">
440
440
+
<ProgressBar percentage={0.65} />
441
441
+
</flex>
442
442
+
</flex>
443
443
+
}
444
444
+
```
445
445
+
446
446
+
## Module Development Workflow
447
447
+
448
448
+
### 1. Create the JSX file
449
449
+
450
450
+
Add to `internal/module/scrn/yourmodule.jsx`:
451
451
+
452
452
+
```jsx
453
453
+
export default function YourModule({width, height, customProp}) {
454
454
+
return <flex direction="vertical">
455
455
+
<text>Hello from {customProp}</text>
456
456
+
</flex>
457
457
+
}
458
458
+
```
459
459
+
460
460
+
### 2. Compile the module
461
461
+
462
462
+
```bash
463
463
+
cd internal/module
464
464
+
go generate ./...
465
465
+
```
466
466
+
467
467
+
This transforms JSX to JS and embeds it in the Go binary.
468
468
+
469
469
+
### 3. Use in display.jsx
470
470
+
471
471
+
```jsx
472
472
+
import YourModule from "scrn/yourmodule"
473
473
+
474
474
+
export default function Display() {
475
475
+
return <YourModule customProp="test" />
476
476
+
}
477
477
+
```
478
478
+
479
479
+
### 4. Run and test
480
480
+
481
481
+
```bash
482
482
+
go run .
483
483
+
```
484
484
+
485
485
+
Visit `http://localhost:8081/api/image.bmp` to see the rendered output.
486
486
+
487
487
+
## Design Guidelines
488
488
+
489
489
+
### Visual Hierarchy
490
490
+
491
491
+
- Use font sizes to establish hierarchy: 120px (primary), 60px (secondary), 30px (labels), 22px (small text)
492
492
+
- Use `weight` for proportional sizing, `size` for fixed measurements
493
493
+
- Add `padding` for breathing room, `margin` for separation
494
494
+
- Use `separator="dashed"` for visual division
495
495
+
496
496
+
### E-ink Optimization
497
497
+
498
498
+
- **Black and white only** - Gray uses a checkerboard pattern (dithering)
499
499
+
- **High contrast** - Ensure text is readable with large font sizes
500
500
+
- **Simple layouts** - Avoid fine details, they won't render well
501
501
+
- **Large touch targets** - Though this is display-only, consider future interaction
502
502
+
503
503
+
### Performance
504
504
+
505
505
+
- **Cache API calls** - Reduces render time and network load
506
506
+
- **Minimize fetch calls** - Batch data or reuse cached responses
507
507
+
- **Simple calculations** - Complex logic slows rendering
508
508
+
509
509
+
### Error Handling
510
510
+
511
511
+
- **Always handle null/missing data** - APIs can fail
512
512
+
- **Provide fallback UI** - Show error messages, don't crash
513
513
+
- **Test with offline data** - Use cache.get() as fallback
514
514
+
515
515
+
## Examples from Built-in Modules
516
516
+
517
517
+
### Simple Utility (ProgressBar)
518
518
+
519
519
+
```jsx
520
520
+
export function ProgressBar({percentage}) {
521
521
+
const p = parseInt(percentage * 100, 10)
522
522
+
const r = 100 - p
523
523
+
524
524
+
return <flex direction="horizontal">
525
525
+
<fill weight={p} color="black" />
526
526
+
<fill weight={r} color="gray" />
527
527
+
</flex>
528
528
+
}
529
529
+
```
530
530
+
531
531
+
**Lessons:**
532
532
+
- Single responsibility: just draws a progress bar
533
533
+
- No data fetching, pure presentation
534
534
+
- Exported as named export for composition
535
535
+
536
536
+
### API Integration (Weather)
537
537
+
538
538
+
```jsx
539
539
+
function getCoordinates(location) {
540
540
+
const coords = cache.get("weather.coordinates")
541
541
+
if (coords) return coords
542
542
+
543
543
+
const url = new URL("/v1/search", "https://geocoding-api.open-meteo.com/")
544
544
+
url.searchParams.set("name", location)
545
545
+
546
546
+
const ret = fetch(url.toString())
547
547
+
if (ret.status === 200) {
548
548
+
const data = ret.json()
549
549
+
const coords = {
550
550
+
latitude: data.results[0].latitude,
551
551
+
longitude: data.results[0].longitude,
552
552
+
}
553
553
+
cache.set("weather.coordinates", coords)
554
554
+
return coords
555
555
+
}
556
556
+
557
557
+
return null
558
558
+
}
559
559
+
560
560
+
export default function Weather({width, height, location}) {
561
561
+
const coords = getCoordinates(location)
562
562
+
563
563
+
let inner = <text>Coordinates not found!</text>
564
564
+
565
565
+
if (coords) {
566
566
+
const weather = getWeather(coords)
567
567
+
if (weather) {
568
568
+
const temp = Math.round(weather.current.temperature_2m)
569
569
+
inner = <text fontSize={60}>{temp}°C</text>
570
570
+
}
571
571
+
}
572
572
+
573
573
+
return <flex direction="vertical">
574
574
+
<flex direction="horizontal">
575
575
+
<img size={256} src="scrn/icons/weather/day/clear.png" />
576
576
+
{inner}
577
577
+
</flex>
578
578
+
</flex>
579
579
+
}
580
580
+
```
581
581
+
582
582
+
**Lessons:**
583
583
+
- Helper functions for data fetching
584
584
+
- Cache layer for coordinates (static data)
585
585
+
- Error handling with fallback UI
586
586
+
- Nested flex layouts for positioning
587
587
+
588
588
+
### Responsive Component (PregnancyTracker)
589
589
+
590
590
+
```jsx
591
591
+
export function PregnancyTracker({width, due}) {
592
592
+
const delta = calculateDelta(due)
593
593
+
const week = formatWeek(delta)
594
594
+
const left = calculateDaysLeft(due)
595
595
+
596
596
+
// Default: horizontal layout
597
597
+
let inner = <flex direction="horizontal">
598
598
+
<Meta>
599
599
+
<text fontSize={30}>Week</text>
600
600
+
<text fontSize={120}>{week}</text>
601
601
+
</Meta>
602
602
+
<Meta>
603
603
+
<text fontSize={30}>Days Left</text>
604
604
+
<text fontSize={120}>{left}</text>
605
605
+
</Meta>
606
606
+
</flex>
607
607
+
608
608
+
// Narrow: vertical layout with smaller fonts
609
609
+
if (width < 500) {
610
610
+
inner = <flex direction="vertical" gap={20}>
611
611
+
<Meta>
612
612
+
<text fontSize={30}>Week</text>
613
613
+
<text fontSize={120}>{week}</text>
614
614
+
</Meta>
615
615
+
<Meta>
616
616
+
<text fontSize={26}>Days Left</text>
617
617
+
<text fontSize={60}>{left}</text>
618
618
+
</Meta>
619
619
+
</flex>
620
620
+
}
621
621
+
622
622
+
return <flex direction="vertical">
623
623
+
{inner}
624
624
+
<ProgressBar percentage={percentage} />
625
625
+
</flex>
626
626
+
}
627
627
+
```
628
628
+
629
629
+
**Lessons:**
630
630
+
- Width-based responsive behavior
631
631
+
- Conditional layout (horizontal vs vertical)
632
632
+
- Reusable components (Meta, ProgressBar)
633
633
+
- Font size scaling for different layouts
634
634
+
635
635
+
## Tips and Gotchas
636
636
+
637
637
+
### ✅ Do
638
638
+
639
639
+
- **Cache API responses** for better performance
640
640
+
- **Handle errors gracefully** with fallback UI
641
641
+
- **Use URL class** for clean query string building
642
642
+
- **Test responsive behavior** at different widths
643
643
+
- **Export reusable utilities** for other modules
644
644
+
- **Use `weight` for flexible sizing**, `size` for fixed measurements
645
645
+
- **Round numbers** before display: `Math.round(value)`
646
646
+
- **Check response status** before parsing: `if (ret.status === 200)`
647
647
+
648
648
+
### ❌ Don't
649
649
+
650
650
+
- **Use async/await** - fetch() is synchronous
651
651
+
- **Return Promises** - everything is sync
652
652
+
- **Mutate props** - treat them as read-only
653
653
+
- **Assume APIs work** - always handle errors
654
654
+
- **Use tiny fonts** - e-ink needs high contrast
655
655
+
- **Forget to recompile** - run `go generate` after JSX changes
656
656
+
- **Use complex images** - 1-bit color only
657
657
+
- **Skip caching** - avoid redundant network calls
658
658
+
659
659
+
## Troubleshooting
660
660
+
661
661
+
### Module not found
662
662
+
663
663
+
```
664
664
+
Error: module "scrn/mymodule" not found
665
665
+
```
666
666
+
667
667
+
**Fix:** Run `go generate ./...` in `internal/module/` to compile JSX files.
668
668
+
669
669
+
### White screen / nothing renders
670
670
+
671
671
+
**Check:**
672
672
+
- Does your module return JSX? (not null, not undefined)
673
673
+
- Are prop names spelled correctly?
674
674
+
- Check server logs for JavaScript errors
675
675
+
676
676
+
### fetch() fails silently
677
677
+
678
678
+
**Fix:** Always check `response.status` before using data:
679
679
+
680
680
+
```jsx
681
681
+
const ret = fetch(url)
682
682
+
if (ret.status !== 200) {
683
683
+
console.log("Failed to fetch:", ret.status)
684
684
+
return null
685
685
+
}
686
686
+
```
687
687
+
688
688
+
### Layout looks wrong
689
689
+
690
690
+
**Check:**
691
691
+
- Are you using `weight` vs `size` correctly?
692
692
+
- Is `direction` set on parent `<flex>`?
693
693
+
- Did you forget `padding`/`margin`?
694
694
+
- Use `justify="center"` for centering
695
695
+
696
696
+
### Image not found
697
697
+
698
698
+
**Ensure:**
699
699
+
- Image is in `internal/module/scrn/icons/` directory
700
700
+
- Path starts with `scrn/icons/` (not `/scrn/icons/`)
701
701
+
- Server was recompiled after adding image
702
702
+
703
703
+
## Advanced Patterns
704
704
+
705
705
+
### Multi-level Caching
706
706
+
707
707
+
```jsx
708
708
+
function getDataWithFallback(key, apiUrl) {
709
709
+
// Try cache first
710
710
+
const cached = cache.get(key)
711
711
+
if (cached && cached.timestamp > Date.now() - 3600000) {
712
712
+
return cached.data
713
713
+
}
714
714
+
715
715
+
// Try API
716
716
+
const response = fetch(apiUrl)
717
717
+
if (response.status === 200) {
718
718
+
const data = response.json()
719
719
+
cache.set(key, {
720
720
+
data: data,
721
721
+
timestamp: Date.now()
722
722
+
})
723
723
+
return data
724
724
+
}
725
725
+
726
726
+
// Fallback to stale cache
727
727
+
if (cached) {
728
728
+
return cached.data
729
729
+
}
730
730
+
731
731
+
return null
732
732
+
}
733
733
+
```
734
734
+
735
735
+
### Conditional Component Trees
736
736
+
737
737
+
```jsx
738
738
+
export default function Display({mode}) {
739
739
+
const components = {
740
740
+
weather: <Weather location="Vienna" />,
741
741
+
tracker: <PregnancyTracker due="2026-07-05" />,
742
742
+
both: <flex direction="horizontal">
743
743
+
<Weather weight={60} />
744
744
+
<PregnancyTracker weight={40} />
745
745
+
</flex>
746
746
+
}
747
747
+
748
748
+
return components[mode] || components.both
749
749
+
}
750
750
+
```
751
751
+
752
752
+
### Dynamic Layouts
753
753
+
754
754
+
```jsx
755
755
+
function calculateLayout(items, width) {
756
756
+
const itemsPerRow = width > 600 ? 3 : 2
757
757
+
const rows = []
758
758
+
759
759
+
for (let i = 0; i < items.length; i += itemsPerRow) {
760
760
+
rows.push(items.slice(i, i + itemsPerRow))
761
761
+
}
762
762
+
763
763
+
return rows
764
764
+
}
765
765
+
766
766
+
export default function Grid({items, width}) {
767
767
+
const rows = calculateLayout(items, width)
768
768
+
769
769
+
return <flex direction="vertical">
770
770
+
{rows.map(row => (
771
771
+
<flex direction="horizontal">
772
772
+
{row.map(item => (
773
773
+
<flex weight={1}>
774
774
+
<text>{item}</text>
775
775
+
</flex>
776
776
+
))}
777
777
+
</flex>
778
778
+
))}
779
779
+
</flex>
780
780
+
}
781
781
+
```
782
782
+
783
783
+
## Summary
784
784
+
785
785
+
Writing modules for scrn involves:
786
786
+
787
787
+
1. **Create JSX component** with `export default function`
788
788
+
2. **Receive props** including `width`, `height`, and custom props
789
789
+
3. **Fetch data** using synchronous `fetch()` and `cache` APIs
790
790
+
4. **Build layout** using `<flex>`, `<text>`, `<fill>`, `<img>`
791
791
+
5. **Handle errors** with fallback UI
792
792
+
6. **Return JSX tree** from component function
793
793
+
7. **Compile with** `go generate ./...`
794
794
+
8. **Import and use** in display.jsx
795
795
+
796
796
+
The flexibility of JSX combined with Go's runtime creates a powerful system for building dynamic, data-driven e-ink displays.
+221
AGENTS.md
···
1
1
+
# AGENTS.md - Working with the scrn codebase
2
2
+
3
3
+
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.
4
4
+
5
5
+
## Project Overview
6
6
+
7
7
+
- **Language**: Go 1.23.3
8
8
+
- **Module**: `tangled.org/cdbrdr.com/scrn`
9
9
+
- **Purpose**: TRMNL-compatible display server that renders JSX layouts to BMP images
10
10
+
- **Display**: 800x480, 1-bit color (black/white), outputs BMP format
11
11
+
12
12
+
## Essential Commands
13
13
+
14
14
+
```bash
15
15
+
# Build the binary
16
16
+
go build -o scrn
17
17
+
18
18
+
# Run the server (uses examples/display.jsx by default)
19
19
+
go run .
20
20
+
21
21
+
# Run tests
22
22
+
go test ./...
23
23
+
go test ./internal/tree/... # specific package
24
24
+
25
25
+
# Regenerate pre-compiled JS modules (run after modifying JSX files)
26
26
+
cd internal/module
27
27
+
go generate ./...
28
28
+
29
29
+
# Download dependencies
30
30
+
go mod download
31
31
+
go mod tidy
32
32
+
```
33
33
+
34
34
+
## Project Structure
35
35
+
36
36
+
```
37
37
+
.
38
38
+
├── main.go # Entry point: loads display.jsx, starts HTTP server on :8081
39
39
+
├── internal/
40
40
+
│ ├── display/
41
41
+
│ │ ├── display.go # Display struct: manages goja runtime, renders BMP
42
42
+
│ │ └── draw.go # Rendering: draws nodes to image (uses gofont, masks)
43
43
+
│ ├── handler/
44
44
+
│ │ └── api.go # HTTP handlers: /api/setup, /api/display, /api/image.bmp
45
45
+
│ ├── tree/
46
46
+
│ │ ├── tree.go # Node interface, CalculateLayout, style parsing
47
47
+
│ │ ├── node.go # parseNode: converts JSX output to internal nodes
48
48
+
│ │ ├── module.go # Module nodes: calls JSX functions recursively
49
49
+
│ │ ├── flex.go # FlexNode: flexbox layout (horizontal/vertical)
50
50
+
│ │ ├── text.go # TextNode: text rendering with font size inheritance
51
51
+
│ │ ├── fill.go # FillNode: solid color fills (black/white/gray)
52
52
+
│ │ ├── img.go # ImgNode: embedded image rendering
53
53
+
│ │ └── tree_test.go # Tests using deep.Equal for struct comparison
54
54
+
│ ├── module/
55
55
+
│ │ ├── module.go # go:embed for JSX files, module loader
56
56
+
│ │ ├── jsx/
57
57
+
│ │ │ └── jsx-runtime.js # JSX transform runtime (h/jsx/jsxs functions)
58
58
+
│ │ ├── scrn/
59
59
+
│ │ │ ├── *.jsx # Built-in modules (weather, tracker, utils)
60
60
+
│ │ │ └── icons/ # PNG/JPG assets for weather icons
61
61
+
│ │ ├── fetch/
62
62
+
│ │ │ └── fetch.go # fetch() polyfill for goja (HTTP GET)
63
63
+
│ │ └── cache/
64
64
+
│ │ └── cache.go # Simple in-memory cache for goja
65
65
+
│ └── transform/
66
66
+
│ └── transform.go # esbuild wrapper: transforms JSX -> JS (IIFE/CJS)
67
67
+
└── examples/
68
68
+
└── display.jsx # Example main component
69
69
+
```
70
70
+
71
71
+
## Architecture Flow
72
72
+
73
73
+
1. **main.go** loads `examples/display.jsx` and creates a `Display`
74
74
+
2. **transform** converts JSX to JS using esbuild with automatic JSX runtime
75
75
+
3. **display** creates a goja runtime, registers modules (fetch, cache, console, url)
76
76
+
4. **JSX components** execute and return a tree structure (via jsx-runtime.js)
77
77
+
5. **tree.CalculateLayout** parses the tree into typed nodes (FlexNode, TextNode, etc.)
78
78
+
6. **Layout engine** calculates bounds using flexbox algorithm with weights/sizes
79
79
+
7. **draw.go** renders nodes to `image.Paletted` (black/white palette)
80
80
+
8. **Output** is BMP with TRMNL-specific header patches
81
81
+
82
82
+
## JSX/Component System
83
83
+
84
84
+
### Built-in Node Types (from jsx-runtime.js)
85
85
+
86
86
+
```jsx
87
87
+
// Flex container - arranges children horizontally or vertically
88
88
+
<flex direction="horizontal|vertical" separator="none|solid|dashed" justify="start|end|center" gap={10}>
89
89
+
90
90
+
// Text node - content from children, supports fontSize
91
91
+
<text fontSize={32}>Hello World</text>
92
92
+
93
93
+
// Fill - solid color block
94
94
+
<fill color="white|black|gray" />
95
95
+
96
96
+
// Image - from embedded assets
97
97
+
<img src="scrn/icons/weather/day/clear.png" />
98
98
+
```
99
99
+
100
100
+
### Style Properties (all nodes)
101
101
+
102
102
+
- `weight` - Flex grow factor (default: 1)
103
103
+
- `size` - Fixed pixel size (overrides weight)
104
104
+
- `margin` - Outer margin in pixels
105
105
+
- `padding` - Inner padding in pixels
106
106
+
- `cornerRadius` - For rounded corners (via mask)
107
107
+
- `fontSize` - Inherited font size for text (default: 32)
108
108
+
109
109
+
### Writing Modules
110
110
+
111
111
+
Modules are JSX files in `internal/module/scrn/`:
112
112
+
113
113
+
```jsx
114
114
+
// Import other modules
115
115
+
import { ProgressBar } from "scrn/utils"
116
116
+
117
117
+
// Export default function - receives props including width/height
118
118
+
export default function Weather({width, height, location}) {
119
119
+
// Use fetch() to get data
120
120
+
const response = fetch(`https://api.example.com/data`)
121
121
+
122
122
+
// Use cache for persistence between renders
123
123
+
cache.set("key", value)
124
124
+
const cached = cache.get("key")
125
125
+
126
126
+
// Return JSX tree
127
127
+
return <flex direction="vertical">
128
128
+
<text fontSize={60}>{temp}°C</text>
129
129
+
</flex>
130
130
+
}
131
131
+
```
132
132
+
133
133
+
### Key Rules
134
134
+
135
135
+
- Components **must** export default a function
136
136
+
- The function receives `width`, `height`, and any props from parent
137
137
+
- `fetch()` is synchronous (blocking) in this runtime
138
138
+
- `cache` is in-memory only (resets on server restart)
139
139
+
- Images must be in `scrn/icons/` path (embedded at build time)
140
140
+
141
141
+
## Code Patterns
142
142
+
143
143
+
### Adding a New Node Type
144
144
+
145
145
+
1. Define `NodeType` constant in `tree/tree.go`
146
146
+
2. Create struct in `tree/newtype.go` with:
147
147
+
- Type-specific fields
148
148
+
- `Type() NodeType` method
149
149
+
- `Bounds() image.Rectangle` method
150
150
+
- `GetStyle() NodeStyle` method
151
151
+
3. Add case to `node.go`'s `GetNode()` switch
152
152
+
4. Add renderer in `draw.go`'s `drawNode()` switch
153
153
+
154
154
+
### Adding a Built-in Module
155
155
+
156
156
+
1. Create `.jsx` file in `internal/module/scrn/`
157
157
+
2. Run `go generate ./...` in `internal/module` to compile
158
158
+
3. Import in display files as `import X from "scrn/filename"`
159
159
+
160
160
+
### Error Handling
161
161
+
162
162
+
- Go errors are defined as vars: `ErrNoDefaultExport = errors.New("...")`
163
163
+
- Goja (JS) errors use `runtime.NewGoError(err)`
164
164
+
- HTTP handlers log errors and return 500 status
165
165
+
166
166
+
## Testing
167
167
+
168
168
+
Tests use `github.com/go-test/deep` for struct comparison:
169
169
+
170
170
+
```go
171
171
+
func TestSomething(t *testing.T) {
172
172
+
result, err := CalculateLayout(input, rect, nil)
173
173
+
expected := &FlexNode{...}
174
174
+
175
175
+
if diff := deep.Equal(result, expected); diff != nil {
176
176
+
t.Error(diff)
177
177
+
}
178
178
+
}
179
179
+
```
180
180
+
181
181
+
Run with: `go test ./internal/tree/...`
182
182
+
183
183
+
## Important Gotchas
184
184
+
185
185
+
1. **JSX Compilation**: JSX files are transformed at build time via `go generate`. The generated `.js` files are embedded and ignored in `.gitignore`.
186
186
+
187
187
+
2. **Synchronous fetch**: The `fetch()` implementation is blocking/synchronous, not Promise-based like browser fetch.
188
188
+
189
189
+
3. **BMP Header Patching**: The BMP output has hardcoded header patches for TRMNL compatibility (bytes 46, 57, 61).
190
190
+
191
191
+
4. **Font Size Inheritance**: `fontSize` is inherited from parent nodes. The default is 32px.
192
192
+
193
193
+
5. **Flex Layout Algorithm**:
194
194
+
- If `size` is set, it's used as fixed pixels
195
195
+
- Otherwise `weight` determines proportional sizing of remaining space
196
196
+
- Weights are normalized across siblings
197
197
+
198
198
+
6. **Module Resolution**: Modules are loaded from `node_modules/scrn/*` path in the goja runtime, mapped to embedded files.
199
199
+
200
200
+
7. **Goja Runtime**: Each Display has its own isolated JS runtime. Modules share the same runtime within a Display.
201
201
+
202
202
+
8. **Image Assets**: Only images in `internal/module/scrn/icons/` are available. They're embedded at build time via `//go:embed`.
203
203
+
204
204
+
9. **Color Palette**: Only black, white, and gray. Gray is rendered as a checkerboard pattern.
205
205
+
206
206
+
## Dependencies to Know
207
207
+
208
208
+
- **goja**: JavaScript runtime for Go (ECMAScript 5.1+)
209
209
+
- **esbuild**: Bundles/transforms JSX (used as Go library)
210
210
+
- **gin**: HTTP web framework
211
211
+
- **gofont**: Font rendering for Go images
212
212
+
- **mapstructure**: Decodes map[string]any into structs (for JSX props)
213
213
+
214
214
+
## API Endpoints (TRMNL-compatible)
215
215
+
216
216
+
- `GET /api/setup` - Returns device registration info
217
217
+
- `GET /api/display` - Returns display metadata with image URL
218
218
+
- `GET /api/image.bmp` - Returns rendered BMP image
219
219
+
- `POST /api/log` - Receives device logs
220
220
+
221
221
+
The server mimics TRMNL's cloud API for local e-ink display testing.
+4
-3
examples/display.jsx
···
1
1
import Weather from "scrn/weather"
2
2
-
import {PregnancyTracker} from "scrn/tracker"
2
2
+
import GitHub from "scrn/github"
3
3
+
import {PregnancyTracker, AgeTracker} from "scrn/tracker"
3
4
4
5
export default function Display() {
5
6
return <flex direction="horizontal" separator="dashed">
6
6
-
<Weather weight={55} padding={20} location="Vienna, Austria" />
7
7
-
<PregnancyTracker weight={45} padding={20} due="2025-07-05" />
7
7
+
<Weather weight={60} padding={20} location="Vienna, Austria" />
8
8
+
<AgeTracker weight={40} padding={20} name="Joe" birthday="2025-10-08" />
8
9
</flex>
9
10
}
+2
go.mod
···
11
11
github.com/go-viper/mapstructure/v2 v2.2.1
12
12
github.com/gonutz/gofont v1.0.0
13
13
github.com/sergeymakinen/go-bmp v1.0.0
14
14
+
go.oneofone.dev/resize v1.0.1
14
15
)
15
16
16
17
require (
···
39
40
github.com/ugorji/go/codec v1.2.12 // indirect
40
41
golang.org/x/arch v0.8.0 // indirect
41
42
golang.org/x/crypto v0.25.0 // indirect
43
43
+
golang.org/x/image v0.1.0 // indirect
42
44
golang.org/x/net v0.27.0 // indirect
43
45
golang.org/x/sys v0.22.0 // indirect
44
46
golang.org/x/text v0.21.0 // indirect
+28
go.sum
···
89
89
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
90
90
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
91
91
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
92
92
+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
93
93
+
go.oneofone.dev/resize v1.0.1 h1:HjpVar/4pxMGrjO44ThaMX1Q5UOBw0KxzbxxRDZPQuA=
94
94
+
go.oneofone.dev/resize v1.0.1/go.mod h1:zGFmn7q4EUZVlnDmxqf+b0mWpxsTt0MH2yx6ng8tpq0=
92
95
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
93
96
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
94
97
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
98
98
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
99
99
+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
95
100
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
96
101
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
102
102
+
golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
103
103
+
golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
104
104
+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
105
105
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
106
106
+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
107
107
+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
97
108
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
98
109
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
110
110
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
111
111
+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
112
112
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
113
113
+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
114
114
+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
115
115
+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99
116
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
117
117
+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
100
118
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
101
119
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
102
120
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
103
121
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
122
122
+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
123
123
+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
124
124
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
125
125
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
126
126
+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
127
127
+
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
104
128
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
105
129
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
130
130
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
131
131
+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
132
132
+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
133
133
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
106
134
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
107
135
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
108
136
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+13
-2
internal/display/draw.go
···
10
10
"strings"
11
11
12
12
"github.com/gonutz/gofont"
13
13
+
"go.oneofone.dev/resize"
13
14
"tangled.org/cdbrdr.com/scrn/internal/display/util"
14
15
"tangled.org/cdbrdr.com/scrn/internal/module"
15
16
"tangled.org/cdbrdr.com/scrn/internal/tree"
···
65
66
return ErrImgSrcNotFound
66
67
}
67
68
69
69
+
width := n.Rect.Dx()
70
70
+
height := n.Rect.Dy()
71
71
+
if width > height {
72
72
+
src = resize.Resize(uint(width), 0, src, resize.MitchellNetravali)
73
73
+
} else if height > width {
74
74
+
src = resize.Resize(0, uint(height), src, resize.MitchellNetravali)
75
75
+
} else {
76
76
+
src = resize.Resize(uint(width), uint(height), src, resize.MitchellNetravali)
77
77
+
}
78
78
+
68
79
srect := src.Bounds()
69
80
sp := image.Pt(
70
70
-
n.Rect.Min.X+(n.Rect.Dx()/2)-(srect.Dx()/2),
71
71
-
n.Rect.Min.Y+(n.Rect.Dy()/2)-(srect.Dy()/2),
81
81
+
n.Rect.Min.X+(width/2)-(srect.Dx()/2),
82
82
+
n.Rect.Min.Y+(height/2)-(srect.Dy()/2),
72
83
)
73
84
sr := image.Rect(
74
85
sp.X,
+115
internal/module/scrn/github.jsx
···
1
1
+
function getGitHubUser(username) {
2
2
+
const cacheKey = "github.user." + username
3
3
+
const cached = cache.get(cacheKey)
4
4
+
5
5
+
if (cached) {
6
6
+
return cached
7
7
+
}
8
8
+
9
9
+
const url = "https://api.github.com/users/" + username
10
10
+
const response = fetch(url)
11
11
+
12
12
+
if (response.status === 200) {
13
13
+
const data = response.json()
14
14
+
cache.set(cacheKey, data)
15
15
+
return data
16
16
+
}
17
17
+
18
18
+
if (response.status === 404) {
19
19
+
return { error: "User not found" }
20
20
+
}
21
21
+
22
22
+
if (response.status === 403) {
23
23
+
return { error: "API rate limit exceeded" }
24
24
+
}
25
25
+
26
26
+
return { error: "Failed to fetch user data" }
27
27
+
}
28
28
+
29
29
+
function formatNumber(num) {
30
30
+
if (num >= 1000000) {
31
31
+
return (num / 1000000).toFixed(1) + "M"
32
32
+
}
33
33
+
if (num >= 1000) {
34
34
+
return (num / 1000).toFixed(1) + "k"
35
35
+
}
36
36
+
return num.toString()
37
37
+
}
38
38
+
39
39
+
function StatBox({label, value, compact}) {
40
40
+
if (compact) {
41
41
+
return <flex direction="vertical" justify="space-between">
42
42
+
<text fontSize={18}>{label}</text>
43
43
+
<text fontSize={24}>{value}</text>
44
44
+
</flex>
45
45
+
}
46
46
+
return <flex direction="vertical" justify="center">
47
47
+
<text fontSize={22}>{label}</text>
48
48
+
<text fontSize={48}>{value}</text>
49
49
+
</flex>
50
50
+
}
51
51
+
52
52
+
export default function GitHub({width, height, username}) {
53
53
+
if (!username) {
54
54
+
return <flex direction="vertical" justify="center">
55
55
+
<text fontSize={32}>No username provided</text>
56
56
+
</flex>
57
57
+
}
58
58
+
59
59
+
const user = getGitHubUser(username)
60
60
+
61
61
+
if (user.error) {
62
62
+
return <flex direction="vertical" justify="center">
63
63
+
<text fontSize={32}>GitHub Error</text>
64
64
+
<text fontSize={22}>{user.error}</text>
65
65
+
</flex>
66
66
+
}
67
67
+
68
68
+
const displayName = user.name || user.login
69
69
+
const repos = formatNumber(user.public_repos || 0)
70
70
+
const followers = formatNumber(user.followers || 0)
71
71
+
const following = formatNumber(user.following || 0)
72
72
+
73
73
+
// Check if narrow layout (50% or less of 800px screen)
74
74
+
const isNarrow = width <= 400
75
75
+
76
76
+
if (isNarrow) {
77
77
+
return <flex direction="vertical" padding={16} gap={12}>
78
78
+
<fill size={20} color="white" />
79
79
+
80
80
+
<flex size={50} direction="vertical" justify="center">
81
81
+
<text fontSize={28}>{displayName}</text>
82
82
+
<text fontSize={16}>@{user.login}</text>
83
83
+
</flex>
84
84
+
85
85
+
<fill size={1} color="gray" />
86
86
+
87
87
+
<flex weight={1} direction="vertical" gap={30}>
88
88
+
<StatBox label="Repositories" value={repos} compact={true} />
89
89
+
<StatBox label="Followers" value={followers} compact={true} />
90
90
+
<StatBox label="Following" value={following} compact={true} />
91
91
+
</flex>
92
92
+
93
93
+
<fill size={20} color="white" />
94
94
+
</flex>
95
95
+
}
96
96
+
97
97
+
return <flex direction="vertical" padding={20} gap={20}>
98
98
+
<fill size={20} color="white" />
99
99
+
100
100
+
<flex size={80} direction="vertical" justify="center">
101
101
+
<text fontSize={48}>{displayName}</text>
102
102
+
<text fontSize={22}>@{user.login}</text>
103
103
+
</flex>
104
104
+
105
105
+
<fill size={2} color="gray" />
106
106
+
107
107
+
<flex weight={1} direction="horizontal" gap={20} justify="center">
108
108
+
<StatBox label="Repositories" value={repos} />
109
109
+
<StatBox label="Followers" value={followers} />
110
110
+
<StatBox label="Following" value={following} />
111
111
+
</flex>
112
112
+
113
113
+
<fill size={60} color="white" />
114
114
+
</flex>
115
115
+
}
+86
internal/module/scrn/tracker.jsx
···
57
57
</flex>
58
58
}
59
59
60
60
+
export function AgeTracker({width, name, birthday}) {
61
61
+
const birth = new Date(birthday)
62
62
+
const now = new Date()
63
63
+
64
64
+
let years = now.getFullYear() - birth.getFullYear()
65
65
+
let months = now.getMonth() - birth.getMonth()
66
66
+
let days = now.getDate() - birth.getDate()
67
67
+
68
68
+
if (days < 0) {
69
69
+
months--
70
70
+
const prevMonth = new Date(now.getFullYear(), now.getMonth(), 0)
71
71
+
days += prevMonth.getDate()
72
72
+
}
73
73
+
74
74
+
if (months < 0) {
75
75
+
years--
76
76
+
months += 12
77
77
+
}
78
78
+
79
79
+
80
80
+
if (width < 500) {
81
81
+
return <flex direction="vertical">
82
82
+
<fill size={20} />
83
83
+
84
84
+
{name && (
85
85
+
<flex direction="vertical">
86
86
+
<text fontSize={40}>{name}'s age</text>
87
87
+
<fill size={1} color="gray" />
88
88
+
</flex>
89
89
+
)}
90
90
+
91
91
+
<flex weight={5}
92
92
+
gap={20}
93
93
+
justify="center"
94
94
+
direction="vertical" >
95
95
+
<Meta size={140}>
96
96
+
<text size={100} fontSize={120}>{years}</text>
97
97
+
<text size={22} fontSize={22}>Years</text>
98
98
+
</Meta>
99
99
+
<flex size={100} direction="horizontal" gap={20}>
100
100
+
<Meta weight={1}>
101
101
+
<text size={60} fontSize={60}>{months}</text>
102
102
+
<text size={22} fontSize={22}>Months</text>
103
103
+
</Meta>
104
104
+
<Meta weight={1}>
105
105
+
<text size={60} fontSize={60}>{days}</text>
106
106
+
<text size={22} fontSize={22}>Days</text>
107
107
+
</Meta>
108
108
+
</flex>
109
109
+
</flex>
110
110
+
111
111
+
<fill size={20} />
112
112
+
</flex>
113
113
+
}
114
114
+
115
115
+
return <flex direction="vertical">
116
116
+
{name && (
117
117
+
<flex direction="vertical">
118
118
+
<text fontSize={50}>{name}'s age</text>
119
119
+
<fill size={1} color="gray" />
120
120
+
</flex>
121
121
+
)}
122
122
+
123
123
+
<flex weight={3} direction="vertical">
124
124
+
<fill color="white" />
125
125
+
<flex size={160} direction="horizontal">
126
126
+
<Meta>
127
127
+
<text size={110} fontSize={120}>{years}</text>
128
128
+
<text size={30} fontSize={30}>Years</text>
129
129
+
</Meta>
130
130
+
<Meta>
131
131
+
<text size={110} fontSize={120}>{months}</text>
132
132
+
<text size={30} fontSize={30}>Months</text>
133
133
+
</Meta>
134
134
+
<Meta>
135
135
+
<text size={110} fontSize={120}>{days}</text>
136
136
+
<text size={30} fontSize={30}>Days</text>
137
137
+
</Meta>
138
138
+
</flex>
139
139
+
<fill color="white" />
140
140
+
</flex>
141
141
+
142
142
+
<fill color="white" />
143
143
+
</flex>
144
144
+
}
145
145
+
60
146
export default function Tracker({width, start, end}) {
61
147
const s = Date.parse(start)
62
148
const e = Date.parse(end)
+1
-1
internal/module/scrn/weather.jsx
···
55
55
const temp = Math.round(weather.current.temperature_2m)
56
56
const unit = weather.current_units.temperature_2m
57
57
58
58
-
inner = <text fontSize={80}>
58
58
+
inner = <text fontSize={60}>
59
59
{temp} {unit}
60
60
</text>
61
61
} else {
+1
-1
main.go
···
25
25
r := gin.Default()
26
26
handler.HandleGroup(r.Group("/api"))
27
27
28
28
-
if err := r.RunTLS(":8081", "localhost.crt", "localhost.key"); err != nil {
28
28
+
if err := r.Run(":8081"); err != nil {
29
29
log.Fatalf("%s", err)
30
30
}
31
31
}