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

530 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Luma development
## Layout
```text
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
```sh
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](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](https://github.com/fhem/LGTV_WebOS/blob/master/lib/FHEM/Devices/LGTV/LGTVWebOS.pm),
[a UK webOS inventory](https://github.com/chrispab/AutoHome/blob/master/items/CT_LGWebOS_TV.items),
and the [UK streaming catalogue](https://github.com/tjeffree/webOS-Streaming-Aggregator/blob/main/frontend/src/services/appCatalog.js).
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](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.
```sh
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](https://github.com/ecklf/lg-luma/issues/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):
```sh
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](https://webostv.developer.lge.com/develop/specifications/app-resolution)
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](https://github.com/GLinnik21/plx-native/blob/13bed25004df4c89f8bcba9558b571b8ed962bd0/rust-modules/base/src/surface.rs#L10-L25)
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:
```sh
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
```sh
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](https://creativecommons.org/licenses/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](https://www.webosbrew.org/develop/caniuse) lists firmware library
versions. Its [library guide](https://www.webosbrew.org/develop/guides/library-version) 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`](https://github.com/webosbrew/SDL-webOS/releases/tag/release-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:
```sh
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:
```sh
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.