Skip to content

Latest commit

 

History

History
88 lines (55 loc) · 18.1 KB

development.md

File metadata and controls

88 lines (55 loc) · 18.1 KB

Developer guide

The main UI separates Control, Scenes, Device setup, and Settings. src/ui/ui-common.js shares version display, runtime status, scene matching, and canvas rendering with Quick controls. The native startMode action restores mode memory and starts lighting in one worker message. Navigation and thumbnail previews never connect USB or start capture.

The local SVG symbols come from Lucide 0.468.0, revision f12b0de177fbc2a6795e99be065887e72b237123. Keep the ISC/MIT notices in src/ui/icons.LICENSE.txt with the subset. Edit the brand in assets/icon.svg, then run pnpm icons to regenerate the UI copy and all desktop icon formats. Commit the generated files; pnpm icons:check checks them in CI on every platform. No icon font, CDN, or runtime icon library is needed.

Develop

Install Tauri's system prerequisites, current stable Rust (tested with 1.99.1), Node.js 24, and pnpm 11. Windows needs the Visual Studio C++ build tools and Windows SDK; macOS needs Xcode command-line tools. For Linux, use Ubuntu 24.04+ and the native packages listed in .github/workflows/build.yml, including WebKitGTK 4.1, ALSA, udev, XCB, PipeWire, and libclang development headers. pulseaudio-utils provides the system-audio monitor helper. See Linux setup for runtime permissions.

pnpm install --frozen-lockfile
pnpm start                 # Native app with development rebuilds
pnpm test                  # Rust protocol, settings, audio, sampling, lifecycle tests
pnpm check                 # Clippy, with warnings treated as errors
pnpm exec playwright install chromium
pnpm test:ui               # Browser UI tests with isolated IPC fixtures
pnpm run build:win         # Windows NSIS installer, on Windows
pnpm run build:mac         # macOS DMG for the host architecture, on macOS
pnpm run build:linux       # Debian package and AppImage, on Linux

For another Mac architecture, use rustup target add x86_64-apple-darwin or aarch64-apple-darwin, then pnpm tauri build --target <target> --bundles dmg. CI builds Windows x64, Linux x64, and both Mac architectures. Bundles appear in src-tauri/target[/<target>]/release/bundle/. Cargo.lock and pnpm-lock.yaml are committed. Build on the minimum Linux distribution you intend to support; newer glibc binaries cannot run on older distributions.

scripts/smoke.cjs tests the packaged Windows UI and IPC through WebView2's local debugging port using the Playwright development dependency. Close any running DynamicLight first, or build a separate test identifier with Tauri's --config option to isolate its settings and single-instance lock. node scripts/smoke.cjs --hardware additionally changes the strip and briefly captures screen/system audio. Ordinary tests never access USB or start capture. Set DYNAMICLIGHT_EXECUTABLE to test an installed executable. The debugging port is enabled only in that test process.

pnpm test:ui runs the real UI against explicit browser-only IPC fixtures, checking narrow, maximized, 4K, and ultrawide layouts at 100–200% display scaling, plus effect/music controls, live metrics, error recovery, and slider coalescing. The Windows native smoke also inspects both windows' actual title-bar and taskbar icon handles. These checks do not test physical hardware. Set UI_BROWSER_CHANNEL=msedge to use an installed Edge browser, or PLAYWRIGHT_PATH to reuse an existing Playwright installation. Screenshots are written to the ignored .local/ directory.

Publish a release

Build desktop apps runs on pull requests, pushes to main, manual dispatches and v* tags (documentation-only changes are skipped by paths-ignore; tags are never path-filtered). Every run builds Windows x64, macOS Apple silicon and Intel, and Linux x64 in parallel, with the command-line tool. macOS and Linux also get their own pnpm check and pnpm test pass, because they compile different code. Pull requests and branches only upload Actions artifacts. Only a pushed v<version> tag publishes a release (this fork does not publish from main).

  1. Set the same version in package.json, src-tauri/Cargo.toml, control/Cargo.toml, the dynamiclight entry in src-tauri/Cargo.lock, the dynamiclight-control entries in both Cargo.lock files, and src-tauri/tauri.conf.json. Start docs/release-notes.md with DynamicLight <version>: it becomes the release text and the updater's notes, and the tag build stops at once if it names another version.
  2. Validate with node scripts/release.cjs version v<version>, node --test scripts/release-test.cjs scripts/publish-release-test.cjs scripts/install-test.cjs (needs minisign) and the ordinary checks above. Merge the release PR once its builds are green.
  3. Tag the merge commit and push the tag: git tag -a v<version> <commit> then git push origin v<version>. The workflow signs the packages with the repository key and publishes.
  4. Windows is required, macOS and Linux are optional. If one of them fails to build, the release is published without it, its platform is left out of latest.json, and the release text says so; macOS/Linux builds are also flagged experimental there. To add a platform later, fix it on main and run Build desktop apps manually on the existing tag with republish ticked: it rebuilds everything on that tag and uploads over the published release (--clobber), latest.json included.
  5. Confirm the attached assets: per platform one package (Windows .exe; Mac .dmg and .app.tar.gz; Linux .deb and .AppImage), its updater signature, the CLI binary, and in addition latest.json, the two install scripts, the USB rule, release-version.txt and SHA256SUMS (21 assets when every platform is present). A suffix such as v0.9.0-beta.1 publishes as a prerelease and does not replace the latest stable download or update feed.

Updater signing

The repository Actions secret TAURI_SIGNING_PRIVATE_KEY contains the Tauri updater private key. Its matching public key and requireSignedVersion: true are committed in tauri.conf.json. Keep an offline backup of the private key: losing or replacing it breaks the update path for installed clients. Rotate through a deliberately planned migration, never by simply regenerating the secret. This key is independent of OS code signing/notarization.

configure-updater.cjs enables updater artifacts in CI. Tag release jobs require the production secrets (TAURI_SIGNING_PRIVATE_KEY and TAURI_SIGNING_PRIVATE_KEY_PASSWORD); PR and branch builds generate disposable keys, so they exercise signing and packaging without access to the release key. Local builds do not need a key by default. CI writes private material only in the runner temporary directory and uploads only explicitly staged assets. Public PR artifacts cannot accept production updates; users should install GitHub Releases.

Install minisign to run release tests and assembly (apt-get install minisign, brew install minisign, or set MINISIGN to its executable). Assembly verifies each artifact against the committed public key and checks the authenticated version before creating any tag or draft. Tests use fresh disposable keys and reject tampered packages. latest.json contains four fixed desktop targets and immutable versioned download URLs; it and all distributed files are checksummed. Retain this desktop namespace when adding the future CLI in #16.

The native updater checks the fixed HTTPS GitHub endpoint, keeps the selected package in native memory, and exposes only status, check, preference and install commands to the main window. It verifies the signature and version before pausing USB/capture. A failed install leaves the worker available to restart lighting; successful macOS/Linux installs explicitly clean up before restarting. Windows NSIS launches the installer and exits. Automatic checks use a native timer, are disabled in debug builds, and persist their opt-out atomically in update-preferences.json, separate from portable scenes. Debian installs show manual guidance. Validate real upgrade/restart flows on each OS before calling hardware validation complete.

Only the publishing job has contents: write. It assembles assets from the current workflow run and fails on version/tag mismatch or when the Windows packages are missing; other platforms without a complete set of packages are dropped as a whole. An unpublished existing tag must resolve to the exact build commit; the workflow never moves tags. It creates a draft, uploads everything, and publishes last. A failed upload leaves a draft that can be completed by rerunning the job. Published versions are read-only no-ops unless the run is a manual dispatch with republish; other corrections need a new version. Stable releases use GitHub's version-based latest selection so a delayed older build does not displace a newer release. CI tests publishing with a fake GitHub client and both installers without installing real applications.

Asset names are deliberately versionless inside a versioned release URL, with a dynamiclight-desktop- prefix and explicit OS/architecture. scripts/release.cjs defines this contract and hashes every distributed file. The installer resolves latest once through release-version.txt, then pins the remaining downloads to that tag. Keep names backward compatible so pinned installer scripts continue to work. Ordinary Actions artifacts contain only platform packages; release-level scripts, rules and checksums are assembled by the release job.

For CLI/API issue #16, add separate CLI artifacts (for example dynamiclight-cli-<target>) and an explicit installer option when the actual CLI exists. The desktop installer owns no command alias or PATH entry. Keep GUI/CLI ownership, upgrades and uninstall behavior explicit; CLI commands should route through the authenticated local engine API rather than opening a second USB writer. Do not advertise headless control or close #16 as part of desktop distribution.

Implementation

Linux CI also exercises computer-audio input against a PulseAudio null sink, X11 capture under Xvfb, validates the udev rule, inspects the Debian package's icon/desktop entry/dependencies, and launches the packaged app on a virtual desktop. These are explicit integration tests; normal unit tests do not capture audio or a screen. Physical Linux USB, sleep, real desktop audio routing, and Wayland compositor behavior still need validation.

src-tauri/src/linux.rs owns the parec monitor subprocess (48 kHz mono float input) and systemd-logind sleep signals. Stopping capture kills and reaps the audio child before joining its reader. The sleep watcher uses a delay inhibitor when available and acknowledges the lighting worker before releasing it. Non-systemd desktops retain manual operation. X11 capture uses xcap in memory; Wayland still-image fallbacks are deliberately disabled because they may prompt repeatedly or write screenshots to disk. A future Wayland implementation should use a persistent ScreenCast portal/PipeWire stream.

  • src-tauri/src/protocol.rs implements RB commands and SC frames, split into 64-byte payloads plus report ID 0.
  • device.rs opens only the lighting interface, rejects the keyboard interface, checks complete writes, and drains replies. A single worker owns all USB operations.
  • engine.rs owns settings, capture lifetimes, effects, smoothing, reconnect, and frame pacing. It does not depend on JavaScript timers. Preview events are limited to 10 Hz and suppressed while the window is unfocused, hidden, or minimized.
  • effects.rs generates five time-based effects and four music styles. Music uses 32 logarithmic FFT bands, an RMS noise gate, fast attack, and a frame-rate-independent release envelope. The bass style responds to low-frequency energy; it does not estimate BPM. Brightness and color smoothing are applied after effect generation.
  • previewSettings validates and applies controls without disk writes. The UI keeps one preview request in flight and coalesces subsequent edits, then commits atomic settings after 650 ms of inactivity. A native Start action owns connect, power, and capture intent. Changing an active source or display explicitly restarts capture; palette/style changes keep the audio stream running. Startup restore and automatic capture resume are separate opt-ins.
  • Telemetry measures completed frame/solid refresh writes, latest write duration, failures, and age of the last successful refresh. Initialization and explicit blackout commands are excluded from these counters. Colors represent generated output; HID replies are drained but not decoded as physical LED feedback. Audio RMS is the input signal before gating; spectrum bars include gain and release.
  • capture.rs enumerates displays through xcap. Windows uses Desktop Duplication with direct edge sampling from a reusable GPU staging texture, with a small GDI thumbnail as a fallback for unsupported or rotated displays. macOS uses native xcap capture. audio.rs uses bounded sample buffers and RustFFT. The macOS bridge handles ScreenCaptureKit system audio, microphone permission, and sleep notifications.
  • The HTML/CSS/JavaScript UI sends validated commands through Tauri IPC. It has no Node.js, filesystem, shell, or remote network access. Runtime assets are embedded in the executable.

Settings are atomically saved under the OS application-data directory in com.dynamiclight.desktop/settings.json. On first launch, valid settings from the Electron prototype's dynamiclight/settings.json are imported. Legacy display IDs reset to the primary display because native display IDs differ. The old file is retained.

Interactive settings changes use patchSettings with only the edited fields. The native worker validates and merges each patch into its current settings: unrelated fields compose, and the last processed edit wins for the same field. Preview edits do not write to disk; an empty persistent patch saves current native state, so a delayed save cannot replay an old window snapshot. Studio keeps only unacknowledged edits over incoming state. Quick controls and tray adjustments use the same native path. Whole-settings commands remain for explicit replacement and compatibility tests, not routine UI edits.

Profiles store portable SceneSettings separately from the local source.screenId binding. Layout, lifecycle preferences, capture-resume consent, comfort limits and mode memory stay in installation settings. Existing profiles containing full settings load through a compatibility decoder; the next library save writes the separated representation. This is groundwork for import/export, not a public interchange format. Older app versions do not understand the new source field. Profile application still selects the saved local display and follows normal capture permission rules.

engine.rs retains one worker and one USB owner. Its settings transaction, calibration session, capture reconciliation and frame generation have separate methods within the module; the frame loop coordinates them in order. Extend these boundaries rather than adding another writer or bypassing lifecycle state.

Portable files use {format:"dynamiclight", version:1, profiles:[{id,name,settings}], palettes:[{id,name,colors}]}. They contain only validated scene controls and selected named palettes. Files are bounded to 512 KiB, 40 profiles and 24 palettes; colors use #RRGGBB with at most eight per palette. Legacy local profile-library JSON is accepted and stripped of local bindings, shortcuts and preferences during conversion. Imports preview conflicts, compare the current library with that preview, and back up profiles.before-import.json before committing through the same persistence/shortcut transaction as normal edits. Restoring that file as profiles.json while the app is closed restores the previous library. Copy is the default; replacement preserves each local profile's ID, shortcut, favorite and display binding. Imported profiles never activate lighting.

run-state.json separately persists requested on/off state on explicit Start/Stop/Disconnect, not transient sleep or USB failure. Quitting preserves that intent. lifecycle.rs combines independent sleep, lock, and display flags; clearing one cannot resume while another still applies. The native worker retries USB/capture every three seconds, polls the chosen monitor or default CPAL device every two seconds while capturing, and never substitutes another explicitly selected monitor. OS events use a separate channel so a full UI command queue cannot drop suspend notifications. On suspend the worker blacks out (if enabled) and closes USB before stream teardown. Automatic Mac capture preflights permission without prompting; a deliberate retry can request permission.

Windows uses suspend callbacks plus a hidden native window for display-power, session-lock, and topology notifications. Mac uses NSWorkspace notifications and best-effort distributed screen-lock notifications (not a documented public lock API), with initial session/display queries. Linux uses logind PrepareForSleep / LockedHint, X11 DPMS, and pactl subscribe for default-output changes. Unsupported monitoring is reported in the UI; Wayland display-only sleep remains unsupported. Physical lifecycle validation is tracked in the test checklist.

Login startup uses the Tauri autostart plugin through a narrow native command. The checkbox reads the OS registration rather than a duplicated settings flag, and failed changes restore the previous UI value. No login entry is created on installation or first launch. --autostart keeps Windows/Mac in the tray and Linux minimized.

Protocol references: HdrBridge wire format, SyncLight RB commands, and section command investigation.

Effect thumbnails call the read-only effect_previews command at up to 10 Hz. It evaluates the same Rust effect functions at 48 sample positions with the selected palette, speed, and brightness, without USB or capture. Requests stop offscreen, on blur, when hidden, or outside Effects mode. Reduced motion uses a fixed phase.