Files
luma-home/development.md
T
Kaloyan DanchevandClaude Opus 4.6 cbea5a322f
CI / Identify a merged version PR (push) Canceled after 0s
CI / Rust checks, rendering, and native package (push) Canceled after 0s
CI / Publish release and Homebrew feed (push) Canceled after 0s
Release PR / Maintain Version Packages PR (push) Canceled after 0s
Merge Luma launcher fork into monorepo
Move trampoline files (boot-hook, watcher, overlay) into trampoline/
subdirectory. Add upstream Luma v2.3.1 source at repo root with our
additions:

- Page::Ha (4th nav tab) with placeholder empty state
- HA tab config schema and default entity mapping (ha-home/)
- Nav keys renamed: "home" → "start", new "ha" tab between Apps and Settings
- All state.rs/view.rs/main.rs/tests.rs updated for 4-tab navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-24 11:52:55 +03:00

36 KiB
Raw Permalink Blame History

Luma development

Layout

Cargo.toml / Cargo.lock       Rust workspace and pinned dependencies
src/                         Rust launcher, platform adapter and GLES2 shaders
assets/                      Editable SVGs, fonts, wallpapers, translations and artwork metadata
vector-geometry/             Shared fill tessellation and vector boundary coverage
tools/vector-assets/         Host-only SVG compiler
tools/                       Asset preparation, SDK/runtime setup, packaging and release feed
tests/                       Desktop rendering and mocked HTTP smoke scripts
tv/autostart.sh              Optional cold-boot launch hook
tv/home.sh / home.cjs         Optional root-only native Home integration
dist/                        Generated ARM IPK, checksum and Homebrew feed
.build/                      Generated assets, SDK/runtime caches and rendering diagnostics

React, JSX, Vite, browser tests and web rendering are no longer part of the project. Node/pnpm remain build/deployment tools for Sharp, the webOS CLI and formatting/linting. The installed app is the Rust executable luma, with app ID com.ecklf.luma.

Toolchain and commands

nix develop
pnpm install --frozen-lockfile
pnpm assets
pnpm dev

The flake supplies Rust 1.93, Cargo/rustfmt/Clippy, Node.js 24, pnpm 10.34.5, SDL2, pkg-config, woff2, Python/fontTools, CMake and Git. Linux includes Mesa and Xvfb. Supported Nix hosts are Apple Silicon macOS and ARM64/x86-64 Linux. Without Nix, supply those tools and Rust 1.88+, including armv7-unknown-linux-gnueabi for TV builds. No browser installation is needed.

Command Purpose
pnpm dev [--options] Run the desktop preview
pnpm assets [--4k] Prepare fonts, vector artwork and raster backgrounds
pnpm screenshots Capture native Home, Apps, General and Inputs into docs/
pnpm assets:vector Recompile bundled SVG icon/logo meshes
pnpm assets:fonts Restore checksum-pinned Noto font source if needed
pnpm sdk Download/relocate a pinned host webOS SDK
pnpm runtime Prepare checksum-pinned SDL-webOS
pnpm build Release ARM cross-build using WEBOS_NDK
pnpm run package Assets, cross-build, IPK verification and Homebrew feed
pnpm deploy --device tv Build/install/launch through an ares device profile
pnpm check Oxlint and workspace Clippy, without executing tests
pnpm format:check Rust and tooling/document formatting checks
pnpm test Release-tooling and Rust workspace unit tests
pnpm test:icons Mocked runtime icon reads and bundled vector checks
pnpm test:smoke Desktop rendering, mocked weather and localization checks
pnpm changeset Add a release note and requested version bump to a PR
pnpm version:check Check synchronized app release versions

The TV target is ARMv7 soft-float calling convention, gnueabi, not gnueabihf. Codegen uses the NDK's Cortex-A9/VFP baseline with NEON disabled for Rust code. Hardware floating point is enabled without changing the calling convention; the pinned compiler emits unstable-feature warnings for those target switches. src/webos-compat.rs implements getauxval for the SDK's older libc by reading real kernel auxiliary values, used by Rust startup and TLS CPU detection.

The NDK's SDL is a link-time stub with Wayland disabled. tools/webos-runtime.mjs instead pins SDL-webOS release-2.30.12-webos.5, standard SDL ABI, with archive/library SHA-256 verification. It is linked and packaged with runtime name libSDL2-2.0.so.0, loaded from $ORIGIN/lib. Desktop pkg-config paths are isolated from the ARM build. WEBOS_NATIVE_LIBS can point to explicitly selected ARM webOS shared libraries; SDL overrides must have a real webOS/Wayland backend. Avoid copying the whole SDK library directory or the TV's graphics drivers/libc.

Controls and navigation

Documentation screenshots

pnpm screenshots generates docs/home.png, docs/apps.png, docs/settings.png and docs/inputs.png, linked by the README. It builds the desktop executable without the TV feature, uses isolated temporary preferences, sample inventory and a fixed date/time, and captures actual 1920×1080 OpenGL framebuffers. Weather is unconfigured, so no real weather APIs are contacted. It does not execute the test suites. A desktop display is required; on headless Linux use xvfb-run -a pnpm screenshots. These images document the native design, not TV hardware behavior.

Input

Input Action
Arrows Move focus, revealing remote-focused content
OK / Enter Activate on release
Hold OK for 600 ms Reorder Home favorite or open app options
OK or Back in move mode Finish reordering without launching
Escape / Back / Backspace Parent navigation, overlay dismissal or quit from Home
Menu / M / right-click App options
Pointer / wheel Hover, click, hold and scrolling
F5 Refresh inventory

Backspace deletes while editing. Native Back uses SDL's webOS-specific keycode. Back dismisses the keyboard first; Weather Location then returns to its parent Location row, HDMI choices to that HDMI row, and the Background gallery to Background in the sidebar. Back from the sidebar focuses the Settings tab, then returns Home. System in the sidebar groups Refresh apps & inputs and the vector power-icon Quit app action separately from General preferences. The Clock subpage contains Clock format (12-hour or 24-hour) and Time format (HH:MM or HH:MM:SS, defaulting to HH:MM). Choosing a format returns to its row in Clock, and Back from Clock restores its General row. With seconds shown, the header clock redraws once per second instead of once per minute. Existing saved hour and seconds preferences are retained. General has an App names subpage with independent Show on Home and Show on Apps toggles. Home captions default to off and Apps captions default to on, stored independently in show_home_names and show_app_names. The legacy show_names preference does not override these defaults. Hidden captions do not reserve layout space. The Greeting subpage contains Name and Clear name. Selecting Name opens the text keyboard (the system keyboard on the TV) and the name is appended to the Home greeting ("Good evening, Ana."). The keyboard starts empty; closing it without a name preserves the saved value. Clear name removes it. Preferences are saved when the keyboard closes, including automatic system-keyboard dismissal. Back from either subpage restores its General row. Input name removal also uses Clear name. Choice checkmarks identify saved values, while the bright row fill identifies only the current focus.

Home favorites form one horizontal strip, never a second row. Left/Right follows saved order, including reorder mode, and reveals complete cards. Up returns to Home navigation; Down enters the nearest input in the first row. The strip position survives page changes. Wheel movement over favorites scrolls horizontally; elsewhere it scrolls vertically. Pointer hover/down never moves the clicked control. Apps Up/Down follows the six-column grid order, including offscreen rows, and only the first row's Up route reaches the category toolbar. Scrolling Apps content fades into the existing wallpaper at the header/footer, with one alpha mask for surfaces, shadows, rings, vector icons/logos and text. The wallpaper/header are unmasked; no blur texture is needed.

TV editing uses the system keyboard through SDL-webOS. Desktop editing uses the custom panel. System text events update app/location queries; Done/Back stops editing and restores focus.

Splash images

pnpm assets renders one 1920×1080 PNG per background into .build/assets/splash/ from the shaded wallpapers. tools/package.mjs ships them under splash/ plus splash.png (the default background), which appinfo.json names as splashBackground, with bgColor set to the UI base colour so the frame between the splash and the first draw is not black. On a normal TV launch and after successful preference saves, the native platform worker passes the loaded background to the embedded tv/sync-splash.cjs helper through Homebrew Channel's elevated exec service. It atomically updates the installed splash.png and the matching enabled Home stage, skipping identical images. Desktop and isolated --preferences / --test-extra-apps runs do not update launch splashes. The first launch after installation or a package update can still show the packaged default until this sync completes; later launches use the saved selection. With Home takeover enabled, tv/home.cjs copies the selected background's splash into the staged Home app on every apply, reading the saved preferences from the TV storage candidates in storage.rs. Production installations prefer internal storage; developer installations prefer developer storage, with existing files taking precedence over the legacy installation-local preferences. Reapplying an active Home mount also refreshes the splash. pnpm test:home checks splash selection, runtime updates and mounted refreshes with a mocked filesystem and no TV calls. Verify the system-rendered splash on a real TV by changing Background, quitting Luma, and relaunching its normal tile; desktop checks cannot establish SAM/compositor image caching behavior.

Vector rendering

assets/ui-icons/ holds standalone SVGs exported from the original Lucide/control artwork, including the supplied PlayStation/Apple TV marks. assets/logos/ holds original bundled logos. tools/vector-assets/ parses SVGs with usvg, resolves stroke outlines and transforms, and uses the shared lyon tessellator to emit .lvec geometry. The format contains positions, edge offsets, vertex colors and coverage; holes, round strokes and the bundled gradients are preserved. Gradient colors are adaptively sampled at build time. The TV uploads each mesh lazily once and draws with a texture-free GLES2 shader. No SVG parser runs on the TV. The shared tessellator also handles font outlines. Installed app PNG/JPEG/WebP artwork, wallpaper photos, tile-background images and the webOS installation icon remain raster assets. See assets.md for licenses.

Installed app artwork

TV inventory retains each app's folderPath, largeIcon and icon. Visible cards resolve artwork from that metadata and the installed appinfo.json, preferring large icons and falling back to the regular icon or icon.png. Developer, store and system application directories are supported, including absolute/file:// paths and relative resource paths. Curated vector brand logos retain their existing display treatment; other apps use their own installed artwork and an automatically sampled card colour. Missing, corrupt or unsupported artwork uses the existing generic treatment.

Discovery, bounded file reads, decoding and colour sampling run on a dedicated worker. If the jailed app cannot read an asset directly, the native process calls Homebrew Channel's elevated exec service with the embedded Node 8-compatible tv/read-app-icon.cjs reader. It returns bounded image bytes without writing collected files or changing filesystem permissions. PNG, JPEG and WebP are supported; runtime SVG parsing is not required. Source files are limited to 512 KiB and 2048×2048 pixels; decoded icons are at most 256×256 with a 16 MiB/256-entry CPU cache. GPU uploads stay on the SDL thread and share the existing 48 MiB texture cache. Pending/unavailable icons do not block input, and failures are cached until inventory is refreshed.

Inventory also refreshes at startup and whenever the window becomes visible. Decoded icons are keyed by app ID, version and icon paths, so those refreshes keep them and retry only previous failures; an updated app has a new version and is read again. Refresh apps (or desktop F5) invalidates all artwork, including previous failures; request tokens discard stale replies. Check newly installed/updated apps and both normal tile and Home-takeover launches on a real TV to verify sandbox access. These paths are covered with mocked filesystem and worker tests, not live-TV calls in automated checks.

Dumped TV artwork, collected icon/colour maps, and their collection tools have been removed. Desktop previews use curated vector logos or generic/category artwork for other demo apps. Asset generation clears retired dumped-artwork outputs from older build caches, and packaging continues rejecting those legacy files. Adding support for an installed TV app's icon does not require collecting artwork or changing JSON.

Curated brand cards and the desktop demo

src/branding.rs contains the exact app-ID allowlist for 38 curated brand and utility cards. Titles and partial ID matches do not select vector overrides. In particular, Amazon Alexa retains its own installed icon rather than receiving Prime Video branding. NOW / Now TV recognizes now.tv and nowtv; WOW, Sky Q and the separate Hong Kong Now TV service are not treated as aliases. Unknown regional IDs continue using their installed artwork. Verify additional aliases against that TV's inventory before adding them to the allowlist.

The aliases include developer fixtures and community webOS inventories, including FHEM's LGTV app IDs, a UK webOS inventory, and the UK streaming catalogue. These sources do not establish that every alias is available on every region/firmware combination.

Every curated logo has an app in demo_inventory(), including all 27 expanded streaming/music brands. Open them with pnpm dev --script apps, then scroll or search the Apps grid. A unit check keeps the SVG directory, curated catalogue and demo inventory aligned. pnpm test:brands renders every card through the actual desktop renderer and writes .build/brands-gallery.png for visual review. Like the other smoke scripts, it uses isolated preferences and no TV or real weather API. The gallery also runs as part of pnpm test:smoke. Asset source/licence details are in assets.md.

Live TV (com.webos.app.livetv) and Web Browser (com.webos.app.browser) also have dedicated SVG-backed demo cards: a blue/cyan TV display with a live indicator, and a matching globe with a navigation pointer. Both are original Luma vector artwork. The existing card caption remains available when names are visible. Brand::artwork_size() sets optical sizing per brand, keeping heavy wordmarks such as Hulu, Joyn and Plex closer to the visual weight of Apple TV, YouTube, Das Erste and ARTE.

Text reads the bundled TTF's advances, kerning and line metrics lazily. Only needed glyph outlines are tessellated in font-em coordinates. Complete text runs are cached in static GPU buffers, independent of position/color/drawable density, avoiding a draw call per letter. CJK fonts open only when needed; there are no glyph bitmap atlases or full-font mesh exports.

The window requests 4× MSAA, with 2×/single-sample fallbacks if EGL rejects a configuration. Multisampled text draws intact glyph fills without inward offsets/fringe triangles, which can cross on very thin small-text strokes. Single-sample targets use a narrower half-pixel boundary fringe. Glyph origins/baselines are pixel-aligned per drawable axis. There is no LCD subpixel rendering, font hinting or complex-script shaping. Inspect multisample_samples and font_antialiasing: offscreen captures can be single-sample while the actual window uses MSAA.

Glyph geometry has an 8 MiB/4096-entry CPU cache target and text runs a 32 MiB/512-entry GPU target. Eviction preserves the current frame, which can temporarily exceed those targets. Raster artwork has a 48 MiB cache budget. Diagnostics include geometry bytes, triangles, draw calls and missing/failed artwork/glyphs. Idle screens draw on demand; VSync paces animation.

Resolution and profiling

Rendering requests and captures

The shipping default requests 1920×1080 and retains a 1920×1080 logical layout. Drawable-aware vector coverage follows the actual SDL framebuffer. --resolution 4k is an experimental native surface request, not a guaranteed panel-output mode. The TV compositor can constrain or rescale it. A 4K screenshot or drawable alone does not prove 1:1 panel presentation.

pnpm dev --resolution 4k --profile-frames 180 --diagnostics .build/profile.json
pnpm dev --script 'home,down' --frames 90 --capture-scale 2 --screenshot .build/home-4k.png

--resolution also accepts 1080p or WIDTHxHEIGHT. Explicit TV requests use a borderless window, avoiding SDL desktop-fullscreen overriding the dimensions. --capture-scale 2 uses a separate 3840×2160 offscreen target and leaves the actual window unchanged. Font/vector geometry uses that target's physical coverage rather than enlarging a 1080p image. Optional @2x wallpaper images need both pnpm assets --4k and node tools/package.mjs --4k; default packages exclude them even if they remain in an old cache. Source photographic detail still limits sharpness.

4K TV investigation

Manual testing on 2026-10-23 used an LG OLED55C57LA (C5), webOS 26, chip O22A3, driver lg1kdrm. A contributor reported similar corruption on a C3 in #25. Usable native 4K UI presentation has not been established on these tested TVs.

The C5's default EGLFS JSON reported 1920x1080, and the active DRM state independently confirmed a 1080p compositor framebuffer and output mode. The connector advertised 3840x2160, and /proc/lg/fbdev/drm-helper/check_mode reported 3840x2160: ok (7680x4320: err). FB0 reported cfg.max_xres = 1920, cfg.max_yres = 1080 and virtual size 1920,2160, consistent with two stacked 1080p buffers rather than a 4K framebuffer.

The startup script uses Weston-style arguments and a weston.ini with backend=drm-backend.so and override-output-from-env=true. Its product environment also generates the EGLFS JSON. Inspect the actual startup path and DRM state; the JSON alone does not establish which output configuration is active.

A temporary compositor-level test bind-mounted patched copies of surface-manager.sh and eglfs_starfish.env, requesting matching 3840×2160 compositor and primary output geometry, then restarted surface-manager-daemon.service. Systemd scheduled restoration after 90 seconds.

Observation Default Forced 4K test
Compositor framebuffer 1920×1080 3840×2160
Format ABGR8888 with AFBC ABGR8888 with AFBC
Pitch 7680 bytes 15360 bytes
Plane source and destination 1920×1080 3840×2160
Active DRM mode 1920×1080 at 60 Hz 3840×2160 at 60 Hz
Visible picture Normal Flickering lines and corruption

Restoring the original configuration returned the picture to normal. This was a system compositor experiment, not a supported Luma setting or installation procedure.

Mode acceptance, a 4K drawable or screenshot, and even matching 4K framebuffer/plane/CRTC dimensions do not prove correct panel presentation. Check the visible output and use a pixel-resolution test pattern before claiming 1:1 presentation. The corruption's exact cause is unresolved: FB0's configured maximum and compatibility metadata alone do not establish a physical hardware limit or explain the failure. These results do not prove that 4K graphics are impossible on every LG TV; they establish that the tested override does not produce a usable picture on this C5. An app-side resolution request alone does not resolve that display-path failure.

For read-only inspection in the TV's root SSH shell (paths depend on firmware):

cat /sys/kernel/debug/dri/0/state
cat /proc/lg/fbdev/status
cat /proc/lg/fbdev/drm-helper/status
cat /proc/lg/fbdev/drm-helper/check_mode

4K video playback is a separate capability. LG's app resolution specifications distinguish 1080p graphics from 4K video on UHD TVs. Native media players can route decoded video through a separate video plane while drawing their UI at 1080p. For example, PlxNative documents its default 1080p UI drawable alongside 4K video support. That media path does not demonstrate a usable 4K OpenGL graphics path for Luma; bypassing the browser does not by itself reconfigure LG's graphics output pipeline.

Frame profiling

--profile-frames N forces redraws and measures swap-to-swap intervals after ten warmup presentations. Hidden windows pause profiling. Capture stalls are excluded. CPU submission is not a GPU timestamp, and --frames counts event-loop iterations, not presentation rate. Inspect window, drawable, drawable_is_4k, last_presented, capture and profile in JSON. Do not infer 60 fps from a successful VSync request; measure actual TV pacing and transitions.

TV startup diagnostics

SAM passes launch metadata as a JSON object in argv[1]. The CLI skips that object while leaving the original process arguments available to SDL/libhelpers. To expose the actual developer-mode sandbox's stdout/stderr, close Luma and run in the TV's root SSH shell:

APP=/media/developer/apps/usr/palm/applications/com.ecklf.luma
XDG_RUNTIME_DIR=/tmp/xdg RUST_BACKTRACE=1 /usr/bin/jailer \
  -t native_devmode -i com.ecklf.luma -p "$APP" \
  "$APP/luma" '{"@system_native_app":true,"nid":"com.ecklf.luma"}'

Startup prints accepted launch parameters, SDL driver, actual sample count, cumulative stage timings, first-frame latency and vector-text cache size. Timings begin inside the executable and exclude SAM/jailer process-launch overhead. The IPK is unpacked at install, not every launch. A previous vector-font startup on the development TV presented its first frame in 399 ms; new rendering paths still need their own hardware comparisons. SDL helper getAppInfo or getSystemSettings registration calls can report permission warnings; they have not blocked the verified interface/inventory runs. Normal launch/relaunch behavior should be checked too.

The native adapter registers LS2 connections from the executable and calls LSCallOneReply. Anonymous names avoid collisions with SDL's registration; the hub still applies the executable's role. luna-send-pub -a app impersonation is rejected on the development webOS 26 TV and is not used. Native requests run on one worker's private GLib context with a 6.5-second budget and 2 MiB reply limit. Errors include URI and the bus refusal. Fixed inventory commands run via elevated Homebrew Channel exec, parsing complete multi-line luna-send -n 1 JSON replies. Input-read failures preserve the last valid inputs. No failed inventory read substitutes demo apps on TV.

Optional native Home integration

tv/home.sh and tv/home.cjs are packaged beside luma. They are a root-only, explicit integration for webOS 10/25 and 11/26. enable stages a native manifest with ID com.webos.app.home and symlinks to the installed executable, libraries and assets under /var/lib/luma-home/app, then bind-mounts it over /usr/palm/applications/com.webos.app.home. SAM rereads that one app through blockSystemApp followed by releaseBlockSystemApp, including forceBlockSystemApp: false when the TV schema requires it. The script verifies the reported manifest before stopping the previous Home process. It attempts to restore LG Home if applying the mount or refreshing SAM fails.

Before staging, enable closes the normal Luma instance with closeByAppId and waits up to five seconds for it to leave the running list. The TV's close endpoint identifies a process, so passing the package ID there can return no app matched by pid. If closing reports an error, the script rechecks the running list and proceeds only if the normal instance has already exited.

Root-shell calls use luna-send -n 1 on both firmware versions, with bounded stdout/stderr capture and operation-specific diagnostics for empty or invalid replies. -t 1 is timing mode: it logs the payload to stderr and writes timing summaries to stdout, not a JSON response.

The enable shell entry point prints the change summary and requires a y/n confirmation before creating the state directory, acquiring the lock or invoking the installer. Only y/Y proceeds. n/N or end-of-input cancels, and other input repeats the prompt. The boot hook and apply remain non-interactive, and disable/restore remain available without confirmation.

The native CLI accepts SAM's Home nid and gives SDL the matching Home surface identity. The executable itself stays at its installed path for LS2 authentication, $ORIGIN/lib, asset discovery and preference selection. Opening Luma's normal tile while the Home mount is active redirects to the Home instance. The scripts do not create a browser wrapper or change the package's app ID.

64-luma-home reapplies the mount from a detached boot hook. The existing TV Home Auto Launch and Quick Start+ settings determine whether power-on opens Home. Until the hook applies, LG Home can still appear. This integration does not change those settings or install a remote-key filter. disable (also restore) removes the mount, refreshes LG's manifest, stops the native Home instance and removes the boot hook. The copied restore scripts remain in /var/lib/luma-home even if the installed package is removed. Disable the standalone 65-luma cold-boot hook before enabling this integration.

Build and script checks cannot establish native Home compatibility. Manually verify Home, Back out of the last app, standby wake with the TV's chosen Quick Start+ setting, a full cold boot, saved preferences and restoring LG Home. The broader webOS 6+ app compatibility statement does not establish Home-replacement support on older firmware.

Manual extra-app preview

pnpm dev --test-extra-apps 120

On the TV, add --test-extra-apps 120 after the JSON argument in the jailed command above. This explicitly appends up to 300 labeled non-launchable cards to a successful real inventory, using its apps as artwork/category templates. It uses scroll-preview-preferences.json, initialized from normal choices once. Favoriting test cards exercises long Home strips without changing normal favorites. Ctrl+C then a normal launch restores real inventory/preferences. The placeholder cards are never a fallback for an inventory failure.

Storage, weather and languages

TV preferences live in an app-owned persistent directory outside the installed executable's payload. Startup probes /media/developer/<appid>/, /media/developer/temp/<appid>/ and /media/internal/.<appid>/, selecting a directory the jailed app can actually create and write. Production /media/cryptofs installations prefer internal storage. /media/developer/temp is on the persistent developer partition, not the reboot-cleared /tmp. No settings are placed in /tmp and no system/app-payload permissions are widened. Existing JSON preferences and a valid weather cache from beside this installation's old executable are imported once, without overwriting the chosen destination. Invalid existing preferences still report an error. Startup prints Luma persistent storage: with the chosen path. Desktop preview retains the legacy $XDG_CONFIG_HOME/luma-native/preferences.json path, or ~/.config/luma-native/preferences.json, so existing preview choices survive flattening. --preferences PATH isolates a manual run. Invalid preference files report an error rather than being overwritten. Writes use atomic temporary-file renames; use one instance per preferences file. Browser local storage is not migrated into native JSON preferences.

Weather Location searches cities/postcodes only on explicit Search. Forecasts run when Home is visible, the widget enabled and a city configured. ureq/rustls uses embedded certificate roots, an eight-second timeout, no redirects/proxy and 256 KiB reply limits. Search/forecast/cache writes have separate workers and generation IDs, avoiding stale responses overwriting current state. Fresh Celsius readings are reused for 15 minutes; valid saved data lasts up to six hours and failure retries after five minutes. Unit changes convert unrounded cached Celsius immediately. Removing a city clears its cache. The .weather.json sidecar uses Unix-seconds timestamps. Mock servers accept only literal loopback HTTP origins and are refused in TV builds.

assets/messages.json supplies English/German/Spanish/Simplified Chinese, including locale formatting and weather labels. App/device names and saved city names remain real data. No geolocation, runtime font/translation services or telemetry are used. Weather attribution: Open-Meteo, CC BY 4.0.

Compatibility

Luma's native startup, rendering and app/input discovery have been verified on rooted webOS 26. External testing confirms compatibility with webOS 6 and newer. On webOS 26 use elevated Homebrew Channel 0.8.0-rc1+ for lunabus.query. Root SSH execution does not establish the normal jailed caller permissions; check TV-menu startup, launching/reopening and remote/pointer behavior manually.

webOS Homebrew Can I Use lists firmware library versions. Its library guide maps internal releases to marketing names: 7/22, 8/23, 9/24, 10/25, 11/26. SDL-webOS's newer 2.30.12-webos.6 reports hotplug verification on webOS 3.4, 4 and 10. That is subsystem evidence for a newer prerelease than the pinned .5 library, not a full Luma compatibility guarantee.

Checks and CI

Rust behavior tests cover navigation, preferences, localization, rendering geometry and weather state without a TV. tests/ scripts exercise actual desktop framebuffers and mock HTTP responses for navigation, persistence, pointer input, translations/CJK, weather limits/cache/races and profiling. They contact no live weather APIs or television. On Linux:

pnpm format:check
pnpm check
pnpm test
pnpm assets
xvfb-run -a pnpm test:smoke

CI uses this native-only flow, then cross-links with a pinned Linux SDK, inspects/normalizes the actual ARM IPK and generates checksums/feed. It uploads release/rendering artifacts on PRs, main-branch pushes and manual runs. pnpm check also validates synchronized release versions, and pnpm test:release covers version synchronization and the publish gate, including the real Changesets version command in an isolated fixture. Host CI cannot prove hardware compatibility, small-text quality or presentation frame rate.

Releases

Add a changeset to feature/fix PRs that should be included in the next app release:

pnpm changeset

Select lg-luma, choose patch/minor/major and describe the change. Commit the generated file under .changeset/ with the PR. Documentation-only and maintenance PRs that do not need a release can omit it. Keep the existing app version in ordinary PRs.

.github/workflows/changeset-status.yml adds a Changesets status check and maintains one comment per PR, including fork PRs. It reports the requested release types or notes that a PR without a changeset will not request an app version bump. For code or behavior changes, add a changeset using semantic versioning: patch for backward-compatible fixes, minor for backward-compatible features, or major for breaking changes. The comment is informational, so documentation-only and maintenance PRs can still merge without a release changeset. It updates when a PR is opened, reopened, edited, marked ready or receives new commits, and skips the generated Version Packages PR. The workflow becomes active once it is merged into the PR's base branch. It uses pull_request_target with a read-only status job and a separate comment-writing job, without installing dependencies or running code from the PR.

After changesets merge into main, .github/workflows/release-pr.yml creates or updates the Version Packages PR on changeset-release/main. Changesets combines the pending notes using the highest requested bump. The custom pnpm version:release command updates package.json and CHANGELOG.md, then synchronizes appinfo.json, the root Cargo.toml, the luma entry in Cargo.lock, and the versioned IPK examples in readme.md. Dependency pins and supporting Rust crate versions are preserved. The package stays private and is never published to npm.

The generated PR receives the same CI build as ordinary PRs. Since PRs created with GitHub's built-in token do not trigger pull_request workflows, the release workflow explicitly dispatches ci.yml for the version branch. Enable Settings → Actions → General → Allow GitHub Actions to create and approve pull requests in the repository. No additional token or npm secret is needed.

Review and merge the version PR when ready to release. Only a main-branch push associated with that merged, same-repository PR and a numeric version increase can enter the publish job. It must also pass the existing checks, desktop smoke tests and native packaging build. Ordinary merges, unmerged PRs and manual CI runs do not publish. To retry a failed publication, rerun the original CI run for the merged version PR's main-branch commit.

Publication retains the existing guards: an already-published version is skipped, a draft can be resumed, and a tag pointing at another commit requires a new app version. pnpm version:check checks synchronization without writing files. pnpm version:sync repairs metadata from the current package.json version without calculating a bump. pnpm version:release is reserved for the automated version PR.

Homebrew Channel's details page reads packages[].shortDescription (or fullDescriptionUrl), not manifest.appDescription. The feed generator writes the root app metadata description into shortDescription and manifest.appDescription. It also copies assets/homebrew-description.html to dist/description.html and sets the package's fullDescriptionUrl to that version's release asset. Homebrew Channel prefers this HTML description when the URL is present. CI uploads the HTML alongside the feed and IPK, so the feed and its description must be published together.

Key modules: state.rs focus/preferences, view.rs geometry, renderer.rs GPU caches/draws, font.rs lazy metrics/outlines, vector.rs GPU mesh upload, platform.rs worker/inventory, luna.rs LS2, weather*.rs scheduling/HTTPS/cache, storage.rs atomic JSON, and main.rs SDL loop.