WIP trmnl BYOS

feat: extending modules

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