Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e294d67
Bump Avalonia 12.0.3 + System.Drawing.Common 10.0.8 (v4.1.39)
logicallysynced May 14, 2026
c0a1a74
Add QMK Raw HID keyboard provider (Beta) (v4.1.40)
logicallysynced May 14, 2026
3402e6e
Switch QMK keymap fetcher to snakkarike/qmk_firmware + chromatics-doc…
logicallysynced May 14, 2026
0e2ae4a
Untrack obj/ — stop committing build intermediates
logicallysynced May 14, 2026
f5b19a8
Bundle QMK keymap data as embedded resource — drop runtime fetch (v4.…
logicallysynced May 14, 2026
671d89e
Surface QMK discovery results to the user (v4.1.43)
logicallysynced May 14, 2026
8117d7b
Tighten QMK no-boards dialog text (v4.1.44)
logicallysynced May 14, 2026
a72964a
Rewrite v4.1.44 QMK changelog entry to remove AI tells
logicallysynced May 14, 2026
1df858e
Drop RGB.NET-Resources XML dependency, infer layouts algorithmically …
logicallysynced May 16, 2026
a823888
Add Logitech-implementation bullet to v4.1.45 changelog
logicallysynced May 16, 2026
cb82a6e
Yeelight LAN device support (Beta) (v4.2.0)
logicallysynced May 16, 2026
46ae65a
Alienware AlienFX device support (Beta) (v4.2.1)
logicallysynced May 16, 2026
4fa8f60
Tighten v4.2.1 CHANGELOG bullets to one line each
logicallysynced May 16, 2026
9f758a6
Yeelight picker, first-run network discovery, AWCC detection (v4.2.2)
logicallysynced May 16, 2026
d3dee5d
Wrap StatusText and per-item labels in adoption dialogs (v4.2.3)
logicallysynced May 16, 2026
8e28636
Phase 1: Windows Dynamic Lighting provider (foreground-only) (v4.2.4)
logicallysynced May 16, 2026
bb0c9ed
Consolidate v4.2.4 CHANGELOG bullets
logicallysynced May 16, 2026
9f20fa5
Dynamic Lighting auto-dedup + conflict popup + CI retry (v4.2.5)
logicallysynced May 16, 2026
19d843a
Dynamic Lighting polish: vendor prefix, auto-disable, bypass setting …
logicallysynced May 16, 2026
36c6887
Copy layers between devices (v4.2.7)
logicallysynced May 16, 2026
459db51
Tighten v4.2.7 CHANGELOG bullets + bump copyright year to 2026
logicallysynced May 16, 2026
c323c7f
Copy-layers per-layer rework + DL bypass default flip (v4.2.8)
logicallysynced May 16, 2026
6cd59bf
Copy-layers: Dynamic-only, Select/Clear all, gated Copy button (v4.2.9)
logicallysynced May 16, 2026
14e2dd1
Copy-layers: order rows by descending zindex (v4.2.10)
logicallysynced May 16, 2026
11bb4d3
Copy-layers: drop redundant "Dynamic:" prefix on row labels (v4.2.11)
logicallysynced May 16, 2026
dcff5a8
Settings: always show DL conflict-bypass row + scope-prefix it (v4.2.12)
logicallysynced May 16, 2026
ca0ef7b
DL: register sparse signed AppX for background lighting access (v4.2.13)
logicallysynced May 16, 2026
0a333ff
CI: matrix on windows-2022 + windows-2025 + sparse-package manifest c…
logicallysynced May 16, 2026
0c08d85
CI: restrict GITHUB_TOKEN to contents: read
logicallysynced May 16, 2026
0a44baa
CI: dedupe push + pull_request runs via concurrency group
logicallysynced May 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 83 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,43 @@ on:
pull_request:
branches: [master, 'chromatics-4.0.x', 'chromatics-4.*']

# Minimum-privilege GITHUB_TOKEN: the workflow only checks out the repo and
# runs build/test/manifest-validation locally on the runner — it never calls
# the GitHub API to write commits, comments, releases, packages, etc. So
# contents: read is the only permission needed. Explicit block also silences
# the GitHub Advanced Security warning about unrestricted token scope.
permissions:
contents: read

# Deduplicate push + pull_request runs on the same branch. A commit pushed to
# a PR branch fires both a `push` event and a `pull_request:synchronize`
# event; with the same concurrency group key (resolved from
# pull_request.head.ref on PR events and ref_name on push events), the
# newer run cancels the older one, so we get a single CI completion per
# commit instead of two parallel ones.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.ref_name }}
cancel-in-progress: true

jobs:
build-and-test:
# GDI + WASAPI loopback means the project targets
# net10.0-windows7.0 and can only build on a Windows runner.
runs-on: windows-latest
# GDI + WASAPI loopback + Windows.Devices.Lights.LampArray plus the
# sparse-package PackageManager APIs mean the project targets
# net10.0-windows10.0.19041.0 and can only build on a Windows runner.
#
# Matrix on both currently-supported GitHub-hosted Windows runners so
# platform-specific regressions surface early:
# - windows-2022 = Server 2022, build 20348 (~Win11 21H2 equivalent)
# - windows-2025 = Server 2025, build 26100 (~Win11 24H2 equivalent)
# windows-2019 (build 17763) was retired by GitHub Actions in mid-2025, so
# SupportedOSPlatformVersion=10.0.17763.0 cannot be exercised in CI today;
# OS-guarded codepaths (e.g. SparsePackageRegistrar skipping on <19041) are
# validated locally / via user-reported issues.
strategy:
fail-fast: false
matrix:
runner: [windows-2022, windows-2025]
runs-on: ${{ matrix.runner }}

steps:
- name: Checkout
Expand All @@ -21,11 +53,58 @@ jobs:
with:
dotnet-version: '10.0.x'

# nuget.org occasionally returns 502 Bad Gateway mid-restore (saw
# one such failure on commit bb0c9ed, second run on the same SHA
# succeeded immediately). Retry up to 3 times before failing the
# build to absorb transient feed flakiness without needing a
# manual re-run.
- name: Restore
run: dotnet restore Chromatics.sln
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
shell: pwsh
command: dotnet restore Chromatics.sln

- name: Build (Release, no restore)
run: dotnet build Chromatics.sln --configuration Release --no-restore --nologo

- name: Test (Release, no build)
run: dotnet test Chromatics.Tests/Chromatics.Tests.csproj --configuration Release --no-build --nologo --verbosity normal

# Validate the Dynamic Lighting sparse-package manifest by packing it
# with makeappx (no signing, /nv = no schema validation skipping). The
# real publish.py pipeline does this against a freshly-bumped version
# number; here we substitute 0.0.0.0 just to satisfy the 4-part
# version requirement. Catches schema regressions (typo'd capability
# names, namespace drift, missing assets) before they reach a user,
# because registration failures at runtime are silent — the AppX
# subsystem reports them via DeploymentResult.ErrorText into the log,
# not via a dialog or exception. SDK 10 ships on both runners.
- name: Validate sparse-package manifest (makeappx)
shell: pwsh
run: |
$sdk = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin' -Directory `
| Where-Object { $_.Name -match '^10\.' } `
| Sort-Object Name -Descending `
| Select-Object -First 1
if (-not $sdk) { throw "Windows 10 SDK not found on runner" }
$makeappx = Join-Path $sdk.FullName 'x64\makeappx.exe'
if (-not (Test-Path $makeappx)) { throw "makeappx.exe not found at $makeappx" }
Write-Host "Using makeappx at $makeappx"

$src = 'Chromatics/Resources/SparsePackage'
$work = Join-Path $env:RUNNER_TEMP 'sparse-validate'
if (Test-Path $work) { Remove-Item $work -Recurse -Force }
Copy-Item $src $work -Recurse

# Substitute the {{VERSION}} placeholder so the manifest parses.
$manifest = Join-Path $work 'AppxManifest.xml'
(Get-Content $manifest -Raw) -replace '\{\{VERSION\}\}', '0.0.0.0' `
| Set-Content $manifest -NoNewline

$out = Join-Path $env:RUNNER_TEMP 'Chromatics.test.appx'
& $makeappx pack /d $work /p $out /o /nv
if ($LASTEXITCODE -ne 0) { throw "makeappx pack failed with exit code $LASTEXITCODE" }
Write-Host "Sparse package validated: $out"
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

All notable changes to Chromatics are documented here.

## 4.2.13

- **New:** Windows Dynamic Lighting (Beta). Controls compatible devices during gameplay, not only while Chromatics has focus. The in-game part needs Windows 11 22H2 or newer; on older builds only the foreground side works. After enabling the provider in Chromatics, open Settings → Personalization → Dynamic Lighting → Background light control in Windows and drag Chromatics to the top. Chromatics only appears in that Windows list while its Dynamic Lighting provider is on; turning it off or uninstalling Chromatics removes the entry.
- **New:** Yeelight device support (Beta).
- **New:** Alienware LightFX device support (Beta).
- **New:** Copy layers between devices. A small copy icon next to the device brightness button on the Mappings tab opens a dialog that duplicates every layer from one device onto another.
- Hue, LIFX and Yeelight tiles added to the first-run wizard, each with their own discovery flow.
- PlayStation, Hue & LIFX devices are no longer classed as Beta.
- Minor fixes to Logitech implementation.
- Some keyboards and headsets used to show up in Chromatics without any usable LEDs. They now get a default key grid filled in automatically, so layers can paint them just like any other device.
- Bumped the project's minimum Windows target to Windows 10 1809 (build 17763).
- Chromatics installer is now officially signed for extra security.

## 4.1.44

- **New:** QMK Raw HID keyboard support (Beta). Covers custom keyboards from NovelKeys, KBDFans, Drop, GMMK, Glorious, and any other brand running QMK firmware with Raw HID enabled. Enable it from Settings → Device Providers or pick it on the first-run device selector. Chromatics auto-detects compatible boards over USB and adopts them with no firmware flashing or extra software required. The provider drives per-key lighting through the OpenRGB-QMK plugin when the firmware has it installed; otherwise it controls the firmware's built-in RGB matrix base colour and effect mode via VIA. A pre-built key layout database covering 2650 QMK boards ships with Chromatics, so the Highlight and Keybind layers map to the correct physical keys without manual setup.
- Updated dependency libraries to latest version

## 4.1.38

- Added Auto-discovery for Hue bridges.
Expand Down
27 changes: 22 additions & 5 deletions Chromatics.DecoratorHarnessUI/Chromatics.DecoratorHarnessUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows7.0</TargetFramework>
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
Expand All @@ -16,11 +16,28 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="12.0.2" />
<PackageReference Include="Avalonia.Desktop" Version="12.0.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.2" />
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="12.0.2" />
<PackageReference Include="Avalonia" Version="12.0.3" />
<PackageReference Include="Avalonia.Desktop" Version="12.0.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.3" />
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="12.0.3" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
</ItemGroup>

<!--
Native SDK wrappers (LogitechLedEnginesWrapper.dll, CUESDK.dll, etc.)
are not shipped in the RGB.NET NuGet packages — they're obtained from
each vendor's SDK installer and live in the main Chromatics project's
bin/<config>/<tfm>/x64 output. Mirror them into the harness's bin so
the harness can run the same set of providers without asking the user
to copy files manually after every clean rebuild.
-->
<Target Name="CopyNativeSdkDlls" AfterTargets="Build">
<ItemGroup>
<_NativeSdkDlls Include="$(MSBuildThisFileDirectory)..\Chromatics\bin\$(Configuration)\$(TargetFramework)\x64\*.dll" />
<_NativeSdkDllsX86 Include="$(MSBuildThisFileDirectory)..\Chromatics\bin\$(Configuration)\$(TargetFramework)\x86\*.dll" />
</ItemGroup>
<Copy SourceFiles="@(_NativeSdkDlls)" DestinationFolder="$(OutputPath)x64" SkipUnchangedFiles="true" Condition="@(_NativeSdkDlls -> Count()) &gt; 0" />
<Copy SourceFiles="@(_NativeSdkDllsX86)" DestinationFolder="$(OutputPath)x86" SkipUnchangedFiles="true" Condition="@(_NativeSdkDllsX86 -> Count()) &gt; 0" />
</Target>

</Project>
108 changes: 91 additions & 17 deletions Chromatics.DecoratorHarnessUI/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,32 @@ private async Task LoadProvider()
{
var provider = SelectedProvider.Factory();

// Native-DLL providers (Logitech, Corsair, etc.) resolve their
// PossibleX64NativePaths entries against the current working
// directory, which is the workspace root when the harness
// launches from VS or `dotnet run` — not the harness's own bin
// folder where the DLLs actually sit. Pre-pend an absolute path
// computed from the harness assembly location so the SDK loader
// finds the wrapper without the user juggling cwd. Mirrors what
// the main app does for Corsair in RGBController.Setup.
string asmDir = System.IO.Path.GetDirectoryName(
System.Reflection.Assembly.GetExecutingAssembly().Location) ?? "";
if (!string.IsNullOrEmpty(asmDir))
{
if (provider is LogitechDeviceProvider)
{
LogitechDeviceProvider.PossibleX64NativePaths.Insert(
0, System.IO.Path.Combine(asmDir, "x64", "LogitechLedEnginesWrapper.dll"));
LogitechDeviceProvider.PossibleX86NativePaths.Insert(
0, System.IO.Path.Combine(asmDir, "x86", "LogitechLedEnginesWrapper.dll"));
}
else if (provider is CorsairDeviceProvider)
{
CorsairDeviceProvider.PossibleX64NativePaths.Insert(
0, System.IO.Path.Combine(asmDir, "x64", "CUESDK.dll"));
}
}

// LIFX is the only provider in the harness that needs an
// adoption gate — the LAN protocol has no concept of "all
// devices on the segment", so the user has to pick which
Expand Down Expand Up @@ -137,20 +163,60 @@ private async Task LoadProvider()
}
}

provider.Initialize(throwExceptions: false);
_surface.Load(provider);
_loadedProvider = provider;

Devices.Clear();
foreach (var d in _surface.Devices)
Devices.Add(new DeviceItem(d.DeviceInfo.DeviceName, d.DeviceInfo.DeviceType.ToString(), d));

StartSurfaceTick();
ProviderLoaded = true;
ProviderStatus = $"{Devices.Count} device(s) loaded";
// Now that the surface has devices, evaluate the WASD highlight
// hook so the toggle works even before the user starts an effect.
RefreshWasdOverlay();
// Capture provider-side exceptions before we try to Initialize so
// the harness surfaces them in the status panel rather than
// silently coming up with zero devices. RGB.NET reports SDK
// failures (missing native DLL, vendor service not running,
// exclusive-mode conflict) through this event and would
// otherwise drop them on the floor when throwExceptions:false.
var providerErrors = new System.Text.StringBuilder();
void OnProviderException(object? s, ExceptionEventArgs e)
=> providerErrors.AppendLine(e.Exception?.Message ?? e.Exception?.GetType().Name ?? "(null)");
provider.Exception += OnProviderException;

try
{
provider.Initialize(throwExceptions: false);
_surface.Load(provider);
_loadedProvider = provider;

// Same Logitech-layout fixup the main app applies in
// RGBController.DevicesChanged.Added — rebinds Led.Location
// for Logitech keyboards/mice so position-aware decorators
// (conical gradients in particular) render correctly. RGB.NET's
// LogitechPerKeyRGBDevice ships every LED at Y=0 by default.
string asmDirForLayout = System.IO.Path.GetDirectoryName(
System.Reflection.Assembly.GetExecutingAssembly().Location) ?? "";
foreach (var d in _surface.Devices)
Chromatics.Helpers.LogitechLayoutFixup.Apply(d, asmDirForLayout);

Devices.Clear();
foreach (var d in _surface.Devices)
Devices.Add(new DeviceItem(d.DeviceInfo.DeviceName, d.DeviceInfo.DeviceType.ToString(), d));

StartSurfaceTick();
ProviderLoaded = true;

if (Devices.Count > 0)
{
ProviderStatus = $"{Devices.Count} device(s) loaded";
}
else if (providerErrors.Length > 0)
{
ProviderStatus = $"0 devices. SDK said: {providerErrors.ToString().Trim()}";
}
else
{
ProviderStatus = "0 devices. SDK initialized but enumerated nothing — vendor service may not be running, or no supported hardware connected.";
}
// Now that the surface has devices, evaluate the WASD highlight
// hook so the toggle works even before the user starts an effect.
RefreshWasdOverlay();
}
finally
{
provider.Exception -= OnProviderException;
}
}
catch (Exception ex)
{
Expand All @@ -170,18 +236,18 @@ private async Task LoadProvider()
"Chromatics.DecoratorHarnessUI",
"lifx-adopted.json");

private static List<Chromatics.Models.LifxAdoptedDevice> LoadHarnessLifxAdoptions()
private static List<Chromatics.Extensions.RGB.NET.Devices.LIFX.LifxAdoptedDevice> LoadHarnessLifxAdoptions()
{
try
{
if (!System.IO.File.Exists(LifxAdoptionsPath)) return new();
var json = System.IO.File.ReadAllText(LifxAdoptionsPath);
return System.Text.Json.JsonSerializer.Deserialize<List<Chromatics.Models.LifxAdoptedDevice>>(json) ?? new();
return System.Text.Json.JsonSerializer.Deserialize<List<Chromatics.Extensions.RGB.NET.Devices.LIFX.LifxAdoptedDevice>>(json) ?? new();
}
catch { return new(); }
}

private static void SaveHarnessLifxAdoptions(IEnumerable<Chromatics.Models.LifxAdoptedDevice> adoptions)
private static void SaveHarnessLifxAdoptions(IEnumerable<Chromatics.Extensions.RGB.NET.Devices.LIFX.LifxAdoptedDevice> adoptions)
{
try
{
Expand Down Expand Up @@ -2247,5 +2313,13 @@ public void Dispose()
_surfaceTimer?.Stop();
_surfaceTimer?.Dispose();
try { _surface.Dispose(); } catch { }

// Provider disposal stops the per-provider UpdateTrigger threads.
// Each provider's trigger is a Task.Factory.StartNew(... LongRunning ...)
// which spawns a foreground thread, and surface.Dispose only kills
// triggers explicitly registered via RegisterUpdateTrigger — not the
// providers' internal ones. Without this, the harness window closes
// but the process hangs around forever.
try { _loadedProvider?.Dispose(); } catch { }
}
}
2 changes: 1 addition & 1 deletion Chromatics.Tests/Chromatics.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0-windows7.0</TargetFramework>
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand Down
Loading
Loading