From e294d671665aded1c6fa79ae9890f15bd5c3cc6f Mon Sep 17 00:00:00 2001 From: Danielle Date: Thu, 14 May 2026 20:21:20 +1000 Subject: [PATCH 01/31] Bump Avalonia 12.0.3 + System.Drawing.Common 10.0.8 (v4.1.39) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Dependabot updates rolled into a single bump: - PR #190: Avalonia + Avalonia.Desktop + Avalonia.Themes.Fluent + Avalonia.Fonts.Inter + Avalonia.Controls.ColorPicker 12.0.2 → 12.0.3. Applied to both Chromatics and Chromatics.DecoratorHarnessUI for parity. - PR #192: System.Drawing.Common 10.0.7 → 10.0.8. Both PRs will auto-close when Dependabot reconciles against the new manifest. Build clean, 130/130 tests pass on both packages. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 4 + .../Chromatics.DecoratorHarnessUI.csproj | 8 +- Chromatics/Chromatics.csproj | 14 +- .../obj/Chromatics.csproj.nuget.dgspec.json | 12 +- .../obj/Chromatics.csproj.nuget.g.props | 4 +- .../obj/Chromatics.csproj.nuget.g.targets | 2 +- Chromatics/obj/project.assets.json | 218 +++++++++--------- 7 files changed, 133 insertions(+), 129 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c85beb23..9086d6dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to Chromatics are documented here. +## 4.1.39 + +- Updated underlying UI dependencies (Avalonia 12.0.3, System.Drawing.Common 10.0.8) for stability and bug fixes. No functional changes. + ## 4.1.38 - Added Auto-discovery for Hue bridges. diff --git a/Chromatics.DecoratorHarnessUI/Chromatics.DecoratorHarnessUI.csproj b/Chromatics.DecoratorHarnessUI/Chromatics.DecoratorHarnessUI.csproj index 61239982..cdad8a4c 100644 --- a/Chromatics.DecoratorHarnessUI/Chromatics.DecoratorHarnessUI.csproj +++ b/Chromatics.DecoratorHarnessUI/Chromatics.DecoratorHarnessUI.csproj @@ -16,10 +16,10 @@ - - - - + + + + diff --git a/Chromatics/Chromatics.csproj b/Chromatics/Chromatics.csproj index 4a3179a4..f97c609b 100644 --- a/Chromatics/Chromatics.csproj +++ b/Chromatics/Chromatics.csproj @@ -4,7 +4,7 @@ WinExe net10.0-windows7.0 Chromatics.Program - 4.1.38.0 + 4.1.39.0 Danielle Thompson app.manifest logicallysynced 2026 @@ -48,12 +48,12 @@ - - - - + + + + - + @@ -66,7 +66,7 @@ - + diff --git a/Chromatics/obj/Chromatics.csproj.nuget.dgspec.json b/Chromatics/obj/Chromatics.csproj.nuget.dgspec.json index 2051f66e..a04729ac 100644 --- a/Chromatics/obj/Chromatics.csproj.nuget.dgspec.json +++ b/Chromatics/obj/Chromatics.csproj.nuget.dgspec.json @@ -53,11 +53,11 @@ "dependencies": { "Avalonia": { "target": "Package", - "version": "[12.0.2, )" + "version": "[12.0.3, )" }, "Avalonia.Controls.ColorPicker": { "target": "Package", - "version": "[12.0.2, )" + "version": "[12.0.3, )" }, "Avalonia.Controls.DataGrid": { "target": "Package", @@ -65,15 +65,15 @@ }, "Avalonia.Desktop": { "target": "Package", - "version": "[12.0.2, )" + "version": "[12.0.3, )" }, "Avalonia.Fonts.Inter": { "target": "Package", - "version": "[12.0.2, )" + "version": "[12.0.3, )" }, "Avalonia.Themes.Fluent": { "target": "Package", - "version": "[12.0.2, )" + "version": "[12.0.3, )" }, "CommunityToolkit.Mvvm": { "target": "Package", @@ -181,7 +181,7 @@ }, "System.Drawing.Common": { "target": "Package", - "version": "[10.0.7, )" + "version": "[10.0.8, )" }, "Velopack": { "target": "Package", diff --git a/Chromatics/obj/Chromatics.csproj.nuget.g.props b/Chromatics/obj/Chromatics.csproj.nuget.g.props index 8a028ee4..2728e425 100644 --- a/Chromatics/obj/Chromatics.csproj.nuget.g.props +++ b/Chromatics/obj/Chromatics.csproj.nuget.g.props @@ -16,12 +16,12 @@ - + C:\Users\Hanielle\.nuget\packages\sentry\6.5.0 C:\Users\Hanielle\.nuget\packages\avalonia.buildservices\11.3.2 - C:\Users\Hanielle\.nuget\packages\avalonia\12.0.2 + C:\Users\Hanielle\.nuget\packages\avalonia\12.0.3 \ No newline at end of file diff --git a/Chromatics/obj/Chromatics.csproj.nuget.g.targets b/Chromatics/obj/Chromatics.csproj.nuget.g.targets index 3c4ff808..9c67a8ce 100644 --- a/Chromatics/obj/Chromatics.csproj.nuget.g.targets +++ b/Chromatics/obj/Chromatics.csproj.nuget.g.targets @@ -5,7 +5,7 @@ - + diff --git a/Chromatics/obj/project.assets.json b/Chromatics/obj/project.assets.json index ad8d242b..fe1dc6f5 100644 --- a/Chromatics/obj/project.assets.json +++ b/Chromatics/obj/project.assets.json @@ -2,11 +2,11 @@ "version": 3, "targets": { "net10.0-windows7.0": { - "Avalonia/12.0.2": { + "Avalonia/12.0.3": { "type": "package", "dependencies": { "Avalonia.BuildServices": "11.3.2", - "Avalonia.Remote.Protocol": "12.0.2", + "Avalonia.Remote.Protocol": "12.0.3", "MicroCom.Runtime": "0.11.4" }, "compile": { @@ -107,11 +107,11 @@ "buildTransitive/Avalonia.BuildServices.targets": {} } }, - "Avalonia.Controls.ColorPicker/12.0.2": { + "Avalonia.Controls.ColorPicker/12.0.3": { "type": "package", "dependencies": { - "Avalonia": "12.0.2", - "Avalonia.Remote.Protocol": "12.0.2" + "Avalonia": "12.0.3", + "Avalonia.Remote.Protocol": "12.0.3" }, "compile": { "lib/net10.0/Avalonia.Controls.ColorPicker.dll": { @@ -140,15 +140,15 @@ } } }, - "Avalonia.Desktop/12.0.2": { + "Avalonia.Desktop/12.0.3": { "type": "package", "dependencies": { - "Avalonia": "12.0.2", - "Avalonia.HarfBuzz": "12.0.2", - "Avalonia.Native": "12.0.2", - "Avalonia.Skia": "12.0.2", - "Avalonia.Win32": "12.0.2", - "Avalonia.X11": "12.0.2" + "Avalonia": "12.0.3", + "Avalonia.HarfBuzz": "12.0.3", + "Avalonia.Native": "12.0.3", + "Avalonia.Skia": "12.0.3", + "Avalonia.Win32": "12.0.3", + "Avalonia.X11": "12.0.3" }, "compile": { "lib/net10.0/Avalonia.Desktop.dll": { @@ -161,10 +161,10 @@ } } }, - "Avalonia.Fonts.Inter/12.0.2": { + "Avalonia.Fonts.Inter/12.0.3": { "type": "package", "dependencies": { - "Avalonia": "12.0.2" + "Avalonia": "12.0.3" }, "compile": { "lib/net10.0/Avalonia.Fonts.Inter.dll": { @@ -177,10 +177,10 @@ } } }, - "Avalonia.FreeDesktop/12.0.2": { + "Avalonia.FreeDesktop/12.0.3": { "type": "package", "dependencies": { - "Avalonia": "12.0.2", + "Avalonia": "12.0.3", "Tmds.DBus.Protocol": "0.92.0" }, "compile": { @@ -194,10 +194,10 @@ } } }, - "Avalonia.FreeDesktop.AtSpi/12.0.2": { + "Avalonia.FreeDesktop.AtSpi/12.0.3": { "type": "package", "dependencies": { - "Avalonia": "12.0.2" + "Avalonia": "12.0.3" }, "compile": { "lib/net10.0/Avalonia.FreeDesktop.AtSpi.dll": { @@ -210,10 +210,10 @@ } } }, - "Avalonia.HarfBuzz/12.0.2": { + "Avalonia.HarfBuzz/12.0.3": { "type": "package", "dependencies": { - "Avalonia": "12.0.2", + "Avalonia": "12.0.3", "HarfBuzzSharp": "8.3.1.3", "HarfBuzzSharp.NativeAssets.Linux": "8.3.1.3", "HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.3" @@ -229,10 +229,10 @@ } } }, - "Avalonia.Native/12.0.2": { + "Avalonia.Native/12.0.3": { "type": "package", "dependencies": { - "Avalonia": "12.0.2" + "Avalonia": "12.0.3" }, "compile": { "lib/net10.0/Avalonia.Native.dll": { @@ -251,7 +251,7 @@ } } }, - "Avalonia.Remote.Protocol/12.0.2": { + "Avalonia.Remote.Protocol/12.0.3": { "type": "package", "compile": { "lib/net10.0/Avalonia.Remote.Protocol.dll": { @@ -264,10 +264,10 @@ } } }, - "Avalonia.Skia/12.0.2": { + "Avalonia.Skia/12.0.3": { "type": "package", "dependencies": { - "Avalonia": "12.0.2", + "Avalonia": "12.0.3", "HarfBuzzSharp": "8.3.1.3", "HarfBuzzSharp.NativeAssets.Linux": "8.3.1.3", "HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.3", @@ -286,10 +286,10 @@ } } }, - "Avalonia.Themes.Fluent/12.0.2": { + "Avalonia.Themes.Fluent/12.0.3": { "type": "package", "dependencies": { - "Avalonia": "12.0.2" + "Avalonia": "12.0.3" }, "compile": { "lib/net10.0/Avalonia.Themes.Fluent.dll": { @@ -302,10 +302,10 @@ } } }, - "Avalonia.Win32/12.0.2": { + "Avalonia.Win32/12.0.3": { "type": "package", "dependencies": { - "Avalonia": "12.0.2", + "Avalonia": "12.0.3", "Avalonia.Angle.Windows.Natives": "2.1.25547.20250602" }, "compile": { @@ -325,13 +325,13 @@ } } }, - "Avalonia.X11/12.0.2": { + "Avalonia.X11/12.0.3": { "type": "package", "dependencies": { - "Avalonia": "12.0.2", - "Avalonia.FreeDesktop": "12.0.2", - "Avalonia.FreeDesktop.AtSpi": "12.0.2", - "Avalonia.Skia": "12.0.2" + "Avalonia": "12.0.3", + "Avalonia.FreeDesktop": "12.0.3", + "Avalonia.FreeDesktop.AtSpi": "12.0.3", + "Avalonia.Skia": "12.0.3" }, "compile": { "lib/net10.0/Avalonia.X11.dll": { @@ -856,7 +856,7 @@ "buildTransitive/netcoreapp3.1/_._": {} } }, - "Microsoft.Win32.SystemEvents/10.0.7": { + "Microsoft.Win32.SystemEvents/10.0.8": { "type": "package", "compile": { "lib/net10.0/Microsoft.Win32.SystemEvents.dll": { @@ -1526,10 +1526,10 @@ } } }, - "System.Drawing.Common/10.0.7": { + "System.Drawing.Common/10.0.8": { "type": "package", "dependencies": { - "Microsoft.Win32.SystemEvents": "10.0.7" + "Microsoft.Win32.SystemEvents": "10.0.8" }, "compile": { "lib/net10.0/System.Drawing.Common.dll": { @@ -1589,10 +1589,10 @@ } }, "libraries": { - "Avalonia/12.0.2": { - "sha512": "852xwfNNAUWE+aKWFackMALAKofcpKnM2g4PN8VKfxG1SO8Stlih7d6QFCbjQl46vuR9WBw/ZVGwn4Ovbj/oxQ==", + "Avalonia/12.0.3": { + "sha512": "OVAzdZB5T/QIOEpw/WmQ0ZJM13BMmLO7RqR5z+lZtBMuStK63W68SV/Q+PNn/GdFEC3Ab8eQhH2FMb8FhNLK+w==", "type": "package", - "path": "avalonia/12.0.2", + "path": "avalonia/12.0.3", "hasTools": true, "files": [ ".nupkg.metadata", @@ -1602,7 +1602,7 @@ "analyzers/dotnet/cs/Avalonia.Analyzers.CodeFixes.CSharp.dll", "analyzers/dotnet/cs/Avalonia.Analyzers.VisualBasic.dll", "analyzers/dotnet/cs/Avalonia.Generators.dll", - "avalonia.12.0.2.nupkg.sha512", + "avalonia.12.0.3.nupkg.sha512", "avalonia.nuspec", "build/Avalonia.Generators.props", "build/Avalonia.props", @@ -1751,15 +1751,15 @@ "tools/netstandard2.0/runtimeconfig.json" ] }, - "Avalonia.Controls.ColorPicker/12.0.2": { - "sha512": "z0NTksfW9B5xNYhFkhMN285SXg62xQ2Vzzmpxsiq5vv036t2aMgHeIE3tomw99sORnOr0EcpAPgycwzIQ7WRIw==", + "Avalonia.Controls.ColorPicker/12.0.3": { + "sha512": "Hb6AyDVpMrNaV8T14DJ9WjV64x4SXpZzhZag212Uyfyc+ZYqpbBWaptGRlalwP2hdML++ThTTqGRvbYyY6A2Vw==", "type": "package", - "path": "avalonia.controls.colorpicker/12.0.2", + "path": "avalonia.controls.colorpicker/12.0.3", "files": [ ".nupkg.metadata", ".signature.p7s", "Icon.png", - "avalonia.controls.colorpicker.12.0.2.nupkg.sha512", + "avalonia.controls.colorpicker.12.0.3.nupkg.sha512", "avalonia.controls.colorpicker.nuspec", "lib/net10.0/Avalonia.Controls.ColorPicker.dll", "lib/net10.0/Avalonia.Controls.ColorPicker.xml", @@ -1784,15 +1784,15 @@ "readme.md" ] }, - "Avalonia.Desktop/12.0.2": { - "sha512": "7Hs7AsEoLh4UHNZnwaAZ5d4db2X0BjdLePuev526tm2ZJIOZGLUxKHvY1GcrWIDO9NBw4Iu3WXBUvsFPZ6vXlQ==", + "Avalonia.Desktop/12.0.3": { + "sha512": "1WT6o5+HFQivTSBqqeyKWUQC2SbesgtlI6+ZT8JN+Rg9YarwWOw3DuPdHIYb2jkLzU+wjSkmBXRi7Nox7hfBOw==", "type": "package", - "path": "avalonia.desktop/12.0.2", + "path": "avalonia.desktop/12.0.3", "files": [ ".nupkg.metadata", ".signature.p7s", "Icon.png", - "avalonia.desktop.12.0.2.nupkg.sha512", + "avalonia.desktop.12.0.3.nupkg.sha512", "avalonia.desktop.nuspec", "lib/net10.0/Avalonia.Desktop.dll", "lib/net10.0/Avalonia.Desktop.xml", @@ -1800,15 +1800,15 @@ "lib/net8.0/Avalonia.Desktop.xml" ] }, - "Avalonia.Fonts.Inter/12.0.2": { - "sha512": "2QEaZc52qqO3W58LfGkcV9V3lX4A6uyIt8zJaSb2ULyeyw9CwdLurJ/bwDcYO3M1dwV7LMDcqjOTuqCf8TIZ0Q==", + "Avalonia.Fonts.Inter/12.0.3": { + "sha512": "UWB0YZ15H0RKkkNgywtsO0aee3fcd6C0KQlG72QuDjoc7UIiwAgIrwu/LXkxCmXJcoh/xK/EWeclkwXEr7E50Q==", "type": "package", - "path": "avalonia.fonts.inter/12.0.2", + "path": "avalonia.fonts.inter/12.0.3", "files": [ ".nupkg.metadata", ".signature.p7s", "Icon.png", - "avalonia.fonts.inter.12.0.2.nupkg.sha512", + "avalonia.fonts.inter.12.0.3.nupkg.sha512", "avalonia.fonts.inter.nuspec", "lib/net10.0/Avalonia.Fonts.Inter.dll", "lib/net10.0/Avalonia.Fonts.Inter.xml", @@ -1816,15 +1816,15 @@ "lib/net8.0/Avalonia.Fonts.Inter.xml" ] }, - "Avalonia.FreeDesktop/12.0.2": { - "sha512": "KHionmetHeH7g4M653eNPD+GKFHTpX0xd6daBt+Gj03Y9ztEjD4B/P34bCrG81f+KK8evcx3ZygAC79O4M05GQ==", + "Avalonia.FreeDesktop/12.0.3": { + "sha512": "t6qSD9slmHDlkBScuOecf5LZHGEhl57d8DQ0raXWXgm3mXkSZc2DTfkGBJajovrQdeuaISlu6sDHzYAs8Zv/iQ==", "type": "package", - "path": "avalonia.freedesktop/12.0.2", + "path": "avalonia.freedesktop/12.0.3", "files": [ ".nupkg.metadata", ".signature.p7s", "Icon.png", - "avalonia.freedesktop.12.0.2.nupkg.sha512", + "avalonia.freedesktop.12.0.3.nupkg.sha512", "avalonia.freedesktop.nuspec", "lib/net10.0/Avalonia.FreeDesktop.dll", "lib/net10.0/Avalonia.FreeDesktop.xml", @@ -1832,15 +1832,15 @@ "lib/net8.0/Avalonia.FreeDesktop.xml" ] }, - "Avalonia.FreeDesktop.AtSpi/12.0.2": { - "sha512": "tuo8ry4mFhns6p31RHvCTineppfc8cI0i6J8ifEnmQ5FSHVATTna1tBjyi26ri8qPEX6nmcVgifKTewfpZEMmw==", + "Avalonia.FreeDesktop.AtSpi/12.0.3": { + "sha512": "+i58/6jM/YrjfPA3vXfHbEeLNPC2BY1lXGZ4HtZU4IHZ1XkP6xGBGEqwABuruAlSpLoyrE9LVMZ0Uqg3pYOtiQ==", "type": "package", - "path": "avalonia.freedesktop.atspi/12.0.2", + "path": "avalonia.freedesktop.atspi/12.0.3", "files": [ ".nupkg.metadata", ".signature.p7s", "Icon.png", - "avalonia.freedesktop.atspi.12.0.2.nupkg.sha512", + "avalonia.freedesktop.atspi.12.0.3.nupkg.sha512", "avalonia.freedesktop.atspi.nuspec", "lib/net10.0/Avalonia.FreeDesktop.AtSpi.dll", "lib/net10.0/Avalonia.FreeDesktop.AtSpi.xml", @@ -1848,15 +1848,15 @@ "lib/net8.0/Avalonia.FreeDesktop.AtSpi.xml" ] }, - "Avalonia.HarfBuzz/12.0.2": { - "sha512": "HGqFe/HmWHQGvwtji7827/RDtEgQxvm3zdnCFl/P6XAwnqek8MoJpnISRTid76XzTBDtNlVqp7qjOujSIE0QCw==", + "Avalonia.HarfBuzz/12.0.3": { + "sha512": "6R8pHRC9iDrAgT7AD/A3mE6hAPYXf66Ql5kWV046msSWF9ntYwuhNmJwlWeFwUVhb7nXThyTOIfUd3WV0GFXXA==", "type": "package", - "path": "avalonia.harfbuzz/12.0.2", + "path": "avalonia.harfbuzz/12.0.3", "files": [ ".nupkg.metadata", ".signature.p7s", "Icon.png", - "avalonia.harfbuzz.12.0.2.nupkg.sha512", + "avalonia.harfbuzz.12.0.3.nupkg.sha512", "avalonia.harfbuzz.nuspec", "lib/net10.0/Avalonia.HarfBuzz.dll", "lib/net10.0/Avalonia.HarfBuzz.xml", @@ -1864,15 +1864,15 @@ "lib/net8.0/Avalonia.HarfBuzz.xml" ] }, - "Avalonia.Native/12.0.2": { - "sha512": "ar8S5eXQY8ttZ3YG5dbtinbC2ltybJU89iwgkpAlLKkV3V7u+4ZXX13o6ilplVyd1lVVSfXgOMvfSMnKQm02Bg==", + "Avalonia.Native/12.0.3": { + "sha512": "o+36bdY62STT9SjoEIlp/lSHVrQH8uC15gMUA4JMszz4Q4r5rgVMVTsorsIAurdawJ1LaBKIzvbDHyyiRjXROg==", "type": "package", - "path": "avalonia.native/12.0.2", + "path": "avalonia.native/12.0.3", "files": [ ".nupkg.metadata", ".signature.p7s", "Icon.png", - "avalonia.native.12.0.2.nupkg.sha512", + "avalonia.native.12.0.3.nupkg.sha512", "avalonia.native.nuspec", "lib/net10.0/Avalonia.Native.dll", "lib/net10.0/Avalonia.Native.xml", @@ -1881,15 +1881,15 @@ "runtimes/osx/native/libAvaloniaNative.dylib" ] }, - "Avalonia.Remote.Protocol/12.0.2": { - "sha512": "mlKTvn0cAEQVkRMJunLzDNugZRbtZIksT5yu+XgcAlOvqTHGyZY+mGP+4/+1IwakZx9nrz6SGwI5yrzNT5g0TQ==", + "Avalonia.Remote.Protocol/12.0.3": { + "sha512": "NHvbiGC461oB3DXt8qgLNN+QfcYARcSxY7diyic9R7u6jQA4bc+ZbjEQKX8y8WyF1vOssedRmuOeTvRXlXbF9Q==", "type": "package", - "path": "avalonia.remote.protocol/12.0.2", + "path": "avalonia.remote.protocol/12.0.3", "files": [ ".nupkg.metadata", ".signature.p7s", "Icon.png", - "avalonia.remote.protocol.12.0.2.nupkg.sha512", + "avalonia.remote.protocol.12.0.3.nupkg.sha512", "avalonia.remote.protocol.nuspec", "lib/net10.0/Avalonia.Remote.Protocol.dll", "lib/net10.0/Avalonia.Remote.Protocol.xml", @@ -1899,15 +1899,15 @@ "lib/netstandard2.0/Avalonia.Remote.Protocol.xml" ] }, - "Avalonia.Skia/12.0.2": { - "sha512": "rtnB7M3VymQY4ErpPMot37bNoZ67mTk4tCb1mJjQCnTkCYgTx/o0yDvPPBFgLZAmcmHp00BxT5w12GkXTQjDMQ==", + "Avalonia.Skia/12.0.3": { + "sha512": "Q0PYiN/B5dZumh89RcDOgbsceh7aUvTGVCKjiq+kcBmVZcvcygHpSRsDJKKUfFHBQLJLtQp7ErLRQUkN4LuIRA==", "type": "package", - "path": "avalonia.skia/12.0.2", + "path": "avalonia.skia/12.0.3", "files": [ ".nupkg.metadata", ".signature.p7s", "Icon.png", - "avalonia.skia.12.0.2.nupkg.sha512", + "avalonia.skia.12.0.3.nupkg.sha512", "avalonia.skia.nuspec", "lib/net10.0/Avalonia.Skia.dll", "lib/net10.0/Avalonia.Skia.xml", @@ -1915,15 +1915,15 @@ "lib/net8.0/Avalonia.Skia.xml" ] }, - "Avalonia.Themes.Fluent/12.0.2": { - "sha512": "uEDLnrS7flCulIjWHnW6eiPiUqjxxAOh76suASMpQ0pJ+kntTEua5R1VcIulwZff79ArMqTVPejf7u0cxlVI2A==", + "Avalonia.Themes.Fluent/12.0.3": { + "sha512": "Acj+gmRm52U8sQIVHk5sCCU65RWjYcurDtxo8zyZSYmM4Vl1z2N9ZLnbK9+wp3N56Z0z/1ddsKYXRq3XfGSP3Q==", "type": "package", - "path": "avalonia.themes.fluent/12.0.2", + "path": "avalonia.themes.fluent/12.0.3", "files": [ ".nupkg.metadata", ".signature.p7s", "Icon.png", - "avalonia.themes.fluent.12.0.2.nupkg.sha512", + "avalonia.themes.fluent.12.0.3.nupkg.sha512", "avalonia.themes.fluent.nuspec", "lib/net10.0/Avalonia.Themes.Fluent.dll", "lib/net10.0/Avalonia.Themes.Fluent.xml", @@ -1931,15 +1931,15 @@ "lib/net8.0/Avalonia.Themes.Fluent.xml" ] }, - "Avalonia.Win32/12.0.2": { - "sha512": "gVUm4hyUvrFlocL0ycPZGAiN2TQhTv7vTa1I2nRkq/G/h6b5hKNoURjGfnZnXuPK3OnRENGPJjvZ+A9+0d5Jsg==", + "Avalonia.Win32/12.0.3": { + "sha512": "shPBe7puXXjEWr9gq4DKZZBpfv2tGxdghK1nsRnVC7J3tx3Hm2/+Ik7vYoIYSdUv7MkAZZ4ham9Inqn8CcODBw==", "type": "package", - "path": "avalonia.win32/12.0.2", + "path": "avalonia.win32/12.0.3", "files": [ ".nupkg.metadata", ".signature.p7s", "Icon.png", - "avalonia.win32.12.0.2.nupkg.sha512", + "avalonia.win32.12.0.3.nupkg.sha512", "avalonia.win32.nuspec", "lib/net10.0/Avalonia.Win32.Automation.dll", "lib/net10.0/Avalonia.Win32.Automation.xml", @@ -1951,15 +1951,15 @@ "lib/net8.0/Avalonia.Win32.xml" ] }, - "Avalonia.X11/12.0.2": { - "sha512": "FXFYfqjYYUAWqtgxEQl1cUWsH61GwTUojbRWURfmPAGcYYbm+2djrSOki5ppzAKKcm5lOmjoafYaeti39bSGQA==", + "Avalonia.X11/12.0.3": { + "sha512": "rQ0gbEcKcWXN2Pc0PS5gtLUEsi/yW1JvEbxGWi9D6ip/hdmczLtEG1n3Irtn7MJFjLJq5N6cLM2Prh4Euxwg9Q==", "type": "package", - "path": "avalonia.x11/12.0.2", + "path": "avalonia.x11/12.0.3", "files": [ ".nupkg.metadata", ".signature.p7s", "Icon.png", - "avalonia.x11.12.0.2.nupkg.sha512", + "avalonia.x11.12.0.3.nupkg.sha512", "avalonia.x11.nuspec", "lib/net10.0/Avalonia.X11.dll", "lib/net10.0/Avalonia.X11.xml", @@ -2679,10 +2679,10 @@ "useSharedDesignerContext.txt" ] }, - "Microsoft.Win32.SystemEvents/10.0.7": { - "sha512": "yRy88RjP9RlFxiaxwkGSh5e7lhSRCUaSwYW323ssK85XTm13b3y65Yp8HuURXNnxGQGo9L4Bz19aQAbrfTyJqA==", + "Microsoft.Win32.SystemEvents/10.0.8": { + "sha512": "J9+VT0lkrA7wW38CGxO2sZd+iH9C0qzioMi/2ztpT32WOVr+04uNqFIrxisTRf+mvtXLtdqcio1AoS0vfAVLLQ==", "type": "package", - "path": "microsoft.win32.systemevents/10.0.7", + "path": "microsoft.win32.systemevents/10.0.8", "files": [ ".nupkg.metadata", ".signature.p7s", @@ -2703,7 +2703,7 @@ "lib/net9.0/Microsoft.Win32.SystemEvents.xml", "lib/netstandard2.0/Microsoft.Win32.SystemEvents.dll", "lib/netstandard2.0/Microsoft.Win32.SystemEvents.xml", - "microsoft.win32.systemevents.10.0.7.nupkg.sha512", + "microsoft.win32.systemevents.10.0.8.nupkg.sha512", "microsoft.win32.systemevents.nuspec", "runtimes/win/lib/net10.0/Microsoft.Win32.SystemEvents.dll", "runtimes/win/lib/net10.0/Microsoft.Win32.SystemEvents.xml", @@ -3554,10 +3554,10 @@ "skiasharp.nativeassets.win32.nuspec" ] }, - "System.Drawing.Common/10.0.7": { - "sha512": "t0dLoUJOFMMyHqwpDuTKVlKVry92yUhf7qwihVT2MUrg3y/ZOHKpij5nb+jb20UT3vc7UlQWQlNmV9NQ5c7aSA==", + "System.Drawing.Common/10.0.8": { + "sha512": "LCISd6JAF80vsy9L6wuetZeOMR3FH+SwKovw5Zl0Nov9pB9ZOiCSqvOfMzooypPItQLv0mGfEePjJ1nCCQztNw==", "type": "package", - "path": "system.drawing.common/10.0.7", + "path": "system.drawing.common/10.0.8", "files": [ ".nupkg.metadata", ".signature.p7s", @@ -3602,7 +3602,7 @@ "lib/xamarinmac20/_._", "lib/xamarintvos10/_._", "lib/xamarinwatchos10/_._", - "system.drawing.common.10.0.7.nupkg.sha512", + "system.drawing.common.10.0.8.nupkg.sha512", "system.drawing.common.nuspec", "useSharedDesignerContext.txt" ] @@ -3656,12 +3656,12 @@ }, "projectFileDependencyGroups": { "net10.0-windows7.0": [ - "Avalonia >= 12.0.2", - "Avalonia.Controls.ColorPicker >= 12.0.2", + "Avalonia >= 12.0.3", + "Avalonia.Controls.ColorPicker >= 12.0.3", "Avalonia.Controls.DataGrid >= 12.0.0", - "Avalonia.Desktop >= 12.0.2", - "Avalonia.Fonts.Inter >= 12.0.2", - "Avalonia.Themes.Fluent >= 12.0.2", + "Avalonia.Desktop >= 12.0.3", + "Avalonia.Fonts.Inter >= 12.0.3", + "Avalonia.Themes.Fluent >= 12.0.3", "CommunityToolkit.Mvvm >= 8.4.2", "HidSharp >= 2.6.4", "HueApi >= 3.2.0", @@ -3688,7 +3688,7 @@ "Sentry >= 6.5.0", "Sentry.Profiling >= 6.5.0", "Sharlayan >= 9.0.34", - "System.Drawing.Common >= 10.0.7", + "System.Drawing.Common >= 10.0.8", "Velopack >= 0.0.1298" ] }, @@ -3745,11 +3745,11 @@ "dependencies": { "Avalonia": { "target": "Package", - "version": "[12.0.2, )" + "version": "[12.0.3, )" }, "Avalonia.Controls.ColorPicker": { "target": "Package", - "version": "[12.0.2, )" + "version": "[12.0.3, )" }, "Avalonia.Controls.DataGrid": { "target": "Package", @@ -3757,15 +3757,15 @@ }, "Avalonia.Desktop": { "target": "Package", - "version": "[12.0.2, )" + "version": "[12.0.3, )" }, "Avalonia.Fonts.Inter": { "target": "Package", - "version": "[12.0.2, )" + "version": "[12.0.3, )" }, "Avalonia.Themes.Fluent": { "target": "Package", - "version": "[12.0.2, )" + "version": "[12.0.3, )" }, "CommunityToolkit.Mvvm": { "target": "Package", @@ -3873,7 +3873,7 @@ }, "System.Drawing.Common": { "target": "Package", - "version": "[10.0.7, )" + "version": "[10.0.8, )" }, "Velopack": { "target": "Package", From c0a1a748a3183c0ab676d44255cb1471923e0004 Mon Sep 17 00:00:00 2001 From: Danielle Date: Thu, 14 May 2026 20:33:19 +1000 Subject: [PATCH 02/31] Add QMK Raw HID keyboard provider (Beta) (v4.1.40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New custom RGB.NET extension for QMK firmware keyboards over Raw HID. Brand-agnostic — discovery filters on HID usage page 0xFF60 / usage 0x61 so it works on any QMK board (NovelKeys, KBDFans, Drop, GMMK, Glorious and others) without a hardcoded VID/PID allow-list. Architecture: - Two protocols handled on the same HID interface. Handshake decides which the device speaks: - VIA-only (universal): single-LED device, drives RGB matrix base hue/sat/val + effect mode. - OpenRGB-QMK (firmware-side plugin): full per-key control via direct mode, chunked SetLedRange writes with 1ms inter-packet pacing. - Per-key boards get a semantic LedId.Keyboard_* mapping by looking up the VIA keymap JSON for their VID/PID from www.caniusevia.com on first connect and merging against the firmware's GetLedInfo matrix coordinates. Cached on disk under %APPDATA%/Chromatics/QmkKeymaps. Falls back to LedId.Custom1..N with a synthetic grid when no keymap is fetchable (offline / unknown board) so the device still works via the Mapping tab drag-position UX. - Auto-adopt on first enable: discovery runs, every responding board is registered to SettingsModel.deviceQmkRawHidAdoptedDevices and picked up by the provider's adopted-set filter. Subsequent launches reuse the persisted list. Per-keyboard disable on the Mapping tab covers the "I don't want this one" case for v1 Beta. - Hot-plug parity with PlayStation provider: DeviceList.Local.Changed reconciles the open set on USB connect/disconnect events. Per- device disable gate parity with LIFX / Hue queues so the Mapping tab disable persistence works end-to-end. UX: - Settings → Device Providers gets a "QMK Keyboards (Beta)" toggle with brand-list tooltip. - First-run device selector adds a matching tile. - Locale strings added to en.json and translated into the six non-EN locales via translate.py. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 5 +- Chromatics/Chromatics.csproj | 2 +- Chromatics/Core/RGBController.cs | 85 +++- .../QmkRawHid/Protocol/OpenRgbQmkProtocol.cs | 170 +++++++ .../QmkRawHid/Protocol/QmkRawHidConstants.cs | 30 ++ .../QmkRawHid/Protocol/QmkRawHidDiscovery.cs | 189 ++++++++ .../Devices/QmkRawHid/Protocol/ViaProtocol.cs | 100 ++++ .../Devices/QmkRawHid/QmkKeycodeMap.cs | 120 +++++ .../Devices/QmkRawHid/QmkKeymapFetcher.cs | 328 +++++++++++++ .../QmkRawHid/QmkRawHidClientDefinition.cs | 85 ++++ .../Devices/QmkRawHid/QmkRawHidDevice.cs | 63 +++ .../Devices/QmkRawHid/QmkRawHidDeviceInfo.cs | 23 + .../QmkRawHid/QmkRawHidProtocolMode.cs | 11 + .../QmkRawHid/QmkRawHidRGBDeviceProvider.cs | 457 ++++++++++++++++++ .../Devices/QmkRawHid/QmkRawHidUpdateQueue.cs | 285 +++++++++++ .../QmkRawHid/QmkRawHidUpdateTrigger.cs | 48 ++ Chromatics/Models/QmkRawHidAdoptedDevice.cs | 27 ++ Chromatics/Models/SettingsModel.cs | 2 + Chromatics/ViewModels/SettingsViewModel.cs | 77 +++ Chromatics/Views/Dialogs/FirstRunDialog.axaml | 4 + .../Views/Dialogs/FirstRunDialog.axaml.cs | 11 +- Chromatics/locale/de.json | 5 +- Chromatics/locale/en.json | 3 + Chromatics/locale/es.json | 5 +- Chromatics/locale/fr.json | 5 +- Chromatics/locale/ja.json | 5 +- Chromatics/locale/ko.json | 5 +- Chromatics/locale/zh_CN.json | 5 +- 28 files changed, 2139 insertions(+), 16 deletions(-) create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/OpenRgbQmkProtocol.cs create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/QmkRawHidConstants.cs create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/QmkRawHidDiscovery.cs create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/ViaProtocol.cs create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeycodeMap.cs create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeymapFetcher.cs create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidClientDefinition.cs create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidDevice.cs create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidDeviceInfo.cs create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidProtocolMode.cs create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidRGBDeviceProvider.cs create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidUpdateQueue.cs create mode 100644 Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidUpdateTrigger.cs create mode 100644 Chromatics/Models/QmkRawHidAdoptedDevice.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9086d6dc..a47b98fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ All notable changes to Chromatics are documented here. -## 4.1.39 +## 4.1.40 -- Updated underlying UI dependencies (Avalonia 12.0.3, System.Drawing.Common 10.0.8) for stability and bug fixes. No functional changes. +- **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 on USB and adopts them — no firmware flashing or extra software required. Per-key lighting is driven via the OpenRGB-QMK plugin when the firmware has it installed; otherwise Chromatics drives the firmware's built-in RGB matrix base colour and effect mode (the VIA fallback path). For per-key boards, Chromatics looks up the physical key layout from the via-keyboards database automatically on first connect so the Highlight / Keybind layers line up with the right keys out of the box. +- Updated dependency libraries to latest version ## 4.1.38 diff --git a/Chromatics/Chromatics.csproj b/Chromatics/Chromatics.csproj index f97c609b..42f198f9 100644 --- a/Chromatics/Chromatics.csproj +++ b/Chromatics/Chromatics.csproj @@ -4,7 +4,7 @@ WinExe net10.0-windows7.0 Chromatics.Program - 4.1.39.0 + 4.1.40.0 Danielle Thompson app.manifest logicallysynced 2026 diff --git a/Chromatics/Core/RGBController.cs b/Chromatics/Core/RGBController.cs index 8ffe6d18..8a3a21ae 100644 --- a/Chromatics/Core/RGBController.cs +++ b/Chromatics/Core/RGBController.cs @@ -304,8 +304,70 @@ public static void Setup() Logger.WriteConsole(Enums.LoggerTypes.Error, $"[LifxDeviceProvider] LoadDeviceProvider Error: {ex.Message}"); } } - - + + if (appSettings.deviceQmkRawHidEnabled) + { + try + { + // QMK Raw HID provider — auto-adopts every QMK-compatible + // keyboard discovered on the USB bus when the user has + // an empty persisted adopted-set (first launch after + // enabling). The adopted-set is the union of (a) the + // boards the user has explicitly seen in the Mapping + // tab and not removed via per-device disable, and (b) + // any new boards that appear on subsequent launches + // — the keymap fetch + handshake is cheap so refreshing + // is fine. Persistence stays in + // deviceQmkRawHidAdoptedDevices for the next launch's + // hot-plug filter. + Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.QmkRawHidRGBDeviceProvider.Instance.AdoptedDevices.Clear(); + var adopted = appSettings.deviceQmkRawHidAdoptedDevices ?? new List(); + if (adopted.Count == 0) + { + // Discover once and adopt everything that responds. + // Subsequent launches will reuse the persisted list + // unless the user explicitly clears it. + var discovered = Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.Protocol.QmkRawHidDiscovery.Discover(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var c in discovered) + { + string mfg = ""; + string prod = ""; + try { mfg = c.Hid.GetManufacturer() ?? ""; } catch { } + try { prod = c.Hid.GetProductName() ?? ""; } catch { } + var key = $"{c.Hid.VendorID:X4}:{c.Hid.ProductID:X4}:{mfg}:{prod}"; + if (!seen.Add(key)) continue; + adopted.Add(new QmkRawHidAdoptedDevice + { + VendorId = c.Hid.VendorID, + ProductId = c.Hid.ProductID, + Manufacturer = mfg, + Product = prod, + LedCount = c.LedCount, + Protocol = c.Protocol.ToString(), + ViaKeymapKey = string.Empty, + }); + } + appSettings.deviceQmkRawHidAdoptedDevices = adopted; + if (adopted.Count > 0) AppSettings.SaveSettings(appSettings); + } + + foreach (var d in adopted) + { + Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.QmkRawHidRGBDeviceProvider.Instance.AdoptedDevices.Add( + new Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.QmkRawHidAdoptedDeviceFilter( + d.VendorId, d.ProductId, d.Manufacturer, d.Product)); + } + + LoadDeviceProvider(Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.QmkRawHidRGBDeviceProvider.Instance); + } + catch (Exception ex) + { + Logger.WriteConsole(Enums.LoggerTypes.Error, $"[QmkRawHidDeviceProvider] LoadDeviceProvider Error: {ex.Message}"); + } + } + + if (appSettings.rgbRefreshRate <= 0) appSettings.rgbRefreshRate = 0.05; _timerUpdateTrigger = new TimerUpdateTrigger(); @@ -378,6 +440,16 @@ public static void RemoveDevice(IRGBDevice device) catch { /* best-effort */ } }); } + else if (device is Extensions.RGB.NET.Devices.QmkRawHid.QmkRawHidDevice qmkDev) + { + // QMK boards have no captured pre-Chromatics state to + // restore — the firmware's built-in RGB matrix mode + // resumes by itself as soon as Update() stops sending + // frames. Gate the queue so any buffered frames in + // flight don't slip through, then let the firmware + // take over. + qmkDev.SetPerDeviceDisabled(true); + } surface.Detach(device); @@ -429,6 +501,11 @@ public static void AddDevice(IRGBDevice device) { hueDev.SetPerDeviceDisabled(false); } + else if (device is Extensions.RGB.NET.Devices.QmkRawHid.QmkRawHidDevice qmkDev) + { + qmkDev.ResetCache(); + qmkDev.SetPerDeviceDisabled(false); + } // Tagged effects (startup rainbow, title-screen starfield) // build their per-device ListLedGroup at the moment the tag @@ -852,6 +929,8 @@ public static bool LoadDeviceProvider(IRGBDeviceProvider provider) lifxDev.SetPerDeviceDisabled(true); else if (device is Extensions.RGB.NET.Devices.Hue.HueDevice hueDev) hueDev.SetPerDeviceDisabled(true); + else if (device is Extensions.RGB.NET.Devices.QmkRawHid.QmkRawHidDevice qmkDev) + qmkDev.SetPerDeviceDisabled(true); if (_activeDevices.ContainsKey(device)) _activeDevices[device] = false; else @@ -1236,7 +1315,7 @@ public static void RegisterTaggedEffect(string tag, Guid deviceGuid, ListLedGrou // Tear down ONLY the groups registered under `tag` — used when the // user disables Startup Animation / Title Screen via the Effects - // tab and we need to stop the rainbow / starfield mid-cycle without + // tab and we need to stop the effects mid-cycle without // killing other running effects on the surface. // // LEDs are painted BLACK and one surface render is forced before diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/OpenRgbQmkProtocol.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/OpenRgbQmkProtocol.cs new file mode 100644 index 00000000..adddc266 --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/OpenRgbQmkProtocol.cs @@ -0,0 +1,170 @@ +using System; +using System.Buffers.Binary; + +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.Protocol +{ + // OpenRGB QMK plugin command set (master branch reference, see + // https://gitlab.com/CalcProgrammer1/OpenRGB → Controllers/QMKOpenRGBController). + // This sits ON TOP of the same Raw HID transport that VIA uses; + // VIA's commands occupy id 0x01-0x09, OpenRGB-QMK's start at 0x20 + // so the two can coexist on one HID interface. Firmware support + // requires the qmk_openrgb fork or patch — we detect at handshake + // and gracefully fall back to ViaProtocol if absent. + // + // Wire format: every request is `cmd_byte | request_payload[31]`. + // Replies echo cmd_byte in byte 0; mismatched echoes mean the + // firmware doesn't speak this command. + internal static class OpenRgbQmkProtocol + { + public const byte Cmd_GetProtocolVersion = 0x20; + public const byte Cmd_GetQmkVersion = 0x21; + public const byte Cmd_GetDeviceInfo = 0x22; + public const byte Cmd_GetLedInfo = 0x23; + public const byte Cmd_GetEnabledModes = 0x24; + public const byte Cmd_GetLedMatrixSize = 0x25; + public const byte Cmd_SetLedRange = 0x27; + public const byte Cmd_SetSingleLed = 0x28; + public const byte Cmd_SetMode = 0x29; + public const byte Cmd_Save = 0x2A; + + // Each Cmd_SetLedRange packet carries (start_idx u16 | count u8 | + // RGB triplets). Header overhead = 1 byte cmd + 2 bytes start + + // 1 byte count = 4 bytes, leaving 28 bytes / 3 = 9 LEDs per packet. + // Caller chunks the strip and paces packets to stay within the + // firmware's RX queue (typical RX queue is 4-8 deep at full speed, + // hence the 1ms inter-packet pacing we use in the queue). + public const int MaxLedsPerSetRange = 9; + + // ── Frame builders ──────────────────────────────────────────── + + public static void BuildGetProtocolVersion(Span payload) + { + payload.Clear(); + payload[0] = Cmd_GetProtocolVersion; + } + + public static void BuildGetDeviceInfo(Span payload) + { + payload.Clear(); + payload[0] = Cmd_GetDeviceInfo; + } + + // Cmd_GetLedInfo takes (start_idx u16) and returns matrix + // (column, row) + flags for up to N LEDs starting at start_idx. + // The firmware decides N based on remaining payload room + // (typically 9 LEDs * 3 bytes per record = 27 + 1 byte echo). + public static void BuildGetLedInfo(Span payload, ushort startIndex) + { + payload.Clear(); + payload[0] = Cmd_GetLedInfo; + BinaryPrimitives.WriteUInt16LittleEndian(payload.Slice(1, 2), startIndex); + } + + public static void BuildGetLedMatrixSize(Span payload) + { + payload.Clear(); + payload[0] = Cmd_GetLedMatrixSize; + } + + // Bulk set: pack RGB triplets starting at byte 4. Caller must + // ensure rgbBytes.Length == count * 3 and count <= MaxLedsPerSetRange. + public static void BuildSetLedRange(Span payload, ushort startIndex, byte count, ReadOnlySpan rgbBytes) + { + if (count > MaxLedsPerSetRange) throw new ArgumentOutOfRangeException(nameof(count)); + if (rgbBytes.Length != count * 3) throw new ArgumentException("rgbBytes length must equal count*3", nameof(rgbBytes)); + payload.Clear(); + payload[0] = Cmd_SetLedRange; + BinaryPrimitives.WriteUInt16LittleEndian(payload.Slice(1, 2), startIndex); + payload[3] = count; + rgbBytes.CopyTo(payload.Slice(4, count * 3)); + } + + public static void BuildSetSingleLed(Span payload, ushort index, byte r, byte g, byte b) + { + payload.Clear(); + payload[0] = Cmd_SetSingleLed; + BinaryPrimitives.WriteUInt16LittleEndian(payload.Slice(1, 2), index); + payload[3] = r; + payload[4] = g; + payload[5] = b; + } + + // Switch the firmware to direct host-driven mode (mode 0 in the + // OpenRGB-QMK convention) or back to a built-in effect index. + // Direct mode suspends the firmware's own RGB matrix animations + // so our Set commands aren't fighting them every tick. + public static void BuildSetMode(Span payload, byte modeIndex) + { + payload.Clear(); + payload[0] = Cmd_SetMode; + payload[1] = modeIndex; + } + + // ── Reply parsers ───────────────────────────────────────────── + + public static ushort TryParseProtocolVersion(ReadOnlySpan reply) + { + if (reply.Length < 3) return 0; + if (reply[0] != Cmd_GetProtocolVersion) return 0; + return BinaryPrimitives.ReadUInt16LittleEndian(reply.Slice(1, 2)); + } + + // GetDeviceInfo reply layout: + // byte 0 = Cmd_GetDeviceInfo (echo) + // bytes 1-2 = total LED count (uint16 LE) + // bytes 3-4 = vendor id (uint16 LE) + // bytes 5-6 = product id (uint16 LE) + // byte 7 = device type (informational) + // bytes 8+ = null-terminated device name + public static bool TryParseDeviceInfo(ReadOnlySpan reply, out ushort ledCount, out ushort vendorId, out ushort productId, out string deviceName) + { + ledCount = 0; vendorId = 0; productId = 0; deviceName = string.Empty; + if (reply.Length < 8) return false; + if (reply[0] != Cmd_GetDeviceInfo) return false; + ledCount = BinaryPrimitives.ReadUInt16LittleEndian(reply.Slice(1, 2)); + vendorId = BinaryPrimitives.ReadUInt16LittleEndian(reply.Slice(3, 2)); + productId = BinaryPrimitives.ReadUInt16LittleEndian(reply.Slice(5, 2)); + + int nameEnd = 8; + while (nameEnd < reply.Length && reply[nameEnd] != 0) nameEnd++; + if (nameEnd > 8) + deviceName = System.Text.Encoding.UTF8.GetString(reply.Slice(8, nameEnd - 8)); + return true; + } + + // GetLedInfo reply layout (variable LED records, 3 bytes each): + // byte 0 = Cmd_GetLedInfo (echo) + // byte 1 = record count for this packet + // then records: column (u8) | row (u8) | flags (u8) + // Caller iterates start_index for additional batches. + public static bool TryParseLedInfoBatch(ReadOnlySpan reply, out int recordCount, out ReadOnlySpan records) + { + recordCount = 0; records = default; + if (reply.Length < 2) return false; + if (reply[0] != Cmd_GetLedInfo) return false; + recordCount = reply[1]; + int recordBytes = recordCount * 3; + if (reply.Length < 2 + recordBytes) return false; + records = reply.Slice(2, recordBytes); + return true; + } + + public static bool TryParseLedMatrixSize(ReadOnlySpan reply, out byte columns, out byte rows) + { + columns = 0; rows = 0; + if (reply.Length < 3) return false; + if (reply[0] != Cmd_GetLedMatrixSize) return false; + columns = reply[1]; + rows = reply[2]; + return true; + } + + public readonly struct LedRecord + { + public readonly byte Column; + public readonly byte Row; + public readonly byte Flags; + public LedRecord(byte column, byte row, byte flags) { Column = column; Row = row; Flags = flags; } + } + } +} diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/QmkRawHidConstants.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/QmkRawHidConstants.cs new file mode 100644 index 00000000..a5f7fb66 --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/QmkRawHidConstants.cs @@ -0,0 +1,30 @@ +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.Protocol +{ + // QMK Raw HID transport constants. Reference: + // https://docs.qmk.fm/#/feature_rawhid (firmware side) and + // OpenRGB's QMKOpenRGBController + VIA's protocol/constants + // for the host side. + internal static class QmkRawHidConstants + { + // Vendor-defined HID usage page + usage that every QMK keyboard + // exposes on its Raw HID interface. Discovery filters HidSharp's + // enumeration on these two values rather than VID/PID so we work + // across NovelKeys, KBDFans, Drop, GMMK, Glorious, etc. without + // a hardcoded allow-list. + public const ushort RawHidUsagePage = 0xFF60; + public const ushort RawHidUsage = 0x61; + + // QMK Raw HID transfers are 32-byte payloads. Windows HidSharp + // expects an extra leading report-id byte, so the output buffer + // is 33 bytes total (report id 0 + 32 data bytes). Reply reports + // are the same shape minus the leading id on the HidSharp read + // side (HidStream strips the id from inputs). + public const int ReportPayloadBytes = 32; + public const int OutputReportBytes = ReportPayloadBytes + 1; + + // Default request/response wait. Most QMK Raw HID round-trips + // complete in ~5-20ms over USB Full Speed; 250ms is generous + // and matches OpenRGB's default for the same protocol. + public const int ResponseTimeoutMs = 250; + } +} diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/QmkRawHidDiscovery.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/QmkRawHidDiscovery.cs new file mode 100644 index 00000000..54fa2752 --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/QmkRawHidDiscovery.cs @@ -0,0 +1,189 @@ +using HidSharp; +using HidSharp.Reports; +using System; +using System.Collections.Generic; + +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.Protocol +{ + // Enumerates the host's HID devices, picks out the ones exposing the + // QMK Raw HID interface (usage page 0xFF60, usage 0x61), and + // classifies each by which protocol(s) the firmware actually + // implements via the handshake probe. + internal static class QmkRawHidDiscovery + { + public enum ProtocolSupport + { + None, // Discovered but neither VIA nor OpenRGB-QMK responded — skip. + ViaOnly, // Tier 1: drive base hue/sat/val via VIA's RGB matrix sub-commands. + OpenRgbQmk, // Tier 2: per-key control via OpenRGB-QMK direct mode. + } + + public readonly struct Candidate + { + public readonly HidDevice Hid; + public readonly ProtocolSupport Protocol; + public readonly int LedCount; // Populated when Protocol == OpenRgbQmk. + public readonly byte MatrixColumns; // Optional hint from OpenRGB-QMK GetLedMatrixSize. + public readonly byte MatrixRows; + public readonly string FirmwareDeviceName; + + public Candidate(HidDevice hid, ProtocolSupport protocol, int ledCount, byte columns, byte rows, string firmwareDeviceName) + { + Hid = hid; Protocol = protocol; LedCount = ledCount; + MatrixColumns = columns; MatrixRows = rows; + FirmwareDeviceName = firmwareDeviceName ?? string.Empty; + } + } + + // Walks HidSharp's enumeration, filters to interfaces whose + // report descriptor advertises the QMK Raw HID usage, and runs + // the handshake on each. Output is ordered by VID:PID for + // stable adoption-dialog presentation across launches. + public static IReadOnlyList Discover() + { + var results = new List(); + HidDevice[] all; + try { all = DeviceList.Local.GetHidDevices() as HidDevice[] ?? new List(DeviceList.Local.GetHidDevices()).ToArray(); } + catch { return results; } + + foreach (HidDevice hid in all) + { + if (!ExposesRawHidUsage(hid)) continue; + + if (!TryHandshake(hid, out var protocol, out int ledCount, out byte cols, out byte rows, out string fwName)) + continue; + + if (protocol == ProtocolSupport.None) continue; + + results.Add(new Candidate(hid, protocol, ledCount, cols, rows, fwName)); + } + + results.Sort((a, b) => + { + int v = a.Hid.VendorID.CompareTo(b.Hid.VendorID); + if (v != 0) return v; + return a.Hid.ProductID.CompareTo(b.Hid.ProductID); + }); + + return results; + } + + // Returns true if any of the HidDevice's top-level collections + // declares usage page 0xFF60, usage 0x61. Multi-interface USB + // devices expose each interface as a separate HidDevice, so + // this check naturally selects only the Raw HID interface and + // leaves the keyboard/consumer interfaces alone. + private static bool ExposesRawHidUsage(HidDevice hid) + { + ReportDescriptor desc; + try { desc = hid.GetReportDescriptor(); } + catch { return false; } + + foreach (var item in desc.DeviceItems) + { + foreach (uint usage in item.Usages.GetAllValues()) + { + ushort page = (ushort)(usage >> 16); + ushort usageLo = (ushort)(usage & 0xFFFF); + if (page == QmkRawHidConstants.RawHidUsagePage && usageLo == QmkRawHidConstants.RawHidUsage) + return true; + } + } + return false; + } + + // Open the candidate's HID stream and try VIA first, then + // OpenRGB-QMK. OpenRGB wins when both respond — it's the + // strictly more capable protocol. Stream is disposed before + // we return; the provider will reopen it for the device's + // permanent UpdateQueue. + private static bool TryHandshake(HidDevice hid, out ProtocolSupport protocol, out int ledCount, out byte cols, out byte rows, out string firmwareDeviceName) + { + protocol = ProtocolSupport.None; + ledCount = 0; cols = 0; rows = 0; firmwareDeviceName = string.Empty; + + HidStream stream; + try + { + if (!hid.TryOpen(out stream)) return false; + } + catch { return false; } + + try + { + stream.ReadTimeout = QmkRawHidConstants.ResponseTimeoutMs; + stream.WriteTimeout = QmkRawHidConstants.ResponseTimeoutMs; + + Span outBuf = stackalloc byte[QmkRawHidConstants.OutputReportBytes]; + Span payload = outBuf.Slice(1, QmkRawHidConstants.ReportPayloadBytes); + byte[] inBuf = new byte[QmkRawHidConstants.ReportPayloadBytes + 1]; + + bool viaOk = false; + ViaProtocol.BuildGetProtocolVersion(payload); + if (SendAndReceive(stream, outBuf, inBuf)) + { + if (ViaProtocol.TryParseProtocolVersion(StripReportId(inBuf)) > 0) + viaOk = true; + } + + bool openRgbOk = false; + OpenRgbQmkProtocol.BuildGetProtocolVersion(payload); + if (SendAndReceive(stream, outBuf, inBuf)) + { + if (OpenRgbQmkProtocol.TryParseProtocolVersion(StripReportId(inBuf)) > 0) + openRgbOk = true; + } + + if (openRgbOk) + { + OpenRgbQmkProtocol.BuildGetDeviceInfo(payload); + if (SendAndReceive(stream, outBuf, inBuf) && + OpenRgbQmkProtocol.TryParseDeviceInfo(StripReportId(inBuf), + out ushort count, out _, out _, out string name)) + { + ledCount = count; + firmwareDeviceName = name; + } + + OpenRgbQmkProtocol.BuildGetLedMatrixSize(payload); + if (SendAndReceive(stream, outBuf, inBuf)) + { + OpenRgbQmkProtocol.TryParseLedMatrixSize(StripReportId(inBuf), out cols, out rows); + } + + protocol = ProtocolSupport.OpenRgbQmk; + return true; + } + + if (viaOk) + { + protocol = ProtocolSupport.ViaOnly; + return true; + } + + return true; + } + finally + { + try { stream.Dispose(); } catch { /* ignore */ } + } + } + + private static bool SendAndReceive(HidStream stream, ReadOnlySpan outBuf, byte[] inBuf) + { + try + { + stream.Write(outBuf.ToArray()); + int n = stream.Read(inBuf, 0, inBuf.Length); + return n > 0; + } + catch { return false; } + } + + // Strips the leading report-id byte HidSharp prepends to inputs + // on Windows; QMK Raw HID always uses report id 0 so the strip + // is unconditional. + private static ReadOnlySpan StripReportId(byte[] inBuf) => + new ReadOnlySpan(inBuf, 1, inBuf.Length - 1); + } +} diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/ViaProtocol.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/ViaProtocol.cs new file mode 100644 index 00000000..bab254f5 --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/Protocol/ViaProtocol.cs @@ -0,0 +1,100 @@ +using System; +using System.Buffers.Binary; + +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.Protocol +{ + // VIA protocol command IDs and frame builders. Reference: + // https://www.caniusevia.com/docs/specification — the live spec + // for VIA's keyboard configuration protocol. VIA-compatible firmware + // is the lowest-common-denominator: most stock QMK boards ship with + // it enabled, and it exposes RGB matrix mode + base hue/sat/val + // controls even when the firmware doesn't have OpenRGB's per-key + // plugin built in. + internal static class ViaProtocol + { + // Top-level command IDs (byte 0 of the request payload). + public const byte Id_GetProtocolVersion = 0x01; + public const byte Id_GetKeyboardValue = 0x02; + public const byte Id_SetKeyboardValue = 0x03; + public const byte Id_LightingSetValue = 0x07; + public const byte Id_LightingGetValue = 0x08; + public const byte Id_LightingSave = 0x09; + + // Lighting sub-commands (byte 1 of request when command = 0x07/0x08). + // The "QMK RGB Matrix" naming reflects firmware-side terminology; + // VIA-compatible firmware exposes these on RGB matrix builds. + public const byte SubId_QmkRgbMatrixBrightness = 0x81; + public const byte SubId_QmkRgbMatrixEffect = 0x82; + public const byte SubId_QmkRgbMatrixEffectSpeed = 0x83; + public const byte SubId_QmkRgbMatrixColor = 0x84; + + // RGB Matrix effect indices. Only SolidColor is hard-required for + // Tier 1; the rest are useful as escape hatches for game-driven + // mode switches (e.g. Vegas → CycleAll, idle → SolidReactiveCross). + public const byte Effect_SolidColor = 1; + public const byte Effect_AlphaMods = 2; + public const byte Effect_GradientUpDown = 3; + public const byte Effect_Breathing = 6; + public const byte Effect_CycleAll = 9; + public const byte Effect_RainbowMovingChevron = 11; + public const byte Effect_DualBeacon = 16; + public const byte Effect_RainbowBeacon = 17; + public const byte Effect_RainbowPinwheels = 18; + public const byte Effect_SolidSplash = 30; + + // ── Frame builders ──────────────────────────────────────────── + // All builders write into a caller-supplied 32-byte payload span + // (no allocation). Caller is responsible for the leading report-id + // byte the HidSharp output buffer needs. + + public static void BuildGetProtocolVersion(Span payload) + { + payload.Clear(); + payload[0] = Id_GetProtocolVersion; + } + + public static void BuildSetRgbMatrixEffect(Span payload, byte effectIndex) + { + payload.Clear(); + payload[0] = Id_LightingSetValue; + payload[1] = SubId_QmkRgbMatrixEffect; + payload[2] = effectIndex; + } + + public static void BuildSetRgbMatrixBrightness(Span payload, byte brightness0to255) + { + payload.Clear(); + payload[0] = Id_LightingSetValue; + payload[1] = SubId_QmkRgbMatrixBrightness; + payload[2] = brightness0to255; + } + + // Hue + saturation in a single packet — VIA's + // QMK_RGB_MATRIX_COLOR sub-command takes 2 bytes after the + // sub-id. Both values are 0-255. + public static void BuildSetRgbMatrixColor(Span payload, byte hue0to255, byte sat0to255) + { + payload.Clear(); + payload[0] = Id_LightingSetValue; + payload[1] = SubId_QmkRgbMatrixColor; + payload[2] = hue0to255; + payload[3] = sat0to255; + } + + public static void BuildLightingSave(Span payload) + { + payload.Clear(); + payload[0] = Id_LightingSave; + } + + // Reply parser for GetProtocolVersion. Returns 0 if the reply + // doesn't echo the command id (firmware doesn't speak VIA, or + // request collided with another consumer on the same HID interface). + public static ushort TryParseProtocolVersion(ReadOnlySpan reply) + { + if (reply.Length < 3) return 0; + if (reply[0] != Id_GetProtocolVersion) return 0; + return BinaryPrimitives.ReadUInt16BigEndian(reply.Slice(1, 2)); + } + } +} diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeycodeMap.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeycodeMap.cs new file mode 100644 index 00000000..c80ce84b --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeycodeMap.cs @@ -0,0 +1,120 @@ +using System.Collections.Frozen; +using System.Collections.Generic; +using RGB.NET.Core; + +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid +{ + // QMK keycode string → RGB.NET LedId. Covers the canonical ANSI 104 + // and the common alternates (numpad, media, ISO Enter / split-keys); + // unknown keycodes return LedId.Invalid so the layout merger falls + // back to LedId.Custom1+i for that LED. QMK keycodes follow the + // KC_ convention; the via-keyboards JSON sometimes strips + // the prefix, so the lookup matches both forms. + internal static class QmkKeycodeMap + { + public static LedId ToLedId(string keycode) + { + if (string.IsNullOrEmpty(keycode)) return LedId.Invalid; + string key = keycode.Trim(); + if (key.StartsWith("KC_")) key = key.Substring(3); + return _map.TryGetValue(key, out var id) ? id : LedId.Invalid; + } + + // Stored as a FrozenDictionary because lookups happen during layout + // construction (once per device + LED count) and the table never + // changes at runtime. + private static readonly FrozenDictionary _map = + new Dictionary(System.StringComparer.OrdinalIgnoreCase) + { + // Letters + ["A"] = LedId.Keyboard_A, ["B"] = LedId.Keyboard_B, ["C"] = LedId.Keyboard_C, + ["D"] = LedId.Keyboard_D, ["E"] = LedId.Keyboard_E, ["F"] = LedId.Keyboard_F, + ["G"] = LedId.Keyboard_G, ["H"] = LedId.Keyboard_H, ["I"] = LedId.Keyboard_I, + ["J"] = LedId.Keyboard_J, ["K"] = LedId.Keyboard_K, ["L"] = LedId.Keyboard_L, + ["M"] = LedId.Keyboard_M, ["N"] = LedId.Keyboard_N, ["O"] = LedId.Keyboard_O, + ["P"] = LedId.Keyboard_P, ["Q"] = LedId.Keyboard_Q, ["R"] = LedId.Keyboard_R, + ["S"] = LedId.Keyboard_S, ["T"] = LedId.Keyboard_T, ["U"] = LedId.Keyboard_U, + ["V"] = LedId.Keyboard_V, ["W"] = LedId.Keyboard_W, ["X"] = LedId.Keyboard_X, + ["Y"] = LedId.Keyboard_Y, ["Z"] = LedId.Keyboard_Z, + + // Top row digits + ["1"] = LedId.Keyboard_1, ["2"] = LedId.Keyboard_2, ["3"] = LedId.Keyboard_3, + ["4"] = LedId.Keyboard_4, ["5"] = LedId.Keyboard_5, ["6"] = LedId.Keyboard_6, + ["7"] = LedId.Keyboard_7, ["8"] = LedId.Keyboard_8, ["9"] = LedId.Keyboard_9, + ["0"] = LedId.Keyboard_0, + + // Function row + ["F1"] = LedId.Keyboard_F1, ["F2"] = LedId.Keyboard_F2, ["F3"] = LedId.Keyboard_F3, + ["F4"] = LedId.Keyboard_F4, ["F5"] = LedId.Keyboard_F5, ["F6"] = LedId.Keyboard_F6, + ["F7"] = LedId.Keyboard_F7, ["F8"] = LedId.Keyboard_F8, ["F9"] = LedId.Keyboard_F9, + ["F10"] = LedId.Keyboard_F10, ["F11"] = LedId.Keyboard_F11, ["F12"] = LedId.Keyboard_F12, + + // Symbols / punctuation (US ANSI) + ["MINS"] = LedId.Keyboard_MinusAndUnderscore, + ["EQL"] = LedId.Keyboard_EqualsAndPlus, + ["LBRC"] = LedId.Keyboard_BracketLeft, + ["RBRC"] = LedId.Keyboard_BracketRight, + ["BSLS"] = LedId.Keyboard_Backslash, + ["SCLN"] = LedId.Keyboard_SemicolonAndColon, + ["QUOT"] = LedId.Keyboard_ApostropheAndDoubleQuote, + ["COMM"] = LedId.Keyboard_CommaAndLessThan, + ["DOT"] = LedId.Keyboard_PeriodAndBiggerThan, + ["SLSH"] = LedId.Keyboard_SlashAndQuestionMark, + ["GRV"] = LedId.Keyboard_GraveAccentAndTilde, + + // Modifiers + edit keys + ["ESC"] = LedId.Keyboard_Escape, + ["TAB"] = LedId.Keyboard_Tab, + ["CAPS"] = LedId.Keyboard_CapsLock, + ["LSFT"] = LedId.Keyboard_LeftShift, + ["RSFT"] = LedId.Keyboard_RightShift, + ["LCTL"] = LedId.Keyboard_LeftCtrl, + ["RCTL"] = LedId.Keyboard_RightCtrl, + ["LALT"] = LedId.Keyboard_LeftAlt, + ["RALT"] = LedId.Keyboard_RightAlt, + ["LGUI"] = LedId.Keyboard_LeftGui, + ["RGUI"] = LedId.Keyboard_RightGui, + ["ENT"] = LedId.Keyboard_Enter, + ["BSPC"] = LedId.Keyboard_Backspace, + ["SPC"] = LedId.Keyboard_Space, + ["APP"] = LedId.Keyboard_Application, + + // Navigation cluster + ["INS"] = LedId.Keyboard_Insert, + ["DEL"] = LedId.Keyboard_Delete, + ["HOME"] = LedId.Keyboard_Home, + ["END"] = LedId.Keyboard_End, + ["PGUP"] = LedId.Keyboard_PageUp, + ["PGDN"] = LedId.Keyboard_PageDown, + ["UP"] = LedId.Keyboard_ArrowUp, + ["DOWN"] = LedId.Keyboard_ArrowDown, + ["LEFT"] = LedId.Keyboard_ArrowLeft, + ["RGHT"] = LedId.Keyboard_ArrowRight, + + // System / lock keys + ["PSCR"] = LedId.Keyboard_PrintScreen, + ["SCRL"] = LedId.Keyboard_ScrollLock, + ["PAUS"] = LedId.Keyboard_PauseBreak, + ["NLCK"] = LedId.Keyboard_NumLock, + + // Numpad + ["P0"] = LedId.Keyboard_Num0, ["P1"] = LedId.Keyboard_Num1, + ["P2"] = LedId.Keyboard_Num2, ["P3"] = LedId.Keyboard_Num3, + ["P4"] = LedId.Keyboard_Num4, ["P5"] = LedId.Keyboard_Num5, + ["P6"] = LedId.Keyboard_Num6, ["P7"] = LedId.Keyboard_Num7, + ["P8"] = LedId.Keyboard_Num8, ["P9"] = LedId.Keyboard_Num9, + ["PDOT"] = LedId.Keyboard_NumPeriodAndDelete, + ["PSLS"] = LedId.Keyboard_NumSlash, + ["PAST"] = LedId.Keyboard_NumAsterisk, + ["PMNS"] = LedId.Keyboard_NumMinus, + ["PPLS"] = LedId.Keyboard_NumPlus, + ["PENT"] = LedId.Keyboard_NumEnter, + + // Some firmwares emit ISO-only keys; map to closest ANSI siblings. + // NUHS = ISO non-US hash (between Enter and ' on ISO layouts). + ["NUHS"] = LedId.Keyboard_Backslash, + // NUBS = ISO non-US backslash (next to Left Shift on ISO layouts). + ["NUBS"] = LedId.Keyboard_NonUsBackslash, + }.ToFrozenDictionary(System.StringComparer.OrdinalIgnoreCase); + } +} diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeymapFetcher.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeymapFetcher.cs new file mode 100644 index 00000000..63b55b3c --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeymapFetcher.cs @@ -0,0 +1,328 @@ +using Chromatics.Core; +using Chromatics.Enums; +using RGB.NET.Core; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid +{ + // Option C from the design discussion: fetch VIA keymap JSON from the + // upstream via-keyboards repo (or a CDN-mirrored equivalent) for any + // adopted board, then merge against the firmware's per-LED matrix + // coordinates to produce semantic LedId.Keyboard_* mappings. Falls + // back to LedId.Custom1..N when no keymap is fetchable (offline first + // run, unknown board, schema mismatch). + // + // Cache lives at %APPDATA%/Chromatics/QmkKeymaps/{vid:X4}_{pid:X4}.json + // and is consulted before any network request. Cache hits are + // unconditional — keymaps are board-defining and don't expire; if a + // user re-flashes with a different layout they can clear the cache + // dir manually. The index itself is refetched every 14 days so new + // boards added upstream show up without a Chromatics release. + internal static class QmkKeymapFetcher + { + // The via-keyboards index URL. Each keyboard entry maps a kebab-name + // path to vendor/product ids; the keymap JSONs themselves live one + // path-level deeper. URL is centralised so a future change of upstream + // host (or use of OpenSignalRGB's database in parallel) only touches + // this one constant. + private const string IndexUrl = "https://www.caniusevia.com/keyboards.json"; + private const string KeymapBase = "https://www.caniusevia.com/keyboards/"; + + private const int RequestTimeoutSeconds = 8; + private static readonly TimeSpan IndexCacheTtl = TimeSpan.FromDays(14); + + private static readonly HttpClient _http = new(new HttpClientHandler { AllowAutoRedirect = true }) + { + Timeout = TimeSpan.FromSeconds(RequestTimeoutSeconds), + }; + + // Loaded index: keyed by (vendorId, productId). Populated lazily on + // first call. ConcurrentDictionary so a multi-board adoption flow + // doesn't serialise behind one another's lookups. + private static ConcurrentDictionary<(int vid, int pid), string> _indexByVidPid; + private static readonly object _indexInitLock = new(); + + public sealed class QmkKeymap + { + public string Name { get; set; } + public int Cols { get; set; } + public int Rows { get; set; } + // Keycode at each matrix coordinate. Key = (col, row). Value is + // the QMK keycode string (e.g. "KC_A", "KC_F1", "KC_NO"). + // Underglow / non-matrix LEDs are absent from this dictionary; + // they get LedId.Custom1+i fallback ids in the merge step. + public IReadOnlyDictionary<(byte col, byte row), string> Keycodes { get; set; } + } + + // Returns the cached/fetched keymap, or null on failure. Never + // throws — the caller treats null as "fall back to Custom1..N". + public static async Task TryGetKeymapAsync(int vendorId, int productId) + { + try + { + string diskPath = ResolveCachePath(vendorId, productId); + if (TryLoadCached(diskPath, out QmkKeymap cached)) return cached; + + if (!TryEnsureIndex()) return null; + if (!_indexByVidPid.TryGetValue((vendorId, productId), out string keymapPath)) + return null; + + string url = KeymapBase + keymapPath; + string body = await _http.GetStringAsync(url).ConfigureAwait(false); + QmkKeymap parsed = ParseKeymapJson(body); + if (parsed == null) return null; + + TrySaveCache(diskPath, body); + return parsed; + } + catch (Exception ex) + { + Logger.WriteConsole(LoggerTypes.Devices, + $"[QMK] VIA keymap fetch failed for {vendorId:X4}:{productId:X4} ({ex.Message}); falling back to Custom1..N.", + forwardToSentry: false); + return null; + } + } + + // ── Index ──────────────────────────────────────────────────── + + private static bool TryEnsureIndex() + { + if (_indexByVidPid != null) return true; + lock (_indexInitLock) + { + if (_indexByVidPid != null) return true; + _indexByVidPid = LoadIndex(); + return _indexByVidPid != null; + } + } + + private static ConcurrentDictionary<(int vid, int pid), string> LoadIndex() + { + string indexCache = Path.Combine(GetCacheDir(), "_index.json"); + string body = null; + + if (File.Exists(indexCache)) + { + var age = DateTime.UtcNow - File.GetLastWriteTimeUtc(indexCache); + if (age < IndexCacheTtl) + { + try { body = File.ReadAllText(indexCache); } + catch { /* fall through to refetch */ } + } + } + + if (string.IsNullOrEmpty(body)) + { + try { body = _http.GetStringAsync(IndexUrl).GetAwaiter().GetResult(); } + catch (Exception ex) + { + Logger.WriteConsole(LoggerTypes.Devices, + $"[QMK] keymap index fetch failed ({ex.Message}); per-key semantic layout will use Custom1..N for now.", + forwardToSentry: false); + return null; + } + try { File.WriteAllText(indexCache, body); } catch { /* best-effort */ } + } + + return ParseIndex(body); + } + + // Index format (caniusevia.com): { "": { "vendorId": "0xABCD", + // "productId": "0x1234", "name": "...", "definition_version": "...", ... } } + // We only need vendorId / productId / kebab-name. vendorId/productId are + // strings with "0x" prefix. + private static ConcurrentDictionary<(int vid, int pid), string> ParseIndex(string json) + { + var dict = new ConcurrentDictionary<(int vid, int pid), string>(); + try + { + using var doc = JsonDocument.Parse(json); + foreach (var kvp in doc.RootElement.EnumerateObject()) + { + string path = kvp.Name; + if (!kvp.Value.TryGetProperty("vendorId", out var vidProp)) continue; + if (!kvp.Value.TryGetProperty("productId", out var pidProp)) continue; + if (!TryParseHexId(vidProp.GetString(), out int vid)) continue; + if (!TryParseHexId(pidProp.GetString(), out int pid)) continue; + dict[(vid, pid)] = path + ".json"; + } + } + catch { /* malformed index — return whatever parsed */ } + return dict; + } + + private static bool TryParseHexId(string s, out int value) + { + value = 0; + if (string.IsNullOrEmpty(s)) return false; + string t = s.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? s.Substring(2) : s; + return int.TryParse(t, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out value); + } + + // ── Keymap parse ───────────────────────────────────────────── + + // VIA keymap JSON layout fields used: + // "matrix": { "rows": N, "cols": N } + // "layouts": { "keymap": [ row, row, ... ] } where each row entry is + // either a "control object" { "x":..., "y":..., "w":... } or a + // string label like "0,5\n\n\nA" — "row,col" prefix tells us + // where in the firmware matrix this keycap sits. + // + // We walk the keymap array, tracking explicit "matrix" prefixes; the + // resulting dictionary lets the layout merger answer + // "what keycap is at (col, row)?" cheaply. + private static QmkKeymap ParseKeymapJson(string json) + { + try + { + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + int cols = 0, rows = 0; + if (root.TryGetProperty("matrix", out var matrix)) + { + if (matrix.TryGetProperty("cols", out var c)) cols = c.GetInt32(); + if (matrix.TryGetProperty("rows", out var r)) rows = r.GetInt32(); + } + + var keycodes = new Dictionary<(byte col, byte row), string>(); + if (root.TryGetProperty("layouts", out var layouts) + && layouts.TryGetProperty("keymap", out var keymap)) + { + foreach (var rowElem in keymap.EnumerateArray()) + { + if (rowElem.ValueKind != JsonValueKind.Array) continue; + foreach (var cellElem in rowElem.EnumerateArray()) + { + if (cellElem.ValueKind != JsonValueKind.String) continue; + string label = cellElem.GetString(); + if (string.IsNullOrEmpty(label)) continue; + + // Label shape: "row,col" then newline-separated labels + // (legend on each keycap face). We only need the + // matrix coordinate prefix. + int comma = label.IndexOf(','); + int nl = label.IndexOf('\n'); + if (comma <= 0 || nl <= comma) continue; + + if (!byte.TryParse(label.Substring(0, comma), out byte row)) continue; + if (!byte.TryParse(label.Substring(comma + 1, nl - comma - 1), out byte col)) continue; + + string keycodeLabel = label.Substring(nl + 1).Trim(); + keycodes[(col, row)] = keycodeLabel; + } + } + } + + return new QmkKeymap + { + Cols = cols, + Rows = rows, + Keycodes = keycodes, + }; + } + catch { return null; } + } + + // ── Cache ──────────────────────────────────────────────────── + + private static string GetCacheDir() + { + string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string dir = Path.Combine(appData, "Chromatics", "QmkKeymaps"); + try { Directory.CreateDirectory(dir); } catch { /* best-effort */ } + return dir; + } + + private static string ResolveCachePath(int vid, int pid) + => Path.Combine(GetCacheDir(), $"{vid:X4}_{pid:X4}.json"); + + private static bool TryLoadCached(string path, out QmkKeymap keymap) + { + keymap = null; + try + { + if (!File.Exists(path)) return false; + string body = File.ReadAllText(path); + keymap = ParseKeymapJson(body); + return keymap != null; + } + catch { return false; } + } + + private static void TrySaveCache(string path, string body) + { + try { File.WriteAllText(path, body); } + catch (Exception ex) + { + Logger.WriteConsole(LoggerTypes.Devices, + $"[QMK] keymap cache write failed for {Path.GetFileName(path)}: {ex.Message}.", + forwardToSentry: false); + } + } + + // ── Layout merge: keymap + LED matrix → ordered LedLayoutEntry list ── + + // Combines the VIA keymap (col,row → keycode label) with the firmware's + // GetLedInfo per-LED records (firmwareIndex → col,row) to produce a + // list ordered by firmwareIndex where each LED has a semantic + // LedId.Keyboard_* whenever the keycode label is mappable, or + // LedId.Custom1+i when it isn't. + // + // Physical layout (Point/Size) is approximated from the matrix + // coordinates so the Avalonia preview shows a recognisable grid + // shape out of the box; users can fine-tune via the Mapping tab. + public static IReadOnlyList BuildLayout( + QmkKeymap keymap, + IReadOnlyList<(int firmwareIndex, byte col, byte row)> ledRecords) + { + const float cellW = 60f; + const float cellH = 60f; + var entries = new List(ledRecords.Count); + + for (int i = 0; i < ledRecords.Count; i++) + { + var rec = ledRecords[i]; + LedId ledId = LedId.Invalid; + + if (keymap?.Keycodes != null + && keymap.Keycodes.TryGetValue((rec.col, rec.row), out string keycode)) + { + ledId = QmkKeycodeMap.ToLedId(keycode); + } + + if (ledId == LedId.Invalid) + ledId = (LedId)((int)LedId.Custom1 + i); + + var location = new Point(rec.col * cellW, rec.row * cellH); + var size = new Size(cellW, cellH); + entries.Add(new QmkLedLayoutEntry(rec.firmwareIndex, rec.col, rec.row, ledId, location, size)); + } + + // De-dupe LedIds (a keymap might claim two LEDs share a semantic + // id — e.g. left+right shift sometimes both map to KC_LSFT). + // RGB.NET requires unique ids per device, so the second + // collision falls back to Custom1+i. + var seen = new HashSet(); + for (int i = 0; i < entries.Count; i++) + { + if (entries[i].PreferredLedId == LedId.Invalid) continue; + if (seen.Add(entries[i].PreferredLedId)) continue; + entries[i] = new QmkLedLayoutEntry( + entries[i].FirmwareIndex, entries[i].MatrixCol, entries[i].MatrixRow, + (LedId)((int)LedId.Custom1 + i), + entries[i].Location, entries[i].Size); + } + + return entries; + } + } +} diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidClientDefinition.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidClientDefinition.cs new file mode 100644 index 00000000..c6fc8357 --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidClientDefinition.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using RGBNetCore = global::RGB.NET.Core; + +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid +{ + // Runtime descriptor for an adopted QMK keyboard. Captures the + // identifying metadata from the firmware handshake plus the + // resolved LED layout (per-key semantic LedIds when a VIA keymap + // was fetchable, Custom1..N otherwise) so the device and update + // queue don't have to re-probe on every Load. + public sealed class QmkRawHidClientDefinition + { + public QmkRawHidClientDefinition( + int vendorId, int productId, + string manufacturer, string product, + string firmwareDeviceName, + int ledCount, + QmkRawHidProtocolMode protocol, + byte matrixColumns, byte matrixRows, + string viaKeymapKey, + IReadOnlyList layout) + { + VendorId = vendorId; + ProductId = productId; + Manufacturer = manufacturer ?? string.Empty; + Product = product ?? string.Empty; + FirmwareDeviceName = firmwareDeviceName ?? string.Empty; + LedCount = ledCount; + Protocol = protocol; + MatrixColumns = matrixColumns; + MatrixRows = matrixRows; + ViaKeymapKey = viaKeymapKey ?? string.Empty; + Layout = layout ?? System.Array.Empty(); + } + + public int VendorId { get; } + public int ProductId { get; } + public string Manufacturer { get; } + public string Product { get; } + public string FirmwareDeviceName { get; } + public int LedCount { get; } + public QmkRawHidProtocolMode Protocol { get; } + public byte MatrixColumns { get; } + public byte MatrixRows { get; } + public string ViaKeymapKey { get; } + + // One entry per RGB LED reported by the firmware, ordered by LED + // index. For OpenRgbQmk mode this is populated from + // Cmd_GetLedInfo + the optional VIA keymap merge. For ViaOnly + // mode the list contains a single entry (LedId.Custom1) used + // for the single representative-colour LED. + public IReadOnlyList Layout { get; } + + // Stable identity for matching against persisted SettingsModel + // entries. Manufacturer/Product strings vary in case across firmware + // builds (e.g. "NovelKeys" vs "Novelkeys"); compare case-insensitive. + public string Identity => $"{VendorId:X4}:{ProductId:X4}:{Manufacturer}:{Product}"; + } + + // One physical LED's placement on the host-side keyboard layout. + // MatrixCol/MatrixRow are the firmware's LED-matrix coordinates; + // PreferredLedId is the semantic RGB.NET id when we matched against a + // VIA keymap (e.g. LedId.Keyboard_A), or LedId.Custom1 + index when + // we fell back to Custom1..N. PreferredLedId is consumed by + // QmkRawHidDevice.InitializeLayout. + public readonly struct QmkLedLayoutEntry + { + public readonly int FirmwareIndex; + public readonly byte MatrixCol; + public readonly byte MatrixRow; + public readonly RGBNetCore.LedId PreferredLedId; + public readonly RGBNetCore.Point Location; + public readonly RGBNetCore.Size Size; + + public QmkLedLayoutEntry(int firmwareIndex, byte matrixCol, byte matrixRow, RGBNetCore.LedId preferredLedId, RGBNetCore.Point location, RGBNetCore.Size size) + { + FirmwareIndex = firmwareIndex; + MatrixCol = matrixCol; + MatrixRow = matrixRow; + PreferredLedId = preferredLedId; + Location = location; + Size = size; + } + } +} diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidDevice.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidDevice.cs new file mode 100644 index 00000000..75d0a891 --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidDevice.cs @@ -0,0 +1,63 @@ +using Chromatics.Extensions.RGB.NET.ColorCorrections; +using RGB.NET.Core; + +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid +{ + public class QmkRawHidDevice : AbstractRGBDevice + { + private readonly QmkRawHidUpdateQueue _updateQueue; + private readonly QmkRawHidClientDefinition _def; + + public QmkRawHidDevice(QmkRawHidDeviceInfo info, QmkRawHidUpdateQueue updateQueue, QmkRawHidClientDefinition def) + : base(info, updateQueue) + { + _updateQueue = updateQueue; + _def = def; + InitializeLayout(); + } + + public QmkRawHidClientDefinition Definition => _def; + + // Per-device lifecycle parity with LifxDevice / HueDevice — the + // queue gate flag is the primary mechanism for stopping paint + // frames on per-device disable in the Mapping tab. + public void SetPerDeviceDisabled(bool disabled) => _updateQueue.SetPerDeviceDisabled(disabled); + public void ResetCache() => _updateQueue.ResetCache(); + public void BeginShutdown() => _updateQueue.BeginShutdown(); + + // Per-device brightness — Hue uses xy chromaticity so it needs a + // separate channel; QMK's RGB matrix is plain RGB so the global + // brightness correction is sufficient. Method kept for parity in + // case future firmware exposes a bridge-style brightness query. + public void SetPerDeviceBrightness(PerDeviceBrightnessCorrection correction) + => _updateQueue.SetPerDeviceBrightness(correction); + + // Layout: VIA-only devices show up as a single representative LED + // (LedId.Custom1) since VIA can't drive per-key from the host. + // OpenRGB-QMK devices iterate the Layout entries the provider built + // — these carry either LedId.Keyboard_* semantic IDs (when a VIA + // keymap was fetchable from the via-keyboards repo) or LedId.Custom1+i + // synthetic ids as a fallback. RGB.NET treats both identically; the + // semantic IDs are just what lets Chromatics' Highlight / Keybinds + // layers light the right physical keys out of the box. + private void InitializeLayout() + { + if (_def.Protocol == QmkRawHidProtocolMode.ViaOnly || _def.Layout.Count == 0) + { + var single = AddLed(LedId.Custom1, new Point(0, 0), new Size(60, 60)); + if (single != null) single.Shape = Shape.Rectangle; + return; + } + + // OpenRGB-QMK: one LED per firmware index, ordered by FirmwareIndex + // so the UpdateQueue's bulk-set path can pack rgbBytes in the + // canonical strip order without a re-sort per frame. + for (int i = 0; i < _def.Layout.Count; i++) + { + var entry = _def.Layout[i]; + var led = AddLed(entry.PreferredLedId, entry.Location, entry.Size); + if (led != null) led.Shape = Shape.Rectangle; + } + } + } +} diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidDeviceInfo.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidDeviceInfo.cs new file mode 100644 index 00000000..a641f149 --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidDeviceInfo.cs @@ -0,0 +1,23 @@ +using RGB.NET.Core; + +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid +{ + public class QmkRawHidDeviceInfo : IRGBDeviceInfo + { + public QmkRawHidDeviceInfo(QmkRawHidClientDefinition def) + { + DeviceName = string.IsNullOrEmpty(def.Product) + ? (string.IsNullOrEmpty(def.FirmwareDeviceName) ? "QMK Keyboard" : def.FirmwareDeviceName) + : def.Product; + Manufacturer = string.IsNullOrEmpty(def.Manufacturer) ? "QMK" : def.Manufacturer; + Model = string.IsNullOrEmpty(def.FirmwareDeviceName) ? def.Product : def.FirmwareDeviceName; + DeviceType = RGBDeviceType.Keyboard; + } + + public RGBDeviceType DeviceType { get; } + public string DeviceName { get; } + public string Manufacturer { get; } + public string Model { get; } + public object LayoutMetadata { get; set; } + } +} diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidProtocolMode.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidProtocolMode.cs new file mode 100644 index 00000000..1636f5ff --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidProtocolMode.cs @@ -0,0 +1,11 @@ +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid +{ + // Which protocol the UpdateQueue should target on a given device. Set + // once at handshake (see QmkRawHidDiscovery) and stable for the + // lifetime of the device — we don't downgrade mid-session. + public enum QmkRawHidProtocolMode + { + ViaOnly, + OpenRgbQmk, + } +} diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidRGBDeviceProvider.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidRGBDeviceProvider.cs new file mode 100644 index 00000000..e1a674b5 --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidRGBDeviceProvider.cs @@ -0,0 +1,457 @@ +using Chromatics.Core; +using Chromatics.Enums; +using Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.Protocol; +using HidSharp; +using RGB.NET.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid +{ + // Custom RGB.NET device provider for QMK Raw HID keyboards — covers + // NovelKeys, KBDFans, Drop, GMMK, Glorious and anything else running + // QMK firmware with Raw HID enabled. Talks to two protocols on the + // same HID interface: + // - VIA (universal, exposes only RGB matrix mode + base hue/sat/val) + // - OpenRGB-QMK plugin (per-key control via direct mode) + // + // Protocol is decided per device at handshake. VIA-only keyboards + // become a single-LED device that drives the firmware's RGB matrix + // base colour; OpenRGB-QMK keyboards become a full per-key keyboard + // with semantic LedId.Keyboard_* IDs when a VIA keymap is fetchable + // from www.caniusevia.com (Custom1..N fallback otherwise). + // + // Hot-plug parity with PlayStationControllerRGBDeviceProvider — + // DeviceList.Local.Changed reconciles the open set on USB connect / + // disconnect events. Adoption picker UX parity with LIFX/Hue — + // ClientDefinitions is hydrated from SettingsModel before LoadDevices + // runs; an empty list short-circuits the load so users explicitly opt + // in to which boards Chromatics drives. + public class QmkRawHidRGBDeviceProvider : AbstractRGBDeviceProvider + { + #region Singleton + + private static QmkRawHidRGBDeviceProvider _instance; + public static QmkRawHidRGBDeviceProvider Instance => _instance ?? new QmkRawHidRGBDeviceProvider(); + + public QmkRawHidRGBDeviceProvider() + { + if (_instance != null) Throw(new Exception($"There can be only one instance of type {nameof(QmkRawHidRGBDeviceProvider)}")); + _instance = this; + } + + #endregion + + #region Configuration & state + + // 30Hz update rate — same cadence as the PlayStation provider. QMK + // boards accept sustained 30Hz over Raw HID without flooding the + // USB EP RX queue; faster than this risks dropped packets on + // multi-zone bulk updates. + private const double UpdateFrequencySeconds = 1.0 / 30.0; + + // Same debounce as PlayStation. Windows fires several PnP events + // for one logical USB connect; we wait long enough for the device + // tree to settle before re-enumerating, otherwise TryOpen wins a + // partially-enumerated handle and the first write fails. + private const int HotplugDebounceMs = 1500; + + public List AdoptedDevices { get; } = new(); + + private readonly Dictionary _openStreams = new(); + private readonly Dictionary _devicePaths = new(); + private bool _hotplugSubscribed; + private int _hotplugScheduleSeq; + + #endregion + + protected override void InitializeSDK() + { + if (!_hotplugSubscribed) + { + DeviceList.Local.Changed += OnHidDeviceListChanged; + _hotplugSubscribed = true; + } + } + + protected override IDeviceUpdateTrigger CreateUpdateTrigger(int id, double updateRateHardLimit) + => new QmkRawHidUpdateTrigger(UpdateFrequencySeconds); + + protected override IEnumerable LoadDevices() + { + return LoadDevicesAsync().GetAwaiter().GetResult(); + } + + // Async device load — runs the handshake on every candidate, fetches + // VIA keymaps in parallel for adopted boards (network calls run + // concurrently rather than serial), then materialises one + // QmkRawHidDevice per adopted+responsive board. + private async Task> LoadDevicesAsync() + { + var devices = new List(); + if (AdoptedDevices.Count == 0) return devices; + + var candidates = QmkRawHidDiscovery.Discover(); + if (candidates.Count == 0) + { + Logger.WriteConsole(LoggerTypes.Devices, + "[QMK] No QMK Raw HID keyboards detected on the USB bus. " + + "Make sure your keyboard's firmware has Raw HID enabled (it's the default for most VIA-compatible builds) " + + "and that no other app (VIA, Vial, OpenRGB) is holding the Raw HID interface exclusively.", + forwardToSentry: false); + return devices; + } + + // Reduce candidate list to adopted entries up-front — no point + // running keymap fetches for boards the user didn't pick. + var adoptedCandidates = new List(candidates.Count); + foreach (var c in candidates) + { + if (!IsAdopted(c)) continue; + adoptedCandidates.Add(c); + } + if (adoptedCandidates.Count == 0) return devices; + + // Fetch keymaps in parallel for the OpenRGB-QMK ones. VIA-only + // boards don't use keymaps (single LED), so skip those. + var keymapTasks = new Dictionary<(int vid, int pid), Task>(); + foreach (var c in adoptedCandidates) + { + if (c.Protocol != QmkRawHidDiscovery.ProtocolSupport.OpenRgbQmk) continue; + var key = (c.Hid.VendorID, c.Hid.ProductID); + if (keymapTasks.ContainsKey(key)) continue; + keymapTasks[key] = QmkKeymapFetcher.TryGetKeymapAsync(c.Hid.VendorID, c.Hid.ProductID); + } + if (keymapTasks.Count > 0) + await Task.WhenAll(keymapTasks.Values).ConfigureAwait(false); + + foreach (var c in adoptedCandidates) + { + try + { + if (!c.Hid.TryOpen(out HidStream stream)) + { + Logger.WriteConsole(LoggerTypes.Error, + $"[QMK] Could not open {SafeProductName(c.Hid)} ({c.Hid.VendorID:X4}:{c.Hid.ProductID:X4}). " + + "Another app may be holding the Raw HID interface exclusively (VIA, Vial, OpenRGB).", + forwardToSentry: false); + continue; + } + stream.ReadTimeout = QmkRawHidConstants.ResponseTimeoutMs; + stream.WriteTimeout = QmkRawHidConstants.ResponseTimeoutMs; + + QmkKeymapFetcher.QmkKeymap keymap = null; + if (c.Protocol == QmkRawHidDiscovery.ProtocolSupport.OpenRgbQmk + && keymapTasks.TryGetValue((c.Hid.VendorID, c.Hid.ProductID), out var kmTask)) + { + keymap = kmTask.Result; + } + + var layout = BuildLayoutForCandidate(stream, c, keymap); + + var def = new QmkRawHidClientDefinition( + vendorId: c.Hid.VendorID, + productId: c.Hid.ProductID, + manufacturer: SafeManufacturer(c.Hid), + product: SafeProductName(c.Hid), + firmwareDeviceName: c.FirmwareDeviceName, + ledCount: layout.Count == 0 ? 1 : layout.Count, + protocol: c.Protocol == QmkRawHidDiscovery.ProtocolSupport.OpenRgbQmk + ? QmkRawHidProtocolMode.OpenRgbQmk + : QmkRawHidProtocolMode.ViaOnly, + matrixColumns: c.MatrixColumns, + matrixRows: c.MatrixRows, + viaKeymapKey: string.Empty, + layout: layout); + + var trigger = (QmkRawHidUpdateTrigger)GetUpdateTrigger(); + var queue = new QmkRawHidUpdateQueue(trigger, def, stream); + var info = new QmkRawHidDeviceInfo(def); + var dev = new QmkRawHidDevice(info, queue, def); + + _openStreams[dev] = stream; + _devicePaths[dev] = c.Hid.DevicePath; + devices.Add(dev); + } + catch (Exception ex) + { + Logger.WriteConsole(LoggerTypes.Error, + $"[QMK] Failed to set up {SafeProductName(c.Hid)}: {ex.Message}", + forwardToSentry: false); + } + } + + return devices; + } + + // For VIA-only: a single Custom1 LED. + // For OpenRGB-QMK: enumerate the firmware's LED records (paged via + // Cmd_GetLedInfo), then merge against the optional VIA keymap to + // produce the semantic LedId list. + private static IReadOnlyList BuildLayoutForCandidate( + HidStream stream, + QmkRawHidDiscovery.Candidate candidate, + QmkKeymapFetcher.QmkKeymap keymap) + { + if (candidate.Protocol == QmkRawHidDiscovery.ProtocolSupport.ViaOnly) + { + return new[] + { + new QmkLedLayoutEntry(0, 0, 0, LedId.Custom1, new Point(0, 0), new Size(60, 60)), + }; + } + + var records = FetchAllLedRecords(stream, candidate.LedCount); + if (records.Count == 0) + { + // OpenRGB-QMK responded to GetDeviceInfo but failed to return + // LED records — degrade to a synthetic grid sized by LedCount + // so the device still appears for the user to position + // manually in the Mapping tab. + return SyntheticGrid(candidate.LedCount, candidate.MatrixColumns); + } + + return QmkKeymapFetcher.BuildLayout(keymap, records); + } + + private static List<(int firmwareIndex, byte col, byte row)> FetchAllLedRecords(HidStream stream, int totalLeds) + { + var records = new List<(int, byte, byte)>(totalLeds); + byte[] outBuf = new byte[QmkRawHidConstants.OutputReportBytes]; + byte[] inBuf = new byte[QmkRawHidConstants.ReportPayloadBytes + 1]; + + int next = 0; + int safetyBudget = (totalLeds / 8) + 32; // upper bound on batch iterations + while (next < totalLeds && safetyBudget-- > 0) + { + OpenRgbQmkProtocol.BuildGetLedInfo( + new Span(outBuf, 1, QmkRawHidConstants.ReportPayloadBytes), + (ushort)next); + try + { + stream.Write(outBuf); + int n = stream.Read(inBuf, 0, inBuf.Length); + if (n <= 0) break; + } + catch { break; } + + if (!OpenRgbQmkProtocol.TryParseLedInfoBatch( + new ReadOnlySpan(inBuf, 1, inBuf.Length - 1), + out int batchCount, out var batch)) + { + break; + } + if (batchCount == 0) break; + + for (int i = 0; i < batchCount && next < totalLeds; i++, next++) + { + byte col = batch[i * 3]; + byte row = batch[i * 3 + 1]; + // batch[i*3+2] is the flags byte; not used here. + records.Add((next, col, row)); + } + } + return records; + } + + private static IReadOnlyList SyntheticGrid(int ledCount, byte hintColumns) + { + int cols = hintColumns > 0 ? hintColumns : Math.Max(1, (int)Math.Ceiling(Math.Sqrt(ledCount * 4.0 / 3.0))); + const float cell = 60f; + var list = new List(ledCount); + for (int i = 0; i < ledCount; i++) + { + int col = i % cols; + int row = i / cols; + list.Add(new QmkLedLayoutEntry( + firmwareIndex: i, + matrixCol: (byte)col, + matrixRow: (byte)row, + preferredLedId: (LedId)((int)LedId.Custom1 + i), + location: new Point(col * cell, row * cell), + size: new Size(cell, cell))); + } + return list; + } + + // ── Adopted-device matching ─────────────────────────────────── + + private bool IsAdopted(QmkRawHidDiscovery.Candidate candidate) + { + foreach (var ad in AdoptedDevices) + { + if (ad.Matches(candidate.Hid)) return true; + } + return false; + } + + // ── Hot-plug ────────────────────────────────────────────────── + + private void OnHidDeviceListChanged(object sender, DeviceListChangedEventArgs e) + { + int seq = System.Threading.Interlocked.Increment(ref _hotplugScheduleSeq); + Task.Run(async () => + { + await Task.Delay(HotplugDebounceMs).ConfigureAwait(false); + if (seq != _hotplugScheduleSeq) return; // newer event superseded us + try { Reconcile(); } catch (Exception ex) + { + Logger.WriteConsole(LoggerTypes.Error, $"[QMK] hot-plug reconcile failed: {ex.Message}", forwardToSentry: false); + } + }); + } + + // Re-enumerate USB and compare to our open set: remove disappeared + // devices, add freshly-connected adopted ones. Same shape as the + // PlayStation provider's reconcile. + private void Reconcile() + { + var currentPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + try + { + foreach (var hid in DeviceList.Local.GetHidDevices()) + { + try { currentPaths.Add(hid.DevicePath); } catch { /* ignore */ } + } + } + catch { return; } + + // Drop devices whose path is no longer present. + var toRemove = new List(); + foreach (var kvp in _devicePaths) + { + if (!currentPaths.Contains(kvp.Value)) + toRemove.Add(kvp.Key); + } + foreach (var dev in toRemove) + { + RemoveDevice(dev); + if (_openStreams.TryGetValue(dev, out var s)) + { + try { s.Dispose(); } catch { /* ignore */ } + _openStreams.Remove(dev); + } + _devicePaths.Remove(dev); + } + + // Add newly-connected adopted devices. + var existingPaths = new HashSet(_devicePaths.Values, StringComparer.OrdinalIgnoreCase); + var freshCandidates = QmkRawHidDiscovery.Discover(); + foreach (var c in freshCandidates) + { + if (!IsAdopted(c)) continue; + if (existingPaths.Contains(c.Hid.DevicePath)) continue; + + try + { + if (!c.Hid.TryOpen(out HidStream stream)) continue; + stream.ReadTimeout = QmkRawHidConstants.ResponseTimeoutMs; + stream.WriteTimeout = QmkRawHidConstants.ResponseTimeoutMs; + + var keymap = c.Protocol == QmkRawHidDiscovery.ProtocolSupport.OpenRgbQmk + ? QmkKeymapFetcher.TryGetKeymapAsync(c.Hid.VendorID, c.Hid.ProductID).GetAwaiter().GetResult() + : null; + + var layout = BuildLayoutForCandidate(stream, c, keymap); + + var def = new QmkRawHidClientDefinition( + c.Hid.VendorID, c.Hid.ProductID, + SafeManufacturer(c.Hid), SafeProductName(c.Hid), + c.FirmwareDeviceName, layout.Count == 0 ? 1 : layout.Count, + c.Protocol == QmkRawHidDiscovery.ProtocolSupport.OpenRgbQmk + ? QmkRawHidProtocolMode.OpenRgbQmk : QmkRawHidProtocolMode.ViaOnly, + c.MatrixColumns, c.MatrixRows, string.Empty, layout); + + var trigger = (QmkRawHidUpdateTrigger)GetUpdateTrigger(); + var queue = new QmkRawHidUpdateQueue(trigger, def, stream); + var info = new QmkRawHidDeviceInfo(def); + var dev = new QmkRawHidDevice(info, queue, def); + + _openStreams[dev] = stream; + _devicePaths[dev] = c.Hid.DevicePath; + AddDevice(dev); + } + catch (Exception ex) + { + Logger.WriteConsole(LoggerTypes.Error, $"[QMK] hot-plug add failed for {SafeProductName(c.Hid)}: {ex.Message}", forwardToSentry: false); + } + } + } + + // ── Dispose ─────────────────────────────────────────────────── + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_hotplugSubscribed) + { + try { DeviceList.Local.Changed -= OnHidDeviceListChanged; } catch { /* ignore */ } + _hotplugSubscribed = false; + } + + foreach (var dev in Devices.OfType()) + { + try { dev.BeginShutdown(); } catch { /* ignore */ } + } + foreach (var s in _openStreams.Values) + { + try { s.Dispose(); } catch { /* ignore */ } + } + _openStreams.Clear(); + _devicePaths.Clear(); + } + + base.Dispose(disposing); + + if (ReferenceEquals(_instance, this)) + _instance = null; + } + + // ── HidSharp string accessors (Manufacturer/Product can throw on disconnected handles) ── + + private static string SafeManufacturer(HidDevice hid) + { + try { return hid.GetManufacturer() ?? string.Empty; } + catch { return string.Empty; } + } + private static string SafeProductName(HidDevice hid) + { + try { return hid.GetProductName() ?? string.Empty; } + catch { return string.Empty; } + } + } + + // Identity filter used by the adopted-set check. Built from the + // SettingsModel's persisted QmkRawHidAdoptedDevice records so the + // provider doesn't have to depend on the Models layer for runtime + // matching. + public sealed class QmkRawHidAdoptedDeviceFilter + { + public int VendorId { get; } + public int ProductId { get; } + public string Manufacturer { get; } + public string Product { get; } + + public QmkRawHidAdoptedDeviceFilter(int vendorId, int productId, string manufacturer, string product) + { + VendorId = vendorId; + ProductId = productId; + Manufacturer = manufacturer ?? string.Empty; + Product = product ?? string.Empty; + } + + public bool Matches(HidDevice hid) + { + if (hid.VendorID != VendorId) return false; + if (hid.ProductID != ProductId) return false; + string mfg, prod; + try { mfg = hid.GetManufacturer() ?? string.Empty; } catch { mfg = string.Empty; } + try { prod = hid.GetProductName() ?? string.Empty; } catch { prod = string.Empty; } + return string.Equals(mfg, Manufacturer, StringComparison.OrdinalIgnoreCase) + && string.Equals(prod, Product, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidUpdateQueue.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidUpdateQueue.cs new file mode 100644 index 00000000..1fe5ff5a --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidUpdateQueue.cs @@ -0,0 +1,285 @@ +using Chromatics.Core; +using Chromatics.Enums; +using Chromatics.Extensions.RGB.NET.ColorCorrections; +using Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.Protocol; +using HidSharp; +using RGB.NET.Core; +using System; +using System.Collections.Generic; +using System.Threading; +using Color = RGB.NET.Core.Color; + +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid +{ + public class QmkRawHidUpdateQueue : UpdateQueue + { + #region Properties & Fields + + private readonly QmkRawHidClientDefinition _def; + private readonly HidStream _stream; + private readonly Lock _lock = new(); + private volatile bool _shuttingDown; + // Per-device disable gate — parity with LifxUpdateQueue. Toggled + // from RGBController.RemoveDevice / AddDevice so paint frames are + // dropped while the user has the keyboard disabled in the Mapping + // tab. Cleared on re-enable. + private volatile bool _perDeviceDisable; + + private PerDeviceBrightnessCorrection _perDeviceBrightness; + + // LedId → firmware LED index. Built once at construction from the + // client definition's Layout so per-frame lookups avoid linear scans. + private readonly Dictionary _ledIndexByLedId; + + // Persistent RGB byte cache for the entire strip — same idea as + // LifxUpdateQueue._strip. Lets sparse decorator updates (a few LEDs + // changed) only resend their chunks rather than the whole strip. + // 3 bytes per LED (R, G, B). Null until the first frame. + private byte[] _ledBytes; + + // Single-LED VIA path coalescing: last sent hue/sat/brightness/effect + // tuple. Set to 0xFFFF when empty so the first frame always sends. + private ushort _lastViaHsbHash = 0xFFFF; + + // True once we've put the OpenRGB-QMK firmware into direct mode + // (mode 0). On enter direct mode the firmware suspends built-in + // RGB matrix effects so our Set commands aren't fighting them. + private bool _openRgbDirectModeArmed; + + #endregion + + #region Constructors + + public QmkRawHidUpdateQueue(IDeviceUpdateTrigger trigger, QmkRawHidClientDefinition def, HidStream stream) + : base(trigger) + { + _def = def; + _stream = stream; + _ledIndexByLedId = new Dictionary(def.Layout.Count); + for (int i = 0; i < def.Layout.Count; i++) + _ledIndexByLedId[def.Layout[i].PreferredLedId] = def.Layout[i].FirmwareIndex; + } + + #endregion + + #region Methods + + public void BeginShutdown() => _shuttingDown = true; + + public void SetPerDeviceDisabled(bool disabled) => _perDeviceDisable = disabled; + + public void ResetCache() + { + lock (_lock) + { + _ledBytes = null; + _lastViaHsbHash = 0xFFFF; + _openRgbDirectModeArmed = false; + } + } + + public void SetPerDeviceBrightness(PerDeviceBrightnessCorrection correction) + => _perDeviceBrightness = correction; + + protected override bool Update(ReadOnlySpan<(object key, Color color)> dataSet) + { + lock (_lock) + { + if (_shuttingDown || _perDeviceDisable) return true; + if (dataSet.IsEmpty) return true; + + try + { + int globalPct = GlobalBrightnessCorrection.Instance.BrightnessPercent; + int perDevicePct = _perDeviceBrightness?.BrightnessPercent ?? 100; + double brightnessScale = (globalPct / 100.0) * (perDevicePct / 100.0); + + if (_def.Protocol == QmkRawHidProtocolMode.OpenRgbQmk) + SendOpenRgbQmkFrame(dataSet, brightnessScale); + else + SendViaFrame(dataSet, brightnessScale); + + return true; + } + catch (Exception ex) + { + QmkRawHidRGBDeviceProvider.Instance?.Throw(ex); + return false; + } + } + } + + // ── VIA path (Tier 1: single representative colour) ────────── + + private void SendViaFrame(ReadOnlySpan<(object key, Color color)> dataSet, double brightnessScale) + { + Color picked = PickRepresentativeColor(dataSet); + ToHsv255(picked, brightnessScale, out byte hue, out byte sat, out byte val); + + // Pack hue/sat/val + effect index into a single 16-bit hash to + // cheaply detect "nothing changed since last frame" and skip + // the three USB writes the VIA path would otherwise burn. + // 7-bit hue, 5-bit sat, 4-bit val is enough resolution for + // change detection without ever sending a no-op. + ushort hash = (ushort)(((hue >> 1) & 0x7F) << 9 + | ((sat >> 3) & 0x1F) << 4 + | ((val >> 4) & 0x0F)); + if (hash == _lastViaHsbHash) return; + _lastViaHsbHash = hash; + + Span outBuf = stackalloc byte[QmkRawHidConstants.OutputReportBytes]; + Span payload = outBuf.Slice(1, QmkRawHidConstants.ReportPayloadBytes); + + ViaProtocol.BuildSetRgbMatrixEffect(payload, ViaProtocol.Effect_SolidColor); + WritePayload(outBuf); + + ViaProtocol.BuildSetRgbMatrixColor(payload, hue, sat); + WritePayload(outBuf); + + ViaProtocol.BuildSetRgbMatrixBrightness(payload, val); + WritePayload(outBuf); + } + + // ── OpenRGB-QMK path (Tier 2: per-key) ─────────────────────── + + private void SendOpenRgbQmkFrame(ReadOnlySpan<(object key, Color color)> dataSet, double brightnessScale) + { + int total = _def.LedCount; + if (total <= 0) return; + + if (_ledBytes == null) _ledBytes = new byte[total * 3]; + + // Arm direct mode once per session. The firmware persists the + // mode setting in RAM, so we don't have to re-arm every frame — + // but DO re-arm after a ResetCache (which clears _openRgbDirectModeArmed + // alongside _ledBytes, so the next Update arms again). + if (!_openRgbDirectModeArmed) + { + Span armBuf = stackalloc byte[QmkRawHidConstants.OutputReportBytes]; + OpenRgbQmkProtocol.BuildSetMode(armBuf.Slice(1, QmkRawHidConstants.ReportPayloadBytes), modeIndex: 0); + WritePayload(armBuf); + _openRgbDirectModeArmed = true; + } + + // Patch dirty LEDs into _ledBytes and remember which chunks + // (LED indices grouped by MaxLedsPerSetRange) need re-sending. + int chunkSize = OpenRgbQmkProtocol.MaxLedsPerSetRange; + int chunkCount = (total + chunkSize - 1) / chunkSize; + Span dirty = chunkCount <= 256 ? stackalloc bool[chunkCount] : new bool[chunkCount]; + + foreach (var (key, color) in dataSet) + { + int idx = TryResolveLedIndex(key); + if (idx < 0 || idx >= total) continue; + + ToRgb255(color, brightnessScale, out byte r, out byte g, out byte b); + int off = idx * 3; + if (_ledBytes[off] == r && _ledBytes[off + 1] == g && _ledBytes[off + 2] == b) continue; + + _ledBytes[off] = r; + _ledBytes[off + 1] = g; + _ledBytes[off + 2] = b; + dirty[idx / chunkSize] = true; + } + + // Send each dirty chunk. Inter-packet pacing keeps us within the + // firmware's RX queue depth on full-speed USB — 1ms is well above + // QMK's worst-case raw_hid_receive turnaround. + Span outBuf = stackalloc byte[QmkRawHidConstants.OutputReportBytes]; + bool first = true; + for (int c = 0; c < chunkCount; c++) + { + if (!dirty[c]) continue; + if (!first) Thread.Sleep(1); + first = false; + + ushort start = (ushort)(c * chunkSize); + int count = Math.Min(chunkSize, total - start); + OpenRgbQmkProtocol.BuildSetLedRange( + outBuf.Slice(1, QmkRawHidConstants.ReportPayloadBytes), + start, (byte)count, + new ReadOnlySpan(_ledBytes, start * 3, count * 3)); + WritePayload(outBuf); + } + } + + private int TryResolveLedIndex(object key) + { + if (key is LedId id && _ledIndexByLedId.TryGetValue(id, out int idx)) return idx; + return -1; + } + + // ── HID write ──────────────────────────────────────────────── + + private void WritePayload(ReadOnlySpan outBuf) + { + if (_stream == null) return; + try { _stream.Write(outBuf.ToArray()); } + catch (System.IO.IOException ex) + { + // PnP unplug between Write call and current frame. Mark + // shutting down so the rest of this batch becomes no-ops; + // the provider's hot-plug reconcile will dispose us shortly. + Logger.WriteConsole(LoggerTypes.Devices, $"[QMK] {_def.Product}: write failed ({ex.Message}); pausing queue."); + _shuttingDown = true; + } + catch (ObjectDisposedException) + { + _shuttingDown = true; + } + } + + // ── Colour helpers ─────────────────────────────────────────── + + private static Color PickRepresentativeColor(ReadOnlySpan<(object key, Color color)> dataSet) + { + if (dataSet.Length == 1) return dataSet[0].color; + int maxIdx = 0; + double maxL = -1; + for (int i = 0; i < dataSet.Length; i++) + { + double l = dataSet[i].color.R + dataSet[i].color.G + dataSet[i].color.B; + if (l > maxL) { maxL = l; maxIdx = i; } + } + return dataSet[maxIdx].color; + } + + private static void ToHsv255(Color rgb, double brightnessScale, out byte hue, out byte sat, out byte val) + { + double r = Math.Clamp(rgb.R, 0.0, 1.0); + double g = Math.Clamp(rgb.G, 0.0, 1.0); + double bb = Math.Clamp(rgb.B, 0.0, 1.0); + + double max = Math.Max(r, Math.Max(g, bb)); + double min = Math.Min(r, Math.Min(g, bb)); + double delta = max - min; + + double h = 0; + if (delta > 0) + { + if (max == r) h = ((g - bb) / delta) % 6; + else if (max == g) h = ((bb - r) / delta) + 2; + else h = ((r - g) / delta) + 4; + h *= 60; + if (h < 0) h += 360; + } + + double s = max == 0 ? 0 : delta / max; + double v = max * Math.Clamp(brightnessScale, 0.0, 1.0); + + hue = (byte)Math.Round((h / 360.0) * 255); + sat = (byte)Math.Round(s * 255); + val = (byte)Math.Round(v * 255); + } + + private static void ToRgb255(Color rgb, double brightnessScale, out byte r, out byte g, out byte b) + { + double scale = Math.Clamp(brightnessScale, 0.0, 1.0); + r = (byte)Math.Round(Math.Clamp(rgb.R, 0.0, 1.0) * 255 * scale); + g = (byte)Math.Round(Math.Clamp(rgb.G, 0.0, 1.0) * 255 * scale); + b = (byte)Math.Round(Math.Clamp(rgb.B, 0.0, 1.0) * 255 * scale); + } + + #endregion + } +} diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidUpdateTrigger.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidUpdateTrigger.cs new file mode 100644 index 00000000..db02e150 --- /dev/null +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkRawHidUpdateTrigger.cs @@ -0,0 +1,48 @@ +using RGB.NET.Core; +using System; +using System.Diagnostics; +using System.Threading; + +namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid +{ + // 30Hz trigger with idle wake every 50ms so per-device brightness + // slider changes propagate to a board sitting on a static layer. + // Matches the LifxDeviceUpdateTrigger shape — refresh tick fires + // CustomUpdateData("refresh") so the queue's OnUpdate can no-op + // gracefully on empty datasets without missing slider-driven + // re-sends if we later add brightness/effect mode change support. + public class QmkRawHidUpdateTrigger : DeviceUpdateTrigger + { + private const int WaitOneTimeoutMs = 50; + + public QmkRawHidUpdateTrigger() { } + + public QmkRawHidUpdateTrigger(double updateRateHardLimit) + : base(updateRateHardLimit) { } + + protected override void UpdateLoop() + { + OnStartup(); + + while (!UpdateToken.IsCancellationRequested) + { + if (HasDataEvent.WaitOne(WaitOneTimeoutMs)) + { + long preUpdateTicks = Stopwatch.GetTimestamp(); + OnUpdate(); + + if (UpdateFrequency > 0) + { + double elapsedMs = (Stopwatch.GetTimestamp() - preUpdateTicks) / (double)TimeSpan.TicksPerMillisecond; + int sleep = (int)(UpdateFrequency * 1000.0 - elapsedMs); + if (sleep > 0) Thread.Sleep(sleep); + } + } + else + { + OnUpdate(new CustomUpdateData(("refresh", true))); + } + } + } + } +} diff --git a/Chromatics/Models/QmkRawHidAdoptedDevice.cs b/Chromatics/Models/QmkRawHidAdoptedDevice.cs new file mode 100644 index 00000000..5e1f1912 --- /dev/null +++ b/Chromatics/Models/QmkRawHidAdoptedDevice.cs @@ -0,0 +1,27 @@ +namespace Chromatics.Models +{ + // Persisted (settings.json) record of a user-adopted QMK keyboard. + // + // Identity is the triple (VendorId, ProductId, Manufacturer + Product), + // not the OS HID DevicePath — DevicePath changes when the user moves the + // keyboard between USB ports, but VID/PID + the firmware-reported product + // string are stable across plugs. We re-resolve a concrete HidDevice on + // each provider start via QmkRawHidDiscovery, then match candidates to + // adopted entries on this triple. + // + // Layout (the VIA keymap source URL) is captured at adoption time so a + // subsequent run knows which keymap JSON to fetch + cache without + // re-running detection. Empty string means "no semantic layout known — + // present LEDs as Custom1..N and let the user position them via the + // Mapping tab." + public class QmkRawHidAdoptedDevice + { + public int VendorId { get; set; } + public int ProductId { get; set; } + public string Manufacturer { get; set; } + public string Product { get; set; } + public int LedCount { get; set; } + public string Protocol { get; set; } // "ViaOnly" | "OpenRgbQmk" + public string ViaKeymapKey { get; set; } // via-keyboards repo key (e.g. "novelkeys/nk65"), empty if unmapped + } +} diff --git a/Chromatics/Models/SettingsModel.cs b/Chromatics/Models/SettingsModel.cs index 9e65b4d8..5e29b228 100644 --- a/Chromatics/Models/SettingsModel.cs +++ b/Chromatics/Models/SettingsModel.cs @@ -49,6 +49,8 @@ public class SettingsModel public bool deviceLifxEnabled { get; set; } = false; public List deviceLifxAdoptedDevices { get; set; } = new(); public List deviceHueAdoptedDevices { get; set; } = new(); + public bool deviceQmkRawHidEnabled { get; set; } = false; + public List deviceQmkRawHidAdoptedDevices { get; set; } = new(); public string deviceHueBridgeIP { get; set; } = "127.0.0.1"; public string deviceHueBridgeClientKey { get; set; } = ""; public double deviceHueBridgeBrightness { get; set; } = -1; diff --git a/Chromatics/ViewModels/SettingsViewModel.cs b/Chromatics/ViewModels/SettingsViewModel.cs index ae571dbb..8e4a3c87 100644 --- a/Chromatics/ViewModels/SettingsViewModel.cs +++ b/Chromatics/ViewModels/SettingsViewModel.cs @@ -271,6 +271,83 @@ private void BuildDeviceToggles(Models.SettingsModel s) cur.deviceLifxEnabled = false; AppSettings.SaveSettings(cur); })); + + // QMK Raw HID — auto-adopts every QMK-compatible board on first + // enable (no picker dialog yet; per-keyboard disable via the + // Mapping tab covers the "I don't want this one" case for v1 + // Beta). Covers NovelKeys, KBDFans, Drop, GMMK, Glorious and any + // other custom keyboard running QMK with Raw HID enabled. + DeviceToggles.Add(new DeviceToggleItem( + "QMK Keyboards (Beta)", + "[BETA] Enable/disable QMK Raw HID keyboard support. Auto-adopts any QMK-compatible keyboard with Raw HID enabled (covers NovelKeys, KBDFans, Drop, GMMK, Glorious, and other custom QMK boards). Default: Disabled", + s.deviceQmkRawHidEnabled, + () => + { + var cur = AppSettings.GetSettings(); + + // Discovery + auto-adopt: run on a background thread to + // keep the Settings dialog responsive — per-device VIA + // handshakes can take 200-500ms each on a sluggish USB + // stack, and discovery + handshake of 5+ boards adds up. + return Task.Run(() => + { + var discovered = Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.Protocol.QmkRawHidDiscovery.Discover(); + if (discovered.Count == 0) + { + // No boards responded — leave the toggle off so + // the user sees the immediate "didn't take" UX + // rather than an empty-but-on provider. + return false; + } + + var adopted = new System.Collections.Generic.List(); + var seen = new System.Collections.Generic.HashSet(StringComparer.OrdinalIgnoreCase); + Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.QmkRawHidRGBDeviceProvider.Instance.AdoptedDevices.Clear(); + foreach (var c in discovered) + { + string mfg = ""; string prod = ""; + try { mfg = c.Hid.GetManufacturer() ?? ""; } catch { } + try { prod = c.Hid.GetProductName() ?? ""; } catch { } + var key = $"{c.Hid.VendorID:X4}:{c.Hid.ProductID:X4}:{mfg}:{prod}"; + if (!seen.Add(key)) continue; + + adopted.Add(new QmkRawHidAdoptedDevice + { + VendorId = c.Hid.VendorID, + ProductId = c.Hid.ProductID, + Manufacturer = mfg, + Product = prod, + LedCount = c.LedCount, + Protocol = c.Protocol.ToString(), + ViaKeymapKey = string.Empty, + }); + Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.QmkRawHidRGBDeviceProvider.Instance.AdoptedDevices.Add( + new Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.QmkRawHidAdoptedDeviceFilter( + c.Hid.VendorID, c.Hid.ProductID, mfg, prod)); + } + + cur.deviceQmkRawHidAdoptedDevices = adopted; + cur.deviceQmkRawHidEnabled = true; + AppSettings.SaveSettings(cur); + + RGBController.LoadDeviceProvider( + Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.QmkRawHidRGBDeviceProvider.Instance); + return true; + }); + }, + () => + { + var prov = Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.QmkRawHidRGBDeviceProvider.Instance; + if (prov != null) + { + prov.AdoptedDevices.Clear(); + RGBController.UnloadDeviceProvider(prov); + prov.Dispose(); + } + var cur = AppSettings.GetSettings(); + cur.deviceQmkRawHidEnabled = false; + AppSettings.SaveSettings(cur); + })); } private static Avalonia.Controls.Window GetMainWindow() diff --git a/Chromatics/Views/Dialogs/FirstRunDialog.axaml b/Chromatics/Views/Dialogs/FirstRunDialog.axaml index 9e293a9b..fef3df81 100644 --- a/Chromatics/Views/Dialogs/FirstRunDialog.axaml +++ b/Chromatics/Views/Dialogs/FirstRunDialog.axaml @@ -70,6 +70,10 @@ + diff --git a/Chromatics/Views/Dialogs/FirstRunDialog.axaml.cs b/Chromatics/Views/Dialogs/FirstRunDialog.axaml.cs index 65faf7c1..2aeda397 100644 --- a/Chromatics/Views/Dialogs/FirstRunDialog.axaml.cs +++ b/Chromatics/Views/Dialogs/FirstRunDialog.axaml.cs @@ -60,7 +60,7 @@ private void OnLanguageChanged(object? sender, SelectionChangedEventArgs e) private ToggleButton[] Tiles() => [ TileRazer, TileLogitech, TileCorsair, TileCoolermaster, - TileSteelSeries, TileAsus, TileMsi, TileWooting, TileNovation, TileOpenRgb, TilePlayStation, + TileSteelSeries, TileAsus, TileMsi, TileWooting, TileNovation, TileOpenRgb, TilePlayStation, TileQmkRawHid, ]; private void OnTileChanged(object? sender, RoutedEventArgs e) => UpdateContinueState(); @@ -87,10 +87,13 @@ private void OnContinue(object? sender, RoutedEventArgs e) s.deviceNovationEnabled = TileNovation.IsChecked ?? false; s.deviceOpenRGBEnabled = TileOpenRgb.IsChecked ?? false; s.devicePlayStationEnabled = TilePlayStation.IsChecked ?? false; + s.deviceQmkRawHidEnabled = TileQmkRawHid.IsChecked ?? false; - // Hue is deliberately omitted from the wizard — it needs the - // bridge-pairing dialog which is inappropriate for first-run flow. - // Users enable Hue from Settings → Device Providers when ready. + // Hue and LIFX are deliberately omitted from the wizard — both + // need bridge / network-discovery dialogs that are inappropriate + // for first-run. Users enable them from Settings → Device + // Providers when ready. QMK is fine here: discovery is local + // (USB HID) and auto-adopt happens on first provider load. s.firstrun = false; AppSettings.SaveSettings(s); diff --git a/Chromatics/locale/de.json b/Chromatics/locale/de.json index 12763aee..f72c911d 100644 --- a/Chromatics/locale/de.json +++ b/Chromatics/locale/de.json @@ -837,5 +837,8 @@ "Loading lights from your Hue bridge...": "Lampen von Ihrer Hue Bridge werden geladen...", "No lights found on this bridge.": "Auf dieser Bridge wurden keine Lampen gefunden.", "{0} Hue light(s) on this bridge.": "{0} Hue-Lampe(n) auf dieser Bridge.", - "Couldn't read lights from the bridge: {0}": "Lampen konnten nicht von der Bridge gelesen werden: {0}" + "Couldn't read lights from the bridge: {0}": "Lampen konnten nicht von der Bridge gelesen werden: {0}", + "QMK Keyboards (Beta)": "QMK-Tastaturen (Beta)", + "QMK keyboards over Raw HID. Covers NovelKeys, KBDFans, Drop, GMMK, Glorious and any other custom keyboard running QMK with Raw HID enabled.": "QMK-Tastaturen über Raw HID. Umfasst NovelKeys, KBDFans, Drop, GMMK, Glorious und jede andere benutzerdefinierte Tastatur, auf der QMK mit aktiviertem Raw HID läuft.", + "[BETA] Enable/disable QMK Raw HID keyboard support. Auto-adopts any QMK-compatible keyboard with Raw HID enabled (covers NovelKeys, KBDFans, Drop, GMMK, Glorious, and other custom QMK boards). Default: Disabled": "[BETA] QMK-Raw-HID-Tastaturunterstützung aktivieren/deaktivieren. Übernimmt automatisch jede QMK-kompatible Tastatur mit aktiviertem Raw HID (umfasst NovelKeys, KBDFans, Drop, GMMK, Glorious und andere benutzerdefinierte QMK-Boards). Standard: Deaktiviert" } \ No newline at end of file diff --git a/Chromatics/locale/en.json b/Chromatics/locale/en.json index 2b4de228..a4331101 100644 --- a/Chromatics/locale/en.json +++ b/Chromatics/locale/en.json @@ -619,6 +619,9 @@ "OpenRGB": "OpenRGB", "PlayStation": "PlayStation", "PlayStation (Beta)": "PlayStation (Beta)", + "QMK Keyboards (Beta)": "QMK Keyboards (Beta)", + "QMK keyboards over Raw HID. Covers NovelKeys, KBDFans, Drop, GMMK, Glorious and any other custom keyboard running QMK with Raw HID enabled.": "QMK keyboards over Raw HID. Covers NovelKeys, KBDFans, Drop, GMMK, Glorious and any other custom keyboard running QMK with Raw HID enabled.", + "[BETA] Enable/disable QMK Raw HID keyboard support. Auto-adopts any QMK-compatible keyboard with Raw HID enabled (covers NovelKeys, KBDFans, Drop, GMMK, Glorious, and other custom QMK boards). Default: Disabled": "[BETA] Enable/disable QMK Raw HID keyboard support. Auto-adopts any QMK-compatible keyboard with Raw HID enabled (covers NovelKeys, KBDFans, Drop, GMMK, Glorious, and other custom QMK boards). Default: Disabled", "Enable/disable Razer device library. Default: Enabled": "Enable/disable Razer device library. Default: Enabled", "Enable/disable Logitech device library. Default: Enabled": "Enable/disable Logitech device library. Default: Enabled", "Enable/disable Corsair device library. Default: Enabled": "Enable/disable Corsair device library. Default: Enabled", diff --git a/Chromatics/locale/es.json b/Chromatics/locale/es.json index d59b16fa..dea34b40 100644 --- a/Chromatics/locale/es.json +++ b/Chromatics/locale/es.json @@ -837,5 +837,8 @@ "Loading lights from your Hue bridge...": "Cargando luces desde tu puente Hue...", "No lights found on this bridge.": "No se encontraron luces en este puente.", "{0} Hue light(s) on this bridge.": "{0} luz(es) Hue en este puente.", - "Couldn't read lights from the bridge: {0}": "No se pudieron leer las luces del puente: {0}" + "Couldn't read lights from the bridge: {0}": "No se pudieron leer las luces del puente: {0}", + "QMK Keyboards (Beta)": "Teclados QMK (Beta)", + "QMK keyboards over Raw HID. Covers NovelKeys, KBDFans, Drop, GMMK, Glorious and any other custom keyboard running QMK with Raw HID enabled.": "Teclados QMK mediante Raw HID. Cubre NovelKeys, KBDFans, Drop, GMMK, Glorious y cualquier otro teclado personalizado que ejecute QMK con Raw HID habilitado.", + "[BETA] Enable/disable QMK Raw HID keyboard support. Auto-adopts any QMK-compatible keyboard with Raw HID enabled (covers NovelKeys, KBDFans, Drop, GMMK, Glorious, and other custom QMK boards). Default: Disabled": "[BETA] Activa/desactiva la compatibilidad con teclados QMK Raw HID. Adopta automáticamente cualquier teclado compatible con QMK que tenga Raw HID habilitado (cubre NovelKeys, KBDFans, Drop, GMMK, Glorious y otros teclados QMK personalizados). Valor predeterminado: Desactivado" } \ No newline at end of file diff --git a/Chromatics/locale/fr.json b/Chromatics/locale/fr.json index 85f70725..3e411c18 100644 --- a/Chromatics/locale/fr.json +++ b/Chromatics/locale/fr.json @@ -837,5 +837,8 @@ "Loading lights from your Hue bridge...": "Chargement des lumières depuis votre pont Hue...", "No lights found on this bridge.": "Aucune lumière trouvée sur ce pont.", "{0} Hue light(s) on this bridge.": "{0} lumière(s) Hue sur ce pont.", - "Couldn't read lights from the bridge: {0}": "Impossible de lire les lumières depuis le pont : {0}" + "Couldn't read lights from the bridge: {0}": "Impossible de lire les lumières depuis le pont : {0}", + "QMK Keyboards (Beta)": "Claviers QMK (bêta)", + "QMK keyboards over Raw HID. Covers NovelKeys, KBDFans, Drop, GMMK, Glorious and any other custom keyboard running QMK with Raw HID enabled.": "Claviers QMK via Raw HID. Couvre NovelKeys, KBDFans, Drop, GMMK, Glorious et tout autre clavier personnalisé exécutant QMK avec Raw HID activé.", + "[BETA] Enable/disable QMK Raw HID keyboard support. Auto-adopts any QMK-compatible keyboard with Raw HID enabled (covers NovelKeys, KBDFans, Drop, GMMK, Glorious, and other custom QMK boards). Default: Disabled": "[BÊTA] Activer/désactiver la prise en charge des claviers QMK Raw HID. Adopte automatiquement tout clavier compatible QMK avec Raw HID activé (couvre NovelKeys, KBDFans, Drop, GMMK, Glorious et autres claviers QMK personnalisés). Par défaut : désactivé" } \ No newline at end of file diff --git a/Chromatics/locale/ja.json b/Chromatics/locale/ja.json index da80c308..1a2fef8c 100644 --- a/Chromatics/locale/ja.json +++ b/Chromatics/locale/ja.json @@ -838,5 +838,8 @@ "Loading lights from your Hue bridge...": "Hueブリッジからライトを読み込んでいます...", "No lights found on this bridge.": "このブリッジにはライトが見つかりませんでした。", "{0} Hue light(s) on this bridge.": "このブリッジには{0}個のHueライトがあります。", - "Couldn't read lights from the bridge: {0}": "ブリッジからライトを読み取れませんでした: {0}" + "Couldn't read lights from the bridge: {0}": "ブリッジからライトを読み取れませんでした: {0}", + "QMK Keyboards (Beta)": "QMKキーボード(ベータ)", + "QMK keyboards over Raw HID. Covers NovelKeys, KBDFans, Drop, GMMK, Glorious and any other custom keyboard running QMK with Raw HID enabled.": "Raw HID経由のQMKキーボード。NovelKeys、KBDFans、Drop、GMMK、Glorious、およびRaw HIDが有効なQMKを実行しているその他のカスタムキーボードに対応します。", + "[BETA] Enable/disable QMK Raw HID keyboard support. Auto-adopts any QMK-compatible keyboard with Raw HID enabled (covers NovelKeys, KBDFans, Drop, GMMK, Glorious, and other custom QMK boards). Default: Disabled": "[ベータ]QMK Raw HIDキーボード対応を有効/無効にします。Raw HIDが有効なQMK対応キーボードを自動的に採用します(NovelKeys、KBDFans、Drop、GMMK、Glorious、およびその他のカスタムQMKボードに対応)。デフォルト:無効" } \ No newline at end of file diff --git a/Chromatics/locale/ko.json b/Chromatics/locale/ko.json index be810b05..4d2ee058 100644 --- a/Chromatics/locale/ko.json +++ b/Chromatics/locale/ko.json @@ -838,5 +838,8 @@ "Loading lights from your Hue bridge...": "Hue 브리지에서 조명을 불러오는 중...", "No lights found on this bridge.": "이 브리지에서 조명을 찾을 수 없습니다.", "{0} Hue light(s) on this bridge.": "이 브리지에 Hue 조명 {0}개가 있습니다.", - "Couldn't read lights from the bridge: {0}": "브리지에서 조명을 읽을 수 없습니다: {0}" + "Couldn't read lights from the bridge: {0}": "브리지에서 조명을 읽을 수 없습니다: {0}", + "QMK Keyboards (Beta)": "QMK 키보드(베타)", + "QMK keyboards over Raw HID. Covers NovelKeys, KBDFans, Drop, GMMK, Glorious and any other custom keyboard running QMK with Raw HID enabled.": "Raw HID를 통한 QMK 키보드입니다. NovelKeys, KBDFans, Drop, GMMK, Glorious 및 Raw HID가 활성화된 QMK를 실행하는 기타 모든 커스텀 키보드를 지원합니다.", + "[BETA] Enable/disable QMK Raw HID keyboard support. Auto-adopts any QMK-compatible keyboard with Raw HID enabled (covers NovelKeys, KBDFans, Drop, GMMK, Glorious, and other custom QMK boards). Default: Disabled": "[베타] QMK Raw HID 키보드 지원을 활성화/비활성화합니다. Raw HID가 활성화된 모든 QMK 호환 키보드를 자동으로 채택합니다(NovelKeys, KBDFans, Drop, GMMK, Glorious 및 기타 커스텀 QMK 보드 포함). 기본값: 비활성화" } \ No newline at end of file diff --git a/Chromatics/locale/zh_CN.json b/Chromatics/locale/zh_CN.json index 7d2fd17e..e1fcda77 100644 --- a/Chromatics/locale/zh_CN.json +++ b/Chromatics/locale/zh_CN.json @@ -837,5 +837,8 @@ "Loading lights from your Hue bridge...": "正在从您的 Hue 网桥加载灯光...", "No lights found on this bridge.": "此网桥上未找到灯。", "{0} Hue light(s) on this bridge.": "此网桥上有 {0} 个 Hue 灯。", - "Couldn't read lights from the bridge: {0}": "无法从网桥读取灯光:{0}" + "Couldn't read lights from the bridge: {0}": "无法从网桥读取灯光:{0}", + "QMK Keyboards (Beta)": "QMK 键盘(测试版)", + "QMK keyboards over Raw HID. Covers NovelKeys, KBDFans, Drop, GMMK, Glorious and any other custom keyboard running QMK with Raw HID enabled.": "通过 Raw HID 连接的 QMK 键盘。涵盖 NovelKeys、KBDFans、Drop、GMMK、Glorious,以及任何其他启用了 Raw HID 且运行 QMK 的自定义键盘。", + "[BETA] Enable/disable QMK Raw HID keyboard support. Auto-adopts any QMK-compatible keyboard with Raw HID enabled (covers NovelKeys, KBDFans, Drop, GMMK, Glorious, and other custom QMK boards). Default: Disabled": "[测试版] 启用/禁用 QMK Raw HID 键盘支持。自动采用任何启用了 Raw HID 的 QMK 兼容键盘(涵盖 NovelKeys、KBDFans、Drop、GMMK、Glorious,以及其他自定义 QMK 键盘)。默认:禁用" } \ No newline at end of file From 3402e6efa141ead6c29b791937db15f0a105cfaa Mon Sep 17 00:00:00 2001 From: Danielle Date: Thu, 14 May 2026 20:59:28 +1000 Subject: [PATCH 03/31] Switch QMK keymap fetcher to snakkarike/qmk_firmware + chromatics-docs index (v4.1.41) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original fetcher pointed at www.caniusevia.com URLs that don't exist and assumed a JSON schema (keycap labels embedded as "row,col\n\n\nA" strings) that via-keyboards doesn't actually use. Rewriting against QMK's keyboard.json instead: - Source: snakkarike/qmk_firmware master branch (covers all the NovelKeys boards plus 2500+ other QMK keyboards). NovelKeys boards land under keyboards/novelkeys/ with proper VID/PID and rgb_matrix layouts. - Index: 2650 unique VID/PID entries built once by scripts/build_qmk_keymap_index.py and hosted at raw.githubusercontent.com/logicallysynced/chromatics-docs/main/qmk_keymap_index.json. - Schema: QMK keyboard.json's layouts..layout array. Each entry has matrix=[row,col] and (often, not always) a label="Esc"/"F1"/etc. Label coverage is inconsistent across boards — NK87 has it, NK65 doesn't — so the merge step gracefully falls back to LedId.Custom_* for LEDs whose matrix position has no label in the JSON. Partial semantic mapping is still better than the alternative of no mapping at all. - Cache: moved to FileOperationsHelper.GetConfigDirectory()/QmkKeymaps so it lives alongside the rest of the user's Chromatics state under %AppData%/Chromatics. SettingsViewModel.ResetChromatics now wipes this cache and the in-memory index when the user hits Reset. QmkKeycodeMap expanded to accept both QMK keycode shortforms ("ESC", "BSPC"), the long KC_ prefix form ("KC_ESC"), and the human legend form QMK keyboard.json's label field actually uses ("Esc", "Backspace", "Page Up"). All three resolve to the same LedId.Keyboard_*. Caveats: - Label coverage is partial — boards without labels in their keyboard.json land in Custom_* and the user maps via Mapping tab. - The OpenRGB-QMK command IDs (0x20-0x2A) in OpenRgbQmkProtocol.cs still need verification against the upstream OpenRGB master firmware fork. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 4 +- Chromatics/Chromatics.csproj | 2 +- .../Devices/QmkRawHid/QmkKeycodeMap.cs | 224 ++++++++++---- .../Devices/QmkRawHid/QmkKeymapFetcher.cs | 284 ++++++++++-------- Chromatics/ViewModels/SettingsViewModel.cs | 6 + 5 files changed, 327 insertions(+), 193 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a47b98fa..8f94087f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ All notable changes to Chromatics are documented here. -## 4.1.40 +## 4.1.41 -- **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 on USB and adopts them — no firmware flashing or extra software required. Per-key lighting is driven via the OpenRGB-QMK plugin when the firmware has it installed; otherwise Chromatics drives the firmware's built-in RGB matrix base colour and effect mode (the VIA fallback path). For per-key boards, Chromatics looks up the physical key layout from the via-keyboards database automatically on first connect so the Highlight / Keybind layers line up with the right keys out of the box. +- **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 on USB and adopts them — no firmware flashing or extra software required. Per-key lighting is driven via the OpenRGB-QMK plugin when the firmware has it installed; otherwise Chromatics drives the firmware's built-in RGB matrix base colour and effect mode (VIA). For per-key LED boards, Chromatics looks up the physical key layout from the via-keyboards database automatically on first connect so the Highlight / Keybind layers line up with the right keys out of the box. - Updated dependency libraries to latest version ## 4.1.38 diff --git a/Chromatics/Chromatics.csproj b/Chromatics/Chromatics.csproj index 42f198f9..b60faffe 100644 --- a/Chromatics/Chromatics.csproj +++ b/Chromatics/Chromatics.csproj @@ -4,7 +4,7 @@ WinExe net10.0-windows7.0 Chromatics.Program - 4.1.40.0 + 4.1.41.0 Danielle Thompson app.manifest logicallysynced 2026 diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeycodeMap.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeycodeMap.cs index c80ce84b..f47eaf4c 100644 --- a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeycodeMap.cs +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeycodeMap.cs @@ -4,29 +4,33 @@ namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid { - // QMK keycode string → RGB.NET LedId. Covers the canonical ANSI 104 - // and the common alternates (numpad, media, ISO Enter / split-keys); - // unknown keycodes return LedId.Invalid so the layout merger falls - // back to LedId.Custom1+i for that LED. QMK keycodes follow the - // KC_ convention; the via-keyboards JSON sometimes strips - // the prefix, so the lookup matches both forms. + // QMK label / keycode string → RGB.NET LedId. QMK boards label their keys + // inconsistently across the keyboards/ tree — some entries use the QMK + // keycode short form ("ESC", "BSPC"), some the full prefix ("KC_ESC"), + // and many use human-readable legends ("Esc", "Backspace", "Page Up"). + // This map covers all three styles for the canonical ANSI 104 plus + // numpad and ISO-only keys; lookup is case-insensitive and the "KC_" + // prefix is stripped if present so all conventions resolve. + // + // Unknown labels return LedId.Invalid so the layout merger falls back + // to LedId.Custom1+i for that LED. internal static class QmkKeycodeMap { - public static LedId ToLedId(string keycode) + public static LedId ToLedId(string label) { - if (string.IsNullOrEmpty(keycode)) return LedId.Invalid; - string key = keycode.Trim(); + if (string.IsNullOrEmpty(label)) return LedId.Invalid; + string key = label.Trim(); if (key.StartsWith("KC_")) key = key.Substring(3); return _map.TryGetValue(key, out var id) ? id : LedId.Invalid; } // Stored as a FrozenDictionary because lookups happen during layout // construction (once per device + LED count) and the table never - // changes at runtime. + // changes at runtime. Case-insensitive — covers "Esc"/"ESC"/"esc". private static readonly FrozenDictionary _map = new Dictionary(System.StringComparer.OrdinalIgnoreCase) { - // Letters + // ── Letters ────────────────────────────────────────── ["A"] = LedId.Keyboard_A, ["B"] = LedId.Keyboard_B, ["C"] = LedId.Keyboard_C, ["D"] = LedId.Keyboard_D, ["E"] = LedId.Keyboard_E, ["F"] = LedId.Keyboard_F, ["G"] = LedId.Keyboard_G, ["H"] = LedId.Keyboard_H, ["I"] = LedId.Keyboard_I, @@ -37,67 +41,154 @@ public static LedId ToLedId(string keycode) ["V"] = LedId.Keyboard_V, ["W"] = LedId.Keyboard_W, ["X"] = LedId.Keyboard_X, ["Y"] = LedId.Keyboard_Y, ["Z"] = LedId.Keyboard_Z, - // Top row digits + // ── Top-row digits ─────────────────────────────────── ["1"] = LedId.Keyboard_1, ["2"] = LedId.Keyboard_2, ["3"] = LedId.Keyboard_3, ["4"] = LedId.Keyboard_4, ["5"] = LedId.Keyboard_5, ["6"] = LedId.Keyboard_6, ["7"] = LedId.Keyboard_7, ["8"] = LedId.Keyboard_8, ["9"] = LedId.Keyboard_9, ["0"] = LedId.Keyboard_0, - // Function row + // ── Function row ───────────────────────────────────── ["F1"] = LedId.Keyboard_F1, ["F2"] = LedId.Keyboard_F2, ["F3"] = LedId.Keyboard_F3, ["F4"] = LedId.Keyboard_F4, ["F5"] = LedId.Keyboard_F5, ["F6"] = LedId.Keyboard_F6, ["F7"] = LedId.Keyboard_F7, ["F8"] = LedId.Keyboard_F8, ["F9"] = LedId.Keyboard_F9, ["F10"] = LedId.Keyboard_F10, ["F11"] = LedId.Keyboard_F11, ["F12"] = LedId.Keyboard_F12, + // F13+ exists on the QMK side (full-size + macropad layouts) but + // RGB.NET's LedId enum tops out at F12; entries above that drop + // to LedId.Custom_* via the merge step's fallback. - // Symbols / punctuation (US ANSI) - ["MINS"] = LedId.Keyboard_MinusAndUnderscore, - ["EQL"] = LedId.Keyboard_EqualsAndPlus, - ["LBRC"] = LedId.Keyboard_BracketLeft, - ["RBRC"] = LedId.Keyboard_BracketRight, - ["BSLS"] = LedId.Keyboard_Backslash, - ["SCLN"] = LedId.Keyboard_SemicolonAndColon, - ["QUOT"] = LedId.Keyboard_ApostropheAndDoubleQuote, - ["COMM"] = LedId.Keyboard_CommaAndLessThan, - ["DOT"] = LedId.Keyboard_PeriodAndBiggerThan, - ["SLSH"] = LedId.Keyboard_SlashAndQuestionMark, - ["GRV"] = LedId.Keyboard_GraveAccentAndTilde, + // ── Punctuation: QMK keycode forms ─────────────────── + ["MINS"] = LedId.Keyboard_MinusAndUnderscore, + ["EQL"] = LedId.Keyboard_EqualsAndPlus, + ["LBRC"] = LedId.Keyboard_BracketLeft, + ["RBRC"] = LedId.Keyboard_BracketRight, + ["BSLS"] = LedId.Keyboard_Backslash, + ["SCLN"] = LedId.Keyboard_SemicolonAndColon, + ["QUOT"] = LedId.Keyboard_ApostropheAndDoubleQuote, + ["COMM"] = LedId.Keyboard_CommaAndLessThan, + ["DOT"] = LedId.Keyboard_PeriodAndBiggerThan, + ["SLSH"] = LedId.Keyboard_SlashAndQuestionMark, + ["GRV"] = LedId.Keyboard_GraveAccentAndTilde, - // Modifiers + edit keys - ["ESC"] = LedId.Keyboard_Escape, - ["TAB"] = LedId.Keyboard_Tab, - ["CAPS"] = LedId.Keyboard_CapsLock, - ["LSFT"] = LedId.Keyboard_LeftShift, - ["RSFT"] = LedId.Keyboard_RightShift, - ["LCTL"] = LedId.Keyboard_LeftCtrl, - ["RCTL"] = LedId.Keyboard_RightCtrl, - ["LALT"] = LedId.Keyboard_LeftAlt, - ["RALT"] = LedId.Keyboard_RightAlt, - ["LGUI"] = LedId.Keyboard_LeftGui, - ["RGUI"] = LedId.Keyboard_RightGui, - ["ENT"] = LedId.Keyboard_Enter, - ["BSPC"] = LedId.Keyboard_Backspace, - ["SPC"] = LedId.Keyboard_Space, - ["APP"] = LedId.Keyboard_Application, + // ── Punctuation: human-legend forms ────────────────── + ["-"] = LedId.Keyboard_MinusAndUnderscore, + ["="] = LedId.Keyboard_EqualsAndPlus, + ["["] = LedId.Keyboard_BracketLeft, + ["]"] = LedId.Keyboard_BracketRight, + ["\\"] = LedId.Keyboard_Backslash, + [";"] = LedId.Keyboard_SemicolonAndColon, + ["'"] = LedId.Keyboard_ApostropheAndDoubleQuote, + [","] = LedId.Keyboard_CommaAndLessThan, + ["."] = LedId.Keyboard_PeriodAndBiggerThan, + ["/"] = LedId.Keyboard_SlashAndQuestionMark, + ["`"] = LedId.Keyboard_GraveAccentAndTilde, - // Navigation cluster - ["INS"] = LedId.Keyboard_Insert, - ["DEL"] = LedId.Keyboard_Delete, - ["HOME"] = LedId.Keyboard_Home, - ["END"] = LedId.Keyboard_End, - ["PGUP"] = LedId.Keyboard_PageUp, - ["PGDN"] = LedId.Keyboard_PageDown, - ["UP"] = LedId.Keyboard_ArrowUp, - ["DOWN"] = LedId.Keyboard_ArrowDown, - ["LEFT"] = LedId.Keyboard_ArrowLeft, - ["RGHT"] = LedId.Keyboard_ArrowRight, + // ── Modifiers + edit: short + long forms ───────────── + ["ESC"] = LedId.Keyboard_Escape, + ["Escape"] = LedId.Keyboard_Escape, + ["TAB"] = LedId.Keyboard_Tab, + ["Tab"] = LedId.Keyboard_Tab, + ["CAPS"] = LedId.Keyboard_CapsLock, + ["Caps"] = LedId.Keyboard_CapsLock, + ["Caps Lock"] = LedId.Keyboard_CapsLock, + ["CapsLock"] = LedId.Keyboard_CapsLock, + ["LSFT"] = LedId.Keyboard_LeftShift, + ["LShift"] = LedId.Keyboard_LeftShift, + ["Left Shift"] = LedId.Keyboard_LeftShift, + ["Shift"] = LedId.Keyboard_LeftShift, // ambiguous label; first occurrence wins, second falls back to Custom_* + ["RSFT"] = LedId.Keyboard_RightShift, + ["RShift"] = LedId.Keyboard_RightShift, + ["Right Shift"] = LedId.Keyboard_RightShift, + ["LCTL"] = LedId.Keyboard_LeftCtrl, + ["LCtrl"] = LedId.Keyboard_LeftCtrl, + ["Left Ctrl"] = LedId.Keyboard_LeftCtrl, + ["Ctrl"] = LedId.Keyboard_LeftCtrl, + ["Control"] = LedId.Keyboard_LeftCtrl, + ["RCTL"] = LedId.Keyboard_RightCtrl, + ["RCtrl"] = LedId.Keyboard_RightCtrl, + ["Right Ctrl"] = LedId.Keyboard_RightCtrl, + ["LALT"] = LedId.Keyboard_LeftAlt, + ["LAlt"] = LedId.Keyboard_LeftAlt, + ["Left Alt"] = LedId.Keyboard_LeftAlt, + ["Alt"] = LedId.Keyboard_LeftAlt, + ["RALT"] = LedId.Keyboard_RightAlt, + ["RAlt"] = LedId.Keyboard_RightAlt, + ["Right Alt"] = LedId.Keyboard_RightAlt, + ["AltGr"] = LedId.Keyboard_RightAlt, + ["LGUI"] = LedId.Keyboard_LeftGui, + ["LGui"] = LedId.Keyboard_LeftGui, + ["Left GUI"] = LedId.Keyboard_LeftGui, + ["Left Win"] = LedId.Keyboard_LeftGui, + ["Win"] = LedId.Keyboard_LeftGui, + ["Windows"] = LedId.Keyboard_LeftGui, + ["Cmd"] = LedId.Keyboard_LeftGui, + ["RGUI"] = LedId.Keyboard_RightGui, + ["RGui"] = LedId.Keyboard_RightGui, + ["Right GUI"] = LedId.Keyboard_RightGui, + ["Right Win"] = LedId.Keyboard_RightGui, + ["ENT"] = LedId.Keyboard_Enter, + ["ENTER"] = LedId.Keyboard_Enter, + ["Enter"] = LedId.Keyboard_Enter, + ["Return"] = LedId.Keyboard_Enter, + ["BSPC"] = LedId.Keyboard_Backspace, + ["Backspace"] = LedId.Keyboard_Backspace, + ["BkSp"] = LedId.Keyboard_Backspace, + ["SPC"] = LedId.Keyboard_Space, + ["Space"] = LedId.Keyboard_Space, + ["APP"] = LedId.Keyboard_Application, + ["Menu"] = LedId.Keyboard_Application, + ["App"] = LedId.Keyboard_Application, - // System / lock keys - ["PSCR"] = LedId.Keyboard_PrintScreen, - ["SCRL"] = LedId.Keyboard_ScrollLock, - ["PAUS"] = LedId.Keyboard_PauseBreak, - ["NLCK"] = LedId.Keyboard_NumLock, + // ── Navigation cluster ─────────────────────────────── + ["INS"] = LedId.Keyboard_Insert, + ["Ins"] = LedId.Keyboard_Insert, + ["Insert"] = LedId.Keyboard_Insert, + ["DEL"] = LedId.Keyboard_Delete, + ["Del"] = LedId.Keyboard_Delete, + ["Delete"] = LedId.Keyboard_Delete, + ["HOME"] = LedId.Keyboard_Home, + ["Home"] = LedId.Keyboard_Home, + ["END"] = LedId.Keyboard_End, + ["End"] = LedId.Keyboard_End, + ["PGUP"] = LedId.Keyboard_PageUp, + ["PgUp"] = LedId.Keyboard_PageUp, + ["Page Up"] = LedId.Keyboard_PageUp, + ["PageUp"] = LedId.Keyboard_PageUp, + ["PGDN"] = LedId.Keyboard_PageDown, + ["PgDn"] = LedId.Keyboard_PageDown, + ["Page Down"] = LedId.Keyboard_PageDown, + ["PageDown"] = LedId.Keyboard_PageDown, + ["UP"] = LedId.Keyboard_ArrowUp, + ["Up"] = LedId.Keyboard_ArrowUp, + ["↑"] = LedId.Keyboard_ArrowUp, + ["DOWN"] = LedId.Keyboard_ArrowDown, + ["Down"] = LedId.Keyboard_ArrowDown, + ["↓"] = LedId.Keyboard_ArrowDown, + ["LEFT"] = LedId.Keyboard_ArrowLeft, + ["Left"] = LedId.Keyboard_ArrowLeft, + ["←"] = LedId.Keyboard_ArrowLeft, + ["RGHT"] = LedId.Keyboard_ArrowRight, + ["Right"] = LedId.Keyboard_ArrowRight, + ["→"] = LedId.Keyboard_ArrowRight, - // Numpad + // ── System / locks ─────────────────────────────────── + ["PSCR"] = LedId.Keyboard_PrintScreen, + ["PrtSc"] = LedId.Keyboard_PrintScreen, + ["PrintScreen"] = LedId.Keyboard_PrintScreen, + ["Print Screen"] = LedId.Keyboard_PrintScreen, + ["SCRL"] = LedId.Keyboard_ScrollLock, + ["ScrLk"] = LedId.Keyboard_ScrollLock, + ["Scroll Lock"] = LedId.Keyboard_ScrollLock, + ["ScrollLock"] = LedId.Keyboard_ScrollLock, + ["PAUS"] = LedId.Keyboard_PauseBreak, + ["Pause"] = LedId.Keyboard_PauseBreak, + ["Break"] = LedId.Keyboard_PauseBreak, + ["Pause/Break"] = LedId.Keyboard_PauseBreak, + ["NLCK"] = LedId.Keyboard_NumLock, + ["NumLk"] = LedId.Keyboard_NumLock, + ["Num Lock"] = LedId.Keyboard_NumLock, + ["NumLock"] = LedId.Keyboard_NumLock, + + // ── Numpad: QMK keycode forms (P0..P9, PDOT etc.) ──── ["P0"] = LedId.Keyboard_Num0, ["P1"] = LedId.Keyboard_Num1, ["P2"] = LedId.Keyboard_Num2, ["P3"] = LedId.Keyboard_Num3, ["P4"] = LedId.Keyboard_Num4, ["P5"] = LedId.Keyboard_Num5, @@ -110,10 +201,23 @@ public static LedId ToLedId(string keycode) ["PPLS"] = LedId.Keyboard_NumPlus, ["PENT"] = LedId.Keyboard_NumEnter, - // Some firmwares emit ISO-only keys; map to closest ANSI siblings. + // ── Numpad: human-legend forms (KP_0..KP_9, KP_Plus, etc.) ── + ["KP_0"] = LedId.Keyboard_Num0, ["KP_1"] = LedId.Keyboard_Num1, + ["KP_2"] = LedId.Keyboard_Num2, ["KP_3"] = LedId.Keyboard_Num3, + ["KP_4"] = LedId.Keyboard_Num4, ["KP_5"] = LedId.Keyboard_Num5, + ["KP_6"] = LedId.Keyboard_Num6, ["KP_7"] = LedId.Keyboard_Num7, + ["KP_8"] = LedId.Keyboard_Num8, ["KP_9"] = LedId.Keyboard_Num9, + ["KP_."] = LedId.Keyboard_NumPeriodAndDelete, + ["KP_/"] = LedId.Keyboard_NumSlash, + ["KP_*"] = LedId.Keyboard_NumAsterisk, + ["KP_-"] = LedId.Keyboard_NumMinus, + ["KP_+"] = LedId.Keyboard_NumPlus, + ["KP_Enter"] = LedId.Keyboard_NumEnter, + + // ── ISO-only keys: map to closest ANSI siblings ────── // NUHS = ISO non-US hash (between Enter and ' on ISO layouts). ["NUHS"] = LedId.Keyboard_Backslash, - // NUBS = ISO non-US backslash (next to Left Shift on ISO layouts). + // NUBS = ISO non-US backslash (next to Left Shift on ISO). ["NUBS"] = LedId.Keyboard_NonUsBackslash, }.ToFrozenDictionary(System.StringComparer.OrdinalIgnoreCase); } diff --git a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeymapFetcher.cs b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeymapFetcher.cs index 63b55b3c..0e9bd444 100644 --- a/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeymapFetcher.cs +++ b/Chromatics/Extensions/RGB.NET/Devices/QmkRawHid/QmkKeymapFetcher.cs @@ -1,8 +1,8 @@ using Chromatics.Core; using Chromatics.Enums; +using Chromatics.Helpers; using RGB.NET.Core; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,28 +12,33 @@ namespace Chromatics.Extensions.RGB.NET.Devices.QmkRawHid { - // Option C from the design discussion: fetch VIA keymap JSON from the - // upstream via-keyboards repo (or a CDN-mirrored equivalent) for any - // adopted board, then merge against the firmware's per-LED matrix - // coordinates to produce semantic LedId.Keyboard_* mappings. Falls - // back to LedId.Custom1..N when no keymap is fetchable (offline first - // run, unknown board, schema mismatch). + // Option C from the design discussion: resolve a per-board layout for any + // QMK keyboard the user has adopted. Pulls two artifacts: // - // Cache lives at %APPDATA%/Chromatics/QmkKeymaps/{vid:X4}_{pid:X4}.json - // and is consulted before any network request. Cache hits are - // unconditional — keymaps are board-defining and don't expire; if a - // user re-flashes with a different layout they can clear the cache - // dir manually. The index itself is refetched every 14 days so new - // boards added upstream show up without a Chromatics release. + // 1) A flat VID/PID → keyboard.json path index hosted on chromatics-docs. + // Built by scripts/build_qmk_keymap_index.py from snakkarike/qmk_firmware + // and refreshed independently of a Chromatics release. + // + // 2) The board's own keyboard.json (or older info.json) fetched on demand + // from snakkarike/qmk_firmware via the GitHub raw CDN. We parse its + // layouts..layout array, building a (matrix_row, matrix_col) → + // keycap-label dictionary. The label is fed to QmkKeycodeMap to derive + // the semantic LedId.Keyboard_* for each per-key LED. + // + // Coverage is mixed by design — QMK's schema is inconsistent across boards. + // Some keyboard.json files include "label" fields on every layout entry + // (NK87 etc. — full semantic mapping); some omit them entirely (NK65 — only + // matrix coords). When a label isn't available the merge step falls back to + // LedId.Custom_* for that LED, and the user positions it via the Mapping + // tab. Better partial than nothing. + // + // Cache lives under FileOperationsHelper.GetConfigDirectory()/QmkKeymaps + // (i.e. %AppData%/Chromatics/QmkKeymaps) so it survives Velopack updates + // and gets wiped by Settings → Reset (see SettingsViewModel.ResetChromatics). internal static class QmkKeymapFetcher { - // The via-keyboards index URL. Each keyboard entry maps a kebab-name - // path to vendor/product ids; the keymap JSONs themselves live one - // path-level deeper. URL is centralised so a future change of upstream - // host (or use of OpenSignalRGB's database in parallel) only touches - // this one constant. - private const string IndexUrl = "https://www.caniusevia.com/keyboards.json"; - private const string KeymapBase = "https://www.caniusevia.com/keyboards/"; + private const string IndexUrl = "https://raw.githubusercontent.com/logicallysynced/chromatics-docs/main/qmk_keymap_index.json"; + private const string KeymapRawBase = "https://raw.githubusercontent.com/snakkarike/qmk_firmware/master/"; private const int RequestTimeoutSeconds = 8; private static readonly TimeSpan IndexCacheTtl = TimeSpan.FromDays(14); @@ -43,40 +48,33 @@ internal static class QmkKeymapFetcher Timeout = TimeSpan.FromSeconds(RequestTimeoutSeconds), }; - // Loaded index: keyed by (vendorId, productId). Populated lazily on - // first call. ConcurrentDictionary so a multi-board adoption flow - // doesn't serialise behind one another's lookups. - private static ConcurrentDictionary<(int vid, int pid), string> _indexByVidPid; + // Loaded index: VID:PID key → repo-relative path. Populated lazily. + private static Dictionary _indexByVidPid; private static readonly object _indexInitLock = new(); public sealed class QmkKeymap { - public string Name { get; set; } - public int Cols { get; set; } - public int Rows { get; set; } - // Keycode at each matrix coordinate. Key = (col, row). Value is - // the QMK keycode string (e.g. "KC_A", "KC_F1", "KC_NO"). - // Underglow / non-matrix LEDs are absent from this dictionary; - // they get LedId.Custom1+i fallback ids in the merge step. - public IReadOnlyDictionary<(byte col, byte row), string> Keycodes { get; set; } + // Keycap label at each keyswitch matrix coordinate. Only entries + // whose source JSON included a "label" field appear here — boards + // without labels return an empty dictionary and the merge step + // falls back to LedId.Custom_* for every LED. + public IReadOnlyDictionary<(byte col, byte row), string> Labels { get; set; } } - // Returns the cached/fetched keymap, or null on failure. Never - // throws — the caller treats null as "fall back to Custom1..N". public static async Task TryGetKeymapAsync(int vendorId, int productId) { try { string diskPath = ResolveCachePath(vendorId, productId); - if (TryLoadCached(diskPath, out QmkKeymap cached)) return cached; + if (TryLoadCachedKeymap(diskPath, out var cached)) return cached; if (!TryEnsureIndex()) return null; - if (!_indexByVidPid.TryGetValue((vendorId, productId), out string keymapPath)) - return null; + string key = $"{vendorId:X4}:{productId:X4}"; + if (!_indexByVidPid.TryGetValue(key, out string repoPath)) return null; - string url = KeymapBase + keymapPath; + string url = KeymapRawBase + UrlPathEncode(repoPath); string body = await _http.GetStringAsync(url).ConfigureAwait(false); - QmkKeymap parsed = ParseKeymapJson(body); + var parsed = ParseKeyboardJson(body); if (parsed == null) return null; TrySaveCache(diskPath, body); @@ -85,7 +83,7 @@ public static async Task TryGetKeymapAsync(int vendorId, int productI catch (Exception ex) { Logger.WriteConsole(LoggerTypes.Devices, - $"[QMK] VIA keymap fetch failed for {vendorId:X4}:{productId:X4} ({ex.Message}); falling back to Custom1..N.", + $"[QMK] keymap fetch failed for {vendorId:X4}:{productId:X4} ({ex.Message}); falling back to Custom1..N.", forwardToSentry: false); return null; } @@ -104,7 +102,7 @@ private static bool TryEnsureIndex() } } - private static ConcurrentDictionary<(int vid, int pid), string> LoadIndex() + private static Dictionary LoadIndex() { string indexCache = Path.Combine(GetCacheDir(), "_index.json"); string body = null; @@ -135,109 +133,126 @@ private static bool TryEnsureIndex() return ParseIndex(body); } - // Index format (caniusevia.com): { "": { "vendorId": "0xABCD", - // "productId": "0x1234", "name": "...", "definition_version": "...", ... } } - // We only need vendorId / productId / kebab-name. vendorId/productId are - // strings with "0x" prefix. - private static ConcurrentDictionary<(int vid, int pid), string> ParseIndex(string json) + // Index format: { "_source": ..., "_generated_utc": ..., "_count": N, + // "index": { "ABCD:1234": "keyboards///keyboard.json", ... } } + private static Dictionary ParseIndex(string json) { - var dict = new ConcurrentDictionary<(int vid, int pid), string>(); + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); try { using var doc = JsonDocument.Parse(json); - foreach (var kvp in doc.RootElement.EnumerateObject()) + if (doc.RootElement.TryGetProperty("index", out var inner)) { - string path = kvp.Name; - if (!kvp.Value.TryGetProperty("vendorId", out var vidProp)) continue; - if (!kvp.Value.TryGetProperty("productId", out var pidProp)) continue; - if (!TryParseHexId(vidProp.GetString(), out int vid)) continue; - if (!TryParseHexId(pidProp.GetString(), out int pid)) continue; - dict[(vid, pid)] = path + ".json"; + foreach (var kvp in inner.EnumerateObject()) + { + string path = kvp.Value.GetString(); + if (!string.IsNullOrEmpty(path)) + dict[kvp.Name] = path; + } } } - catch { /* malformed index — return whatever parsed */ } + catch { /* malformed — return whatever parsed */ } return dict; } - private static bool TryParseHexId(string s, out int value) - { - value = 0; - if (string.IsNullOrEmpty(s)) return false; - string t = s.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? s.Substring(2) : s; - return int.TryParse(t, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out value); - } - - // ── Keymap parse ───────────────────────────────────────────── - - // VIA keymap JSON layout fields used: - // "matrix": { "rows": N, "cols": N } - // "layouts": { "keymap": [ row, row, ... ] } where each row entry is - // either a "control object" { "x":..., "y":..., "w":... } or a - // string label like "0,5\n\n\nA" — "row,col" prefix tells us - // where in the firmware matrix this keycap sits. + // ── Keyboard.json parse ────────────────────────────────────── + + // Schema we care about (everything else ignored): + // "layouts": { + // "LAYOUT_": { + // "layout": [ + // { "matrix": [row, col], "label": "Esc", "x": 0, "y": 0 }, + // ... + // ] + // }, + // ... + // } // - // We walk the keymap array, tracking explicit "matrix" prefixes; the - // resulting dictionary lets the layout merger answer - // "what keycap is at (col, row)?" cheaply. - private static QmkKeymap ParseKeymapJson(string json) + // QMK boards frequently expose multiple LAYOUT_* alternates (ANSI vs ISO + // vs split-spacebar) — we pick the first that yielded any labels. The + // alternates usually share matrix coords for the keys they overlap on, + // so the choice is mostly cosmetic for our purposes. + // + // Label coverage varies wildly across boards. NK87's entries have + // "label": "Esc"; NK65's same field is absent. We emit only what's + // actually there; the merge step in BuildLayout falls back to + // LedId.Custom_* for LEDs whose (col,row) has no label. + private static QmkKeymap ParseKeyboardJson(string json) { try { using var doc = JsonDocument.Parse(json); var root = doc.RootElement; - int cols = 0, rows = 0; - if (root.TryGetProperty("matrix", out var matrix)) - { - if (matrix.TryGetProperty("cols", out var c)) cols = c.GetInt32(); - if (matrix.TryGetProperty("rows", out var r)) rows = r.GetInt32(); - } - - var keycodes = new Dictionary<(byte col, byte row), string>(); - if (root.TryGetProperty("layouts", out var layouts) - && layouts.TryGetProperty("keymap", out var keymap)) + var labels = new Dictionary<(byte col, byte row), string>(); + if (root.TryGetProperty("layouts", out var layouts) && layouts.ValueKind == JsonValueKind.Object) { - foreach (var rowElem in keymap.EnumerateArray()) + foreach (var layoutProp in layouts.EnumerateObject()) { - if (rowElem.ValueKind != JsonValueKind.Array) continue; - foreach (var cellElem in rowElem.EnumerateArray()) + if (!layoutProp.Value.TryGetProperty("layout", out var layoutArr)) continue; + if (layoutArr.ValueKind != JsonValueKind.Array) continue; + if (layoutArr.GetArrayLength() == 0) continue; + + foreach (var entry in layoutArr.EnumerateArray()) { - if (cellElem.ValueKind != JsonValueKind.String) continue; - string label = cellElem.GetString(); - if (string.IsNullOrEmpty(label)) continue; - - // Label shape: "row,col" then newline-separated labels - // (legend on each keycap face). We only need the - // matrix coordinate prefix. - int comma = label.IndexOf(','); - int nl = label.IndexOf('\n'); - if (comma <= 0 || nl <= comma) continue; - - if (!byte.TryParse(label.Substring(0, comma), out byte row)) continue; - if (!byte.TryParse(label.Substring(comma + 1, nl - comma - 1), out byte col)) continue; - - string keycodeLabel = label.Substring(nl + 1).Trim(); - keycodes[(col, row)] = keycodeLabel; + if (entry.ValueKind != JsonValueKind.Object) continue; + if (!entry.TryGetProperty("matrix", out var mat)) continue; + if (mat.ValueKind != JsonValueKind.Array || mat.GetArrayLength() < 2) continue; + if (!entry.TryGetProperty("label", out var labelProp)) continue; + if (labelProp.ValueKind != JsonValueKind.String) continue; + + int row = mat[0].GetInt32(); + int col = mat[1].GetInt32(); + if (row < 0 || row > byte.MaxValue || col < 0 || col > byte.MaxValue) continue; + + string label = labelProp.GetString(); + if (string.IsNullOrWhiteSpace(label)) continue; + + // First-write-wins across alternate layouts: a key + // that appears in LAYOUT_60_ansi and LAYOUT_60_iso + // at the same matrix coord keeps the ANSI label, + // which is the conventional preferred default. + var key = ((byte)col, (byte)row); + if (!labels.ContainsKey(key)) + labels[key] = label; } + + // First layout with content satisfies us — alternates + // mostly share matrix coords, no reason to keep parsing. + if (labels.Count > 0) break; } } - return new QmkKeymap - { - Cols = cols, - Rows = rows, - Keycodes = keycodes, - }; + return new QmkKeymap { Labels = labels }; } catch { return null; } } // ── Cache ──────────────────────────────────────────────────── + // Public so SettingsViewModel.ResetChromatics can wipe the cache when + // the user resets the app. Recursive delete; missing dir is a no-op. + public static void ClearCache() + { + try + { + var dir = GetCacheDir(); + if (Directory.Exists(dir)) + Directory.Delete(dir, recursive: true); + } + catch (Exception ex) + { + Logger.WriteConsole(LoggerTypes.Devices, + $"[QMK] keymap cache clear failed: {ex.Message}", + forwardToSentry: false); + } + + lock (_indexInitLock) { _indexByVidPid = null; } + } + private static string GetCacheDir() { - string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - string dir = Path.Combine(appData, "Chromatics", "QmkKeymaps"); + string dir = Path.Combine(FileOperationsHelper.GetConfigDirectory(), "QmkKeymaps"); try { Directory.CreateDirectory(dir); } catch { /* best-effort */ } return dir; } @@ -245,14 +260,14 @@ private static string GetCacheDir() private static string ResolveCachePath(int vid, int pid) => Path.Combine(GetCacheDir(), $"{vid:X4}_{pid:X4}.json"); - private static bool TryLoadCached(string path, out QmkKeymap keymap) + private static bool TryLoadCachedKeymap(string path, out QmkKeymap keymap) { keymap = null; try { if (!File.Exists(path)) return false; string body = File.ReadAllText(path); - keymap = ParseKeymapJson(body); + keymap = ParseKeyboardJson(body); return keymap != null; } catch { return false; } @@ -269,17 +284,25 @@ private static void TrySaveCache(string path, string body) } } - // ── Layout merge: keymap + LED matrix → ordered LedLayoutEntry list ── + // Path-segment encode (preserve slashes) for paths containing spaces or + // other characters QMK keymap filenames occasionally use. + private static string UrlPathEncode(string path) + { + return string.Join("/", path.Split('/').Select(Uri.EscapeDataString)); + } + + // ── Layout merge: firmware LED records + keymap labels → LedId list ── - // Combines the VIA keymap (col,row → keycode label) with the firmware's - // GetLedInfo per-LED records (firmwareIndex → col,row) to produce a - // list ordered by firmwareIndex where each LED has a semantic - // LedId.Keyboard_* whenever the keycode label is mappable, or - // LedId.Custom1+i when it isn't. + // Combines the firmware's GetLedInfo records (per-LED matrix col/row) + // with the keymap's labels (per matrix coord → "Esc" / "F1" / etc.) to + // assign a semantic LedId.Keyboard_* to each LED. LEDs whose + // (col, row) doesn't appear in the keymap (underglow, board variants, + // or boards whose keyboard.json omits labels entirely) fall back to + // LedId.Custom1+firmwareIndex. // - // Physical layout (Point/Size) is approximated from the matrix - // coordinates so the Avalonia preview shows a recognisable grid - // shape out of the box; users can fine-tune via the Mapping tab. + // Physical placement is derived from matrix coords × a fixed cell + // size — good enough for the Mapping tab preview; users adjust via + // drag-position UX from there. public static IReadOnlyList BuildLayout( QmkKeymap keymap, IReadOnlyList<(int firmwareIndex, byte col, byte row)> ledRecords) @@ -293,10 +316,10 @@ public static IReadOnlyList BuildLayout( var rec = ledRecords[i]; LedId ledId = LedId.Invalid; - if (keymap?.Keycodes != null - && keymap.Keycodes.TryGetValue((rec.col, rec.row), out string keycode)) + if (keymap?.Labels != null + && keymap.Labels.TryGetValue((rec.col, rec.row), out string label)) { - ledId = QmkKeycodeMap.ToLedId(keycode); + ledId = QmkKeycodeMap.ToLedId(label); } if (ledId == LedId.Invalid) @@ -307,14 +330,15 @@ public static IReadOnlyList BuildLayout( entries.Add(new QmkLedLayoutEntry(rec.firmwareIndex, rec.col, rec.row, ledId, location, size)); } - // De-dupe LedIds (a keymap might claim two LEDs share a semantic - // id — e.g. left+right shift sometimes both map to KC_LSFT). - // RGB.NET requires unique ids per device, so the second - // collision falls back to Custom1+i. + // De-dupe LedIds: two LEDs claiming the same semantic id (left vs + // right Shift both label "Shift", for instance) keep the first + // and demote the rest to Custom_* so RGB.NET's uniqueness + // invariant holds. var seen = new HashSet(); for (int i = 0; i < entries.Count; i++) { if (entries[i].PreferredLedId == LedId.Invalid) continue; + if ((int)entries[i].PreferredLedId >= (int)LedId.Custom1) { seen.Add(entries[i].PreferredLedId); continue; } if (seen.Add(entries[i].PreferredLedId)) continue; entries[i] = new QmkLedLayoutEntry( entries[i].FirmwareIndex, entries[i].MatrixCol, entries[i].MatrixRow, diff --git a/Chromatics/ViewModels/SettingsViewModel.cs b/Chromatics/ViewModels/SettingsViewModel.cs index 8e4a3c87..c24de466 100644 --- a/Chromatics/ViewModels/SettingsViewModel.cs +++ b/Chromatics/ViewModels/SettingsViewModel.cs @@ -609,6 +609,12 @@ public void ResetChromatics() var path = Path.Combine(env, f); if (File.Exists(path)) FileSystem.DeleteFile(path); } + + // Provider-specific caches live alongside the settings files + // and want clearing too. QMK Raw HID caches per-board VIA + // keymap JSONs + the upstream VID/PID index under + // QmkKeymaps/; the fetcher's static state is also reset. + Chromatics.Extensions.RGB.NET.Devices.QmkRawHid.QmkKeymapFetcher.ClearCache(); } catch (Exception ex) { From 0e2ae4a4e0ce07d8a2f0d64efeaa4b43dd64f52e Mon Sep 17 00:00:00 2001 From: Danielle Date: Thu, 14 May 2026 21:01:18 +1000 Subject: [PATCH 04/31] =?UTF-8?q?Untrack=20obj/=20=E2=80=94=20stop=20commi?= =?UTF-8?q?tting=20build=20intermediates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 56 stale files under Chromatics/obj/ have been tracked since the .NET 5 / .NET 6 era. They're regenerated on every build, contain machine-absolute paths inside Chromatics.csproj.nuget.dgspec.json (useless on a fresh clone), and are already covered by .gitignore's */obj entry — the only reason they kept showing up in commits is that they were committed before the gitignore landed and git keeps tracking files it has already seen. git rm --cached -r — files stay on disk locally, just stop being tracked. The current net10 build writes to obj/Debug/net10.0-windows7.0/ which the .gitignore correctly excludes; the leftover net5.0-windows / net6.0-windows / Release intermediates here aren't referenced by anything the current solution builds. No code-behaviour impact, just diff hygiene. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../obj/Chromatics.csproj.nuget.dgspec.json | 486 -- .../obj/Chromatics.csproj.nuget.g.props | 27 - .../obj/Chromatics.csproj.nuget.g.targets | 12 - ...CoreApp,Version=v5.0.AssemblyAttributes.cs | 4 - .../net5.0-windows/Chromatics.AssemblyInfo.cs | 27 - .../Chromatics.AssemblyInfoInputs.cache | 1 - .../Chromatics.Chromatics.resources | Bin 180 -> 0 bytes .../Chromatics.Forms.Fm_MainWindow.resources | Bin 180 -> 0 bytes ...Chromatics.Forms.Pn_LayerDisplay.resources | Bin 180 -> 0 bytes .../Chromatics.Forms.Uc_Console.resources | Bin 180 -> 0 bytes .../Chromatics.Forms.Uc_Mappings.resources | Bin 180 -> 0 bytes ...omatics.Forms.Uc_VirtualKeyboard.resources | Bin 180 -> 0 bytes ....GeneratedMSBuildEditorConfig.editorconfig | 16 - .../Chromatics.Properties.Resources.resources | Bin 2176 -> 0 bytes .../net5.0-windows/Chromatics.assets.cache | Bin 11997 -> 0 bytes .../Chromatics.csproj.AssemblyReference.cache | Bin 118799 -> 0 bytes .../Chromatics.csproj.CopyComplete | 0 .../Chromatics.csproj.CoreCompileInputs.cache | 1 - .../Chromatics.csproj.FileListAbsolute.txt | 48 - .../Chromatics.csproj.GenerateResource.cache | Bin 598 -> 0 bytes .../Chromatics.designer.deps.json | 456 -- .../Chromatics.designer.runtimeconfig.json | 18 - .../obj/Debug/net5.0-windows/Chromatics.dll | Bin 116736 -> 0 bytes .../Chromatics.genruntimeconfig.cache | 1 - .../obj/Debug/net5.0-windows/Chromatics.pdb | Bin 38156 -> 0 bytes .../obj/Debug/net5.0-windows/apphost.exe | Bin 129024 -> 0 bytes .../Debug/net5.0-windows/ref/Chromatics.dll | Bin 25088 -> 0 bytes ...CoreApp,Version=v6.0.AssemblyAttributes.cs | 4 - .../net6.0-windows/Chromatics.assets.cache | Bin 29009 -> 0 bytes .../Chromatics.csproj.FileListAbsolute.txt | 195 - .../Chromatics.designer.deps.json | 654 --- .../Chromatics.designer.runtimeconfig.json | 23 - .../obj/Debug/net6.0-windows/apphost.exe | Bin 250880 -> 0 bytes ...CoreApp,Version=v5.0.AssemblyAttributes.cs | 4 - .../net5.0-windows/Chromatics.AssemblyInfo.cs | 25 - .../Chromatics.AssemblyInfoInputs.cache | 1 - .../Chromatics.Chromatics.resources | Bin 180 -> 0 bytes .../Chromatics.Forms.Fm_MainWindow.resources | Bin 180 -> 0 bytes ...Chromatics.Forms.Pn_LayerDisplay.resources | Bin 180 -> 0 bytes .../Chromatics.Forms.Uc_Console.resources | Bin 180 -> 0 bytes .../Chromatics.Forms.Uc_Mappings.resources | Bin 180 -> 0 bytes ....GeneratedMSBuildEditorConfig.editorconfig | 16 - .../net5.0-windows/Chromatics.assets.cache | Bin 11638 -> 0 bytes .../Chromatics.csproj.AssemblyReference.cache | Bin 6 -> 0 bytes .../Chromatics.csproj.CopyComplete | 0 .../Chromatics.csproj.CoreCompileInputs.cache | 1 - .../Chromatics.csproj.FileListAbsolute.txt | 44 - .../Chromatics.csproj.GenerateResource.cache | Bin 338 -> 0 bytes .../Chromatics.designer.deps.json | 452 -- .../Chromatics.designer.runtimeconfig.json | 19 - .../obj/Release/net5.0-windows/Chromatics.dll | Bin 47616 -> 0 bytes .../Chromatics.genruntimeconfig.cache | 1 - .../obj/Release/net5.0-windows/Chromatics.pdb | Bin 25732 -> 0 bytes .../obj/Release/net5.0-windows/apphost.exe | Bin 125952 -> 0 bytes .../Release/net5.0-windows/ref/Chromatics.dll | Bin 15872 -> 0 bytes Chromatics/obj/project.assets.json | 4177 ----------------- 56 files changed, 6713 deletions(-) delete mode 100644 Chromatics/obj/Chromatics.csproj.nuget.dgspec.json delete mode 100644 Chromatics/obj/Chromatics.csproj.nuget.g.props delete mode 100644 Chromatics/obj/Chromatics.csproj.nuget.g.targets delete mode 100644 Chromatics/obj/Debug/net5.0-windows/.NETCoreApp,Version=v5.0.AssemblyAttributes.cs delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.AssemblyInfo.cs delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.AssemblyInfoInputs.cache delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.Chromatics.resources delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Fm_MainWindow.resources delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Pn_LayerDisplay.resources delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Uc_Console.resources delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Uc_Mappings.resources delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Uc_VirtualKeyboard.resources delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.GeneratedMSBuildEditorConfig.editorconfig delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.Properties.Resources.resources delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.assets.cache delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.AssemblyReference.cache delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.CopyComplete delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.CoreCompileInputs.cache delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.FileListAbsolute.txt delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.GenerateResource.cache delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.designer.deps.json delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.designer.runtimeconfig.json delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.dll delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.genruntimeconfig.cache delete mode 100644 Chromatics/obj/Debug/net5.0-windows/Chromatics.pdb delete mode 100644 Chromatics/obj/Debug/net5.0-windows/apphost.exe delete mode 100644 Chromatics/obj/Debug/net5.0-windows/ref/Chromatics.dll delete mode 100644 Chromatics/obj/Debug/net6.0-windows/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs delete mode 100644 Chromatics/obj/Debug/net6.0-windows/Chromatics.assets.cache delete mode 100644 Chromatics/obj/Debug/net6.0-windows/Chromatics.csproj.FileListAbsolute.txt delete mode 100644 Chromatics/obj/Debug/net6.0-windows/Chromatics.designer.deps.json delete mode 100644 Chromatics/obj/Debug/net6.0-windows/Chromatics.designer.runtimeconfig.json delete mode 100644 Chromatics/obj/Debug/net6.0-windows/apphost.exe delete mode 100644 Chromatics/obj/Release/net5.0-windows/.NETCoreApp,Version=v5.0.AssemblyAttributes.cs delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.AssemblyInfo.cs delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.AssemblyInfoInputs.cache delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.Chromatics.resources delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.Forms.Fm_MainWindow.resources delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.Forms.Pn_LayerDisplay.resources delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.Forms.Uc_Console.resources delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.Forms.Uc_Mappings.resources delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.GeneratedMSBuildEditorConfig.editorconfig delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.assets.cache delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.csproj.AssemblyReference.cache delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.csproj.CopyComplete delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.csproj.CoreCompileInputs.cache delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.csproj.FileListAbsolute.txt delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.csproj.GenerateResource.cache delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.designer.deps.json delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.designer.runtimeconfig.json delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.dll delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.genruntimeconfig.cache delete mode 100644 Chromatics/obj/Release/net5.0-windows/Chromatics.pdb delete mode 100644 Chromatics/obj/Release/net5.0-windows/apphost.exe delete mode 100644 Chromatics/obj/Release/net5.0-windows/ref/Chromatics.dll delete mode 100644 Chromatics/obj/project.assets.json diff --git a/Chromatics/obj/Chromatics.csproj.nuget.dgspec.json b/Chromatics/obj/Chromatics.csproj.nuget.dgspec.json deleted file mode 100644 index a04729ac..00000000 --- a/Chromatics/obj/Chromatics.csproj.nuget.dgspec.json +++ /dev/null @@ -1,486 +0,0 @@ -{ - "format": 1, - "restore": { - "D:\\Git Projects\\roxaskeyheart\\Chromatics Workbench\\Chromatics\\Chromatics\\Chromatics.csproj": {} - }, - "projects": { - "D:\\Git Projects\\roxaskeyheart\\Chromatics Workbench\\Chromatics\\Chromatics\\Chromatics.csproj": { - "version": "4.1.38", - "restore": { - "projectUniqueName": "D:\\Git Projects\\roxaskeyheart\\Chromatics Workbench\\Chromatics\\Chromatics\\Chromatics.csproj", - "projectName": "Chromatics", - "projectPath": "D:\\Git Projects\\roxaskeyheart\\Chromatics Workbench\\Chromatics\\Chromatics\\Chromatics.csproj", - "packagesPath": "C:\\Users\\Hanielle\\.nuget\\packages\\", - "outputPath": "D:\\Git Projects\\roxaskeyheart\\Chromatics Workbench\\Chromatics\\Chromatics\\obj\\", - "projectStyle": "PackageReference", - "fallbackFolders": [ - "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" - ], - "configFilePaths": [ - "C:\\Users\\Hanielle\\AppData\\Roaming\\NuGet\\NuGet.Config", - "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config", - "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" - ], - "originalTargetFrameworks": [ - "net10.0-windows7.0" - ], - "sources": { - "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, - "C:\\Program Files\\dotnet\\library-packs": {}, - "https://api.nuget.org/v3/index.json": {} - }, - "frameworks": { - "net10.0-windows7.0": { - "targetAlias": "net10.0-windows7.0", - "projectReferences": {} - } - }, - "warningProperties": { - "warnAsError": [ - "NU1605" - ] - }, - "restoreAuditProperties": { - "enableAudit": "true", - "auditLevel": "low", - "auditMode": "all" - }, - "SdkAnalysisLevel": "10.0.200" - }, - "frameworks": { - "net10.0-windows7.0": { - "targetAlias": "net10.0-windows7.0", - "dependencies": { - "Avalonia": { - "target": "Package", - "version": "[12.0.3, )" - }, - "Avalonia.Controls.ColorPicker": { - "target": "Package", - "version": "[12.0.3, )" - }, - "Avalonia.Controls.DataGrid": { - "target": "Package", - "version": "[12.0.0, )" - }, - "Avalonia.Desktop": { - "target": "Package", - "version": "[12.0.3, )" - }, - "Avalonia.Fonts.Inter": { - "target": "Package", - "version": "[12.0.3, )" - }, - "Avalonia.Themes.Fluent": { - "target": "Package", - "version": "[12.0.3, )" - }, - "CommunityToolkit.Mvvm": { - "target": "Package", - "version": "[8.4.2, )" - }, - "HidSharp": { - "target": "Package", - "version": "[2.6.4, )" - }, - "HueApi": { - "target": "Package", - "version": "[3.2.0, )" - }, - "HueApi.ColorConverters": { - "target": "Package", - "version": "[3.1.0, )" - }, - "HueApi.Entertainment": { - "target": "Package", - "version": "[3.1.0, )" - }, - "Markdown.Avalonia.Tight": { - "target": "Package", - "version": "[12.0.0-a3, )" - }, - "NAudio": { - "target": "Package", - "version": "[2.3.0, )" - }, - "NLog": { - "target": "Package", - "version": "[6.1.3, )" - }, - "Newtonsoft.Json": { - "target": "Package", - "version": "[13.0.4, )" - }, - "RGB.NET.Core": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.Devices.Asus": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.Devices.CoolerMaster": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.Devices.Corsair": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.Devices.Logitech": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.Devices.Msi": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.Devices.Novation": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.Devices.OpenRGB": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.Devices.Razer": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.Devices.SteelSeries": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.Devices.Wooting": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.HID": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.Layout": { - "target": "Package", - "version": "[3.2.0, )" - }, - "RGB.NET.Presets": { - "target": "Package", - "version": "[3.2.0, )" - }, - "Sentry": { - "target": "Package", - "version": "[6.5.0, )" - }, - "Sentry.Profiling": { - "target": "Package", - "version": "[6.5.0, )" - }, - "Sharlayan": { - "target": "Package", - "version": "[9.0.34, )" - }, - "System.Drawing.Common": { - "target": "Package", - "version": "[10.0.8, )" - }, - "Velopack": { - "target": "Package", - "version": "[0.0.1298, )" - } - }, - "imports": [ - "net461", - "net462", - "net47", - "net471", - "net472", - "net48", - "net481" - ], - "assetTargetFallback": true, - "warn": true, - "frameworkReferences": { - "Microsoft.NETCore.App": { - "privateAssets": "all" - } - }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.203/PortableRuntimeIdentifierGraph.json", - "packagesToPrune": { - "Microsoft.CSharp": "(,4.7.32767]", - "Microsoft.VisualBasic": "(,10.4.32767]", - "Microsoft.Win32.Primitives": "(,4.3.32767]", - "Microsoft.Win32.Registry": "(,5.0.32767]", - "runtime.any.System.Collections": "(,4.3.32767]", - "runtime.any.System.Diagnostics.Tools": "(,4.3.32767]", - "runtime.any.System.Diagnostics.Tracing": "(,4.3.32767]", - "runtime.any.System.Globalization": "(,4.3.32767]", - "runtime.any.System.Globalization.Calendars": "(,4.3.32767]", - "runtime.any.System.IO": "(,4.3.32767]", - "runtime.any.System.Reflection": "(,4.3.32767]", - "runtime.any.System.Reflection.Extensions": "(,4.3.32767]", - "runtime.any.System.Reflection.Primitives": "(,4.3.32767]", - "runtime.any.System.Resources.ResourceManager": "(,4.3.32767]", - "runtime.any.System.Runtime": "(,4.3.32767]", - "runtime.any.System.Runtime.Handles": "(,4.3.32767]", - "runtime.any.System.Runtime.InteropServices": "(,4.3.32767]", - "runtime.any.System.Text.Encoding": "(,4.3.32767]", - "runtime.any.System.Text.Encoding.Extensions": "(,4.3.32767]", - "runtime.any.System.Threading.Tasks": "(,4.3.32767]", - "runtime.any.System.Threading.Timer": "(,4.3.32767]", - "runtime.aot.System.Collections": "(,4.3.32767]", - "runtime.aot.System.Diagnostics.Tools": "(,4.3.32767]", - "runtime.aot.System.Diagnostics.Tracing": "(,4.3.32767]", - "runtime.aot.System.Globalization": "(,4.3.32767]", - "runtime.aot.System.Globalization.Calendars": "(,4.3.32767]", - "runtime.aot.System.IO": "(,4.3.32767]", - "runtime.aot.System.Reflection": "(,4.3.32767]", - "runtime.aot.System.Reflection.Extensions": "(,4.3.32767]", - "runtime.aot.System.Reflection.Primitives": "(,4.3.32767]", - "runtime.aot.System.Resources.ResourceManager": "(,4.3.32767]", - "runtime.aot.System.Runtime": "(,4.3.32767]", - "runtime.aot.System.Runtime.Handles": "(,4.3.32767]", - "runtime.aot.System.Runtime.InteropServices": "(,4.3.32767]", - "runtime.aot.System.Text.Encoding": "(,4.3.32767]", - "runtime.aot.System.Text.Encoding.Extensions": "(,4.3.32767]", - "runtime.aot.System.Threading.Tasks": "(,4.3.32767]", - "runtime.aot.System.Threading.Timer": "(,4.3.32767]", - "runtime.debian.8-x64.runtime.native.System": "(,4.3.32767]", - "runtime.debian.8-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.debian.8-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.debian.8-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography": "(,4.3.32767]", - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "(,4.3.32767]", - "runtime.debian.9-x64.runtime.native.System": "(,4.3.32767]", - "runtime.debian.9-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.debian.9-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.debian.9-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.fedora.23-x64.runtime.native.System": "(,4.3.32767]", - "runtime.fedora.23-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.fedora.23-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.fedora.23-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography": "(,4.3.32767]", - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "(,4.3.32767]", - "runtime.fedora.24-x64.runtime.native.System": "(,4.3.32767]", - "runtime.fedora.24-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.fedora.24-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.fedora.24-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography": "(,4.3.32767]", - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "(,4.3.32767]", - "runtime.fedora.27-x64.runtime.native.System": "(,4.3.32767]", - "runtime.fedora.27-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.fedora.27-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.fedora.27-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.fedora.28-x64.runtime.native.System": "(,4.3.32767]", - "runtime.fedora.28-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.fedora.28-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.fedora.28-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.opensuse.13.2-x64.runtime.native.System": "(,4.3.32767]", - "runtime.opensuse.13.2-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.opensuse.13.2-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.opensuse.13.2-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography": "(,4.3.32767]", - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "(,4.3.32767]", - "runtime.opensuse.42.1-x64.runtime.native.System": "(,4.3.32767]", - "runtime.opensuse.42.1-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.opensuse.42.1-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.opensuse.42.1-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography": "(,4.3.32767]", - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "(,4.3.32767]", - "runtime.opensuse.42.3-x64.runtime.native.System": "(,4.3.32767]", - "runtime.opensuse.42.3-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.opensuse.42.3-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.opensuse.42.3-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.osx.10.10-x64.runtime.native.System": "(,4.3.32767]", - "runtime.osx.10.10-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.osx.10.10-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.osx.10.10-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography": "(,4.3.32767]", - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "(,4.3.32767]", - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "(,4.3.32767]", - "runtime.rhel.7-x64.runtime.native.System": "(,4.3.32767]", - "runtime.rhel.7-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.rhel.7-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.rhel.7-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography": "(,4.3.32767]", - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "(,4.3.32767]", - "runtime.ubuntu.14.04-x64.runtime.native.System": "(,4.3.32767]", - "runtime.ubuntu.14.04-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.ubuntu.14.04-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.ubuntu.14.04-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography": "(,4.3.32767]", - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "(,4.3.32767]", - "runtime.ubuntu.16.04-x64.runtime.native.System": "(,4.3.32767]", - "runtime.ubuntu.16.04-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.ubuntu.16.04-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.ubuntu.16.04-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography": "(,4.3.32767]", - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "(,4.3.32767]", - "runtime.ubuntu.16.10-x64.runtime.native.System": "(,4.3.32767]", - "runtime.ubuntu.16.10-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.ubuntu.16.10-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.ubuntu.16.10-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography": "(,4.3.32767]", - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "(,4.3.32767]", - "runtime.ubuntu.18.04-x64.runtime.native.System": "(,4.3.32767]", - "runtime.ubuntu.18.04-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.ubuntu.18.04-x64.runtime.native.System.Net.Http": "(,4.3.32767]", - "runtime.ubuntu.18.04-x64.runtime.native.System.Net.Security": "(,4.3.32767]", - "runtime.unix.Microsoft.Win32.Primitives": "(,4.3.32767]", - "runtime.unix.System.Console": "(,4.3.32767]", - "runtime.unix.System.Diagnostics.Debug": "(,4.3.32767]", - "runtime.unix.System.IO.FileSystem": "(,4.3.32767]", - "runtime.unix.System.Net.Primitives": "(,4.3.32767]", - "runtime.unix.System.Net.Sockets": "(,4.3.32767]", - "runtime.unix.System.Private.Uri": "(,4.3.32767]", - "runtime.unix.System.Runtime.Extensions": "(,4.3.32767]", - "runtime.win.Microsoft.Win32.Primitives": "(,4.3.32767]", - "runtime.win.System.Console": "(,4.3.32767]", - "runtime.win.System.Diagnostics.Debug": "(,4.3.32767]", - "runtime.win.System.IO.FileSystem": "(,4.3.32767]", - "runtime.win.System.Net.Primitives": "(,4.3.32767]", - "runtime.win.System.Net.Sockets": "(,4.3.32767]", - "runtime.win.System.Runtime.Extensions": "(,4.3.32767]", - "runtime.win10-arm-aot.runtime.native.System.IO.Compression": "(,4.0.32767]", - "runtime.win10-arm64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.win10-x64-aot.runtime.native.System.IO.Compression": "(,4.0.32767]", - "runtime.win10-x86-aot.runtime.native.System.IO.Compression": "(,4.0.32767]", - "runtime.win7-x64.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.win7-x86.runtime.native.System.IO.Compression": "(,4.3.32767]", - "runtime.win7.System.Private.Uri": "(,4.3.32767]", - "runtime.win8-arm.runtime.native.System.IO.Compression": "(,4.3.32767]", - "System.AppContext": "(,4.3.32767]", - "System.Buffers": "(,5.0.32767]", - "System.Collections": "(,4.3.32767]", - "System.Collections.Concurrent": "(,4.3.32767]", - "System.Collections.Immutable": "(,10.0.32767]", - "System.Collections.NonGeneric": "(,4.3.32767]", - "System.Collections.Specialized": "(,4.3.32767]", - "System.ComponentModel": "(,4.3.32767]", - "System.ComponentModel.Annotations": "(,4.3.32767]", - "System.ComponentModel.EventBasedAsync": "(,4.3.32767]", - "System.ComponentModel.Primitives": "(,4.3.32767]", - "System.ComponentModel.TypeConverter": "(,4.3.32767]", - "System.Console": "(,4.3.32767]", - "System.Data.Common": "(,4.3.32767]", - "System.Data.DataSetExtensions": "(,4.4.32767]", - "System.Diagnostics.Contracts": "(,4.3.32767]", - "System.Diagnostics.Debug": "(,4.3.32767]", - "System.Diagnostics.DiagnosticSource": "(,10.0.32767]", - "System.Diagnostics.FileVersionInfo": "(,4.3.32767]", - "System.Diagnostics.Process": "(,4.3.32767]", - "System.Diagnostics.StackTrace": "(,4.3.32767]", - "System.Diagnostics.TextWriterTraceListener": "(,4.3.32767]", - "System.Diagnostics.Tools": "(,4.3.32767]", - "System.Diagnostics.TraceSource": "(,4.3.32767]", - "System.Diagnostics.Tracing": "(,4.3.32767]", - "System.Drawing.Primitives": "(,4.3.32767]", - "System.Dynamic.Runtime": "(,4.3.32767]", - "System.Formats.Asn1": "(,10.0.32767]", - "System.Formats.Tar": "(,10.0.32767]", - "System.Globalization": "(,4.3.32767]", - "System.Globalization.Calendars": "(,4.3.32767]", - "System.Globalization.Extensions": "(,4.3.32767]", - "System.IO": "(,4.3.32767]", - "System.IO.Compression": "(,4.3.32767]", - "System.IO.Compression.ZipFile": "(,4.3.32767]", - "System.IO.FileSystem": "(,4.3.32767]", - "System.IO.FileSystem.AccessControl": "(,4.4.32767]", - "System.IO.FileSystem.DriveInfo": "(,4.3.32767]", - "System.IO.FileSystem.Primitives": "(,4.3.32767]", - "System.IO.FileSystem.Watcher": "(,4.3.32767]", - "System.IO.IsolatedStorage": "(,4.3.32767]", - "System.IO.MemoryMappedFiles": "(,4.3.32767]", - "System.IO.Pipelines": "(,10.0.32767]", - "System.IO.Pipes": "(,4.3.32767]", - "System.IO.Pipes.AccessControl": "(,5.0.32767]", - "System.IO.UnmanagedMemoryStream": "(,4.3.32767]", - "System.Linq": "(,4.3.32767]", - "System.Linq.AsyncEnumerable": "(,10.0.32767]", - "System.Linq.Expressions": "(,4.3.32767]", - "System.Linq.Parallel": "(,4.3.32767]", - "System.Linq.Queryable": "(,4.3.32767]", - "System.Memory": "(,5.0.32767]", - "System.Net.Http": "(,4.3.32767]", - "System.Net.Http.Json": "(,10.0.32767]", - "System.Net.NameResolution": "(,4.3.32767]", - "System.Net.NetworkInformation": "(,4.3.32767]", - "System.Net.Ping": "(,4.3.32767]", - "System.Net.Primitives": "(,4.3.32767]", - "System.Net.Requests": "(,4.3.32767]", - "System.Net.Security": "(,4.3.32767]", - "System.Net.ServerSentEvents": "(,10.0.32767]", - "System.Net.Sockets": "(,4.3.32767]", - "System.Net.WebHeaderCollection": "(,4.3.32767]", - "System.Net.WebSockets": "(,4.3.32767]", - "System.Net.WebSockets.Client": "(,4.3.32767]", - "System.Numerics.Vectors": "(,5.0.32767]", - "System.ObjectModel": "(,4.3.32767]", - "System.Private.DataContractSerialization": "(,4.3.32767]", - "System.Private.Uri": "(,4.3.32767]", - "System.Reflection": "(,4.3.32767]", - "System.Reflection.DispatchProxy": "(,6.0.32767]", - "System.Reflection.Emit": "(,4.7.32767]", - "System.Reflection.Emit.ILGeneration": "(,4.7.32767]", - "System.Reflection.Emit.Lightweight": "(,4.7.32767]", - "System.Reflection.Extensions": "(,4.3.32767]", - "System.Reflection.Metadata": "(,10.0.32767]", - "System.Reflection.Primitives": "(,4.3.32767]", - "System.Reflection.TypeExtensions": "(,4.3.32767]", - "System.Resources.Reader": "(,4.3.32767]", - "System.Resources.ResourceManager": "(,4.3.32767]", - "System.Resources.Writer": "(,4.3.32767]", - "System.Runtime": "(,4.3.32767]", - "System.Runtime.CompilerServices.Unsafe": "(,7.0.32767]", - "System.Runtime.CompilerServices.VisualC": "(,4.3.32767]", - "System.Runtime.Extensions": "(,4.3.32767]", - "System.Runtime.Handles": "(,4.3.32767]", - "System.Runtime.InteropServices": "(,4.3.32767]", - "System.Runtime.InteropServices.RuntimeInformation": "(,4.3.32767]", - "System.Runtime.Loader": "(,4.3.32767]", - "System.Runtime.Numerics": "(,4.3.32767]", - "System.Runtime.Serialization.Formatters": "(,4.3.32767]", - "System.Runtime.Serialization.Json": "(,4.3.32767]", - "System.Runtime.Serialization.Primitives": "(,4.3.32767]", - "System.Runtime.Serialization.Xml": "(,4.3.32767]", - "System.Security.AccessControl": "(,6.0.32767]", - "System.Security.Claims": "(,4.3.32767]", - "System.Security.Cryptography.Algorithms": "(,4.3.32767]", - "System.Security.Cryptography.Cng": "(,5.0.32767]", - "System.Security.Cryptography.Csp": "(,4.3.32767]", - "System.Security.Cryptography.Encoding": "(,4.3.32767]", - "System.Security.Cryptography.OpenSsl": "(,5.0.32767]", - "System.Security.Cryptography.Primitives": "(,4.3.32767]", - "System.Security.Cryptography.X509Certificates": "(,4.3.32767]", - "System.Security.Principal": "(,4.3.32767]", - "System.Security.Principal.Windows": "(,5.0.32767]", - "System.Security.SecureString": "(,4.3.32767]", - "System.Text.Encoding": "(,4.3.32767]", - "System.Text.Encoding.CodePages": "(,10.0.32767]", - "System.Text.Encoding.Extensions": "(,4.3.32767]", - "System.Text.Encodings.Web": "(,10.0.32767]", - "System.Text.Json": "(,10.0.32767]", - "System.Text.RegularExpressions": "(,4.3.32767]", - "System.Threading": "(,4.3.32767]", - "System.Threading.AccessControl": "(,10.0.32767]", - "System.Threading.Channels": "(,10.0.32767]", - "System.Threading.Overlapped": "(,4.3.32767]", - "System.Threading.Tasks": "(,4.3.32767]", - "System.Threading.Tasks.Dataflow": "(,10.0.32767]", - "System.Threading.Tasks.Extensions": "(,5.0.32767]", - "System.Threading.Tasks.Parallel": "(,4.3.32767]", - "System.Threading.Thread": "(,4.3.32767]", - "System.Threading.ThreadPool": "(,4.3.32767]", - "System.Threading.Timer": "(,4.3.32767]", - "System.ValueTuple": "(,4.5.32767]", - "System.Xml.ReaderWriter": "(,4.3.32767]", - "System.Xml.XDocument": "(,4.3.32767]", - "System.Xml.XmlDocument": "(,4.3.32767]", - "System.Xml.XmlSerializer": "(,4.3.32767]", - "System.Xml.XPath": "(,4.3.32767]", - "System.Xml.XPath.XDocument": "(,5.0.32767]" - } - } - } - } - } -} \ No newline at end of file diff --git a/Chromatics/obj/Chromatics.csproj.nuget.g.props b/Chromatics/obj/Chromatics.csproj.nuget.g.props deleted file mode 100644 index 2728e425..00000000 --- a/Chromatics/obj/Chromatics.csproj.nuget.g.props +++ /dev/null @@ -1,27 +0,0 @@ - - - - True - NuGet - $(MSBuildThisFileDirectory)project.assets.json - $(UserProfile)\.nuget\packages\ - C:\Users\Hanielle\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages - PackageReference - 7.0.0 - - - - - - - - - - - - - C:\Users\Hanielle\.nuget\packages\sentry\6.5.0 - C:\Users\Hanielle\.nuget\packages\avalonia.buildservices\11.3.2 - C:\Users\Hanielle\.nuget\packages\avalonia\12.0.3 - - \ No newline at end of file diff --git a/Chromatics/obj/Chromatics.csproj.nuget.g.targets b/Chromatics/obj/Chromatics.csproj.nuget.g.targets deleted file mode 100644 index 9c67a8ce..00000000 --- a/Chromatics/obj/Chromatics.csproj.nuget.g.targets +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/Chromatics/obj/Debug/net5.0-windows/.NETCoreApp,Version=v5.0.AssemblyAttributes.cs b/Chromatics/obj/Debug/net5.0-windows/.NETCoreApp,Version=v5.0.AssemblyAttributes.cs deleted file mode 100644 index 2f7e5ec5..00000000 --- a/Chromatics/obj/Debug/net5.0-windows/.NETCoreApp,Version=v5.0.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v5.0", FrameworkDisplayName = "")] diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.AssemblyInfo.cs b/Chromatics/obj/Debug/net5.0-windows/Chromatics.AssemblyInfo.cs deleted file mode 100644 index ab5f0c35..00000000 --- a/Chromatics/obj/Debug/net5.0-windows/Chromatics.AssemblyInfo.cs +++ /dev/null @@ -1,27 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -using System; -using System.Reflection; - -[assembly: System.Reflection.AssemblyCompanyAttribute("Roxas Keyheart")] -[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] -[assembly: System.Reflection.AssemblyCopyrightAttribute("Roxas Keyheart 2022")] -[assembly: System.Reflection.AssemblyFileVersionAttribute("3.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("3.0.0")] -[assembly: System.Reflection.AssemblyProductAttribute("Chromatics")] -[assembly: System.Reflection.AssemblyTitleAttribute("Chromatics")] -[assembly: System.Reflection.AssemblyVersionAttribute("3.0.0.0")] -[assembly: System.Resources.NeutralResourcesLanguageAttribute("en-AU")] -[assembly: System.Runtime.Versioning.TargetPlatformAttribute("Windows7.0")] -[assembly: System.Runtime.Versioning.SupportedOSPlatformAttribute("Windows7.0")] - -// Generated by the MSBuild WriteCodeFragment class. - diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.AssemblyInfoInputs.cache b/Chromatics/obj/Debug/net5.0-windows/Chromatics.AssemblyInfoInputs.cache deleted file mode 100644 index 48b653ed..00000000 --- a/Chromatics/obj/Debug/net5.0-windows/Chromatics.AssemblyInfoInputs.cache +++ /dev/null @@ -1 +0,0 @@ -69c4aba92e88b03e0944241bc811a03043a4aacb diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.Chromatics.resources b/Chromatics/obj/Debug/net5.0-windows/Chromatics.Chromatics.resources deleted file mode 100644 index 6c05a9776bd7cbae976fdcec7e3a254e93018279..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmX?i>is@O1_p+SK%5g?SzMBus~417oL^d$oLUTL1*ImYq!#HYR*8GxXUf^%t3Noi54ZC+|=Nl{{sjzU0bQch;FcWPxwes*e}ZIZcpqG__J onW3ezNveT`r81^vrFkWpxv4PQgHubGfR2KJ07n-P+5+SQ04Y>DD*ylh diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Fm_MainWindow.resources b/Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Fm_MainWindow.resources deleted file mode 100644 index 6c05a9776bd7cbae976fdcec7e3a254e93018279..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmX?i>is@O1_p+SK%5g?SzMBus~417oL^d$oLUTL1*ImYq!#HYR*8GxXUf^%t3Noi54ZC+|=Nl{{sjzU0bQch;FcWPxwes*e}ZIZcpqG__J onW3ezNveT`r81^vrFkWpxv4PQgHubGfR2KJ07n-P+5+SQ04Y>DD*ylh diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Pn_LayerDisplay.resources b/Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Pn_LayerDisplay.resources deleted file mode 100644 index 6c05a9776bd7cbae976fdcec7e3a254e93018279..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmX?i>is@O1_p+SK%5g?SzMBus~417oL^d$oLUTL1*ImYq!#HYR*8GxXUf^%t3Noi54ZC+|=Nl{{sjzU0bQch;FcWPxwes*e}ZIZcpqG__J onW3ezNveT`r81^vrFkWpxv4PQgHubGfR2KJ07n-P+5+SQ04Y>DD*ylh diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Uc_Console.resources b/Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Uc_Console.resources deleted file mode 100644 index 6c05a9776bd7cbae976fdcec7e3a254e93018279..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmX?i>is@O1_p+SK%5g?SzMBus~417oL^d$oLUTL1*ImYq!#HYR*8GxXUf^%t3Noi54ZC+|=Nl{{sjzU0bQch;FcWPxwes*e}ZIZcpqG__J onW3ezNveT`r81^vrFkWpxv4PQgHubGfR2KJ07n-P+5+SQ04Y>DD*ylh diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Uc_Mappings.resources b/Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Uc_Mappings.resources deleted file mode 100644 index 6c05a9776bd7cbae976fdcec7e3a254e93018279..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmX?i>is@O1_p+SK%5g?SzMBus~417oL^d$oLUTL1*ImYq!#HYR*8GxXUf^%t3Noi54ZC+|=Nl{{sjzU0bQch;FcWPxwes*e}ZIZcpqG__J onW3ezNveT`r81^vrFkWpxv4PQgHubGfR2KJ07n-P+5+SQ04Y>DD*ylh diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Uc_VirtualKeyboard.resources b/Chromatics/obj/Debug/net5.0-windows/Chromatics.Forms.Uc_VirtualKeyboard.resources deleted file mode 100644 index 6c05a9776bd7cbae976fdcec7e3a254e93018279..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmX?i>is@O1_p+SK%5g?SzMBus~417oL^d$oLUTL1*ImYq!#HYR*8GxXUf^%t3Noi54ZC+|=Nl{{sjzU0bQch;FcWPxwes*e}ZIZcpqG__J onW3ezNveT`r81^vrFkWpxv4PQgHubGfR2KJ07n-P+5+SQ04Y>DD*ylh diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.GeneratedMSBuildEditorConfig.editorconfig b/Chromatics/obj/Debug/net5.0-windows/Chromatics.GeneratedMSBuildEditorConfig.editorconfig deleted file mode 100644 index 834feee8..00000000 --- a/Chromatics/obj/Debug/net5.0-windows/Chromatics.GeneratedMSBuildEditorConfig.editorconfig +++ /dev/null @@ -1,16 +0,0 @@ -is_global = true -build_property.ApplicationManifest = app.manifest -build_property.StartupObject = Chromatics.Program -build_property.ApplicationDefaultFont = -build_property.ApplicationHighDpiMode = -build_property.ApplicationUseCompatibleTextRendering = -build_property.ApplicationVisualStyles = -build_property.TargetFramework = net5.0-windows -build_property.TargetPlatformMinVersion = 7.0 -build_property.UsingMicrosoftNETSdkWeb = -build_property.ProjectTypeGuids = -build_property.InvariantGlobalization = -build_property.PlatformNeutralAssembly = -build_property._SupportedPlatformList = Linux,macOS,Windows -build_property.RootNamespace = Chromatics -build_property.ProjectDir = C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\ diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.Properties.Resources.resources b/Chromatics/obj/Debug/net5.0-windows/Chromatics.Properties.Resources.resources deleted file mode 100644 index 2a63b9e75fed1dad5e998ebcf766e5d6b13822a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2176 zcmcIleNYr-7=I7(I|?Qu#l+>9rZBkMz2msMl}qsUz#BxagOZdPU3T}l<0Qgkkc>4Q)Uzs*QUA`=3?l9C*4lKjB2EEG+84>;=&$7Y&ozutY{eV*s{ zdw%;oyB%L&J0t@D#KX%bS&q*wF`UB6xP@YoV%!RoS7LZKEAVaw!MGWbC7o;u%NGVM ztPDvrVgm92#{}dBMht{7Bq`9qo`4vOol>#L7gGq?E%*|2LlyC|E$Wz-b>%oe)V66h<+Jl{nIMkHP5j ze-NPnRogT+tqsFy4N9s>OBw->49zcFAS(C~V#hcD1AKr2ByfSnpa6~(aDYPK1Oo5` ztUzk$masLJ`59r8q9?&=n0YQ?h2ztKbHX@yU4M5k2ZyPWDNo7~Y?6l*8IVQ`1q@>5 z$U>+pN&A+zGbsR!q1oI#DKBdtP6?cX4A>~VoC|sbkTTorA}I$WAq7k!%Nyj^4ty?0 zSlS?8pvgkBTt>#u&Mgs{oRayuREdMq)AHHRA}L-R2H+TpM7*4ncjI1zd^j!+?*q$9 zIWlY_ISld z4$(x~hFgjBQ7kw!$nBEk!j(#o$D{D56@pl(#PoW7Acjhn2rUxbK3*cdiM%^57ytT=5ax1sNI8s0)~(co-MNQrI1cSpo%$$m&}Ag4xcD!md5oOB3*?N#&oSt zV@k#_okp$JsgtlY6P9L7H>Cw*+|M^TDLE|-)$4ScbUlWdl69ySOV^@kIyNU+r#I?? zzGmJnkvzo&aao8v>I+dH;ENkYhLi*`R}h@R3b5D(NpRZ*7h=rOAPZQY7Ci32%Nfa) zfFG%444p0t95Spf&fW+8-?JX^rT>d!N=QZ-D2;!VN^l9*U*Ps$;ls(j)?s+qFCuIV z?>eaufRIizk(TT2x$@3Lp(i!G{IlIy#`k5`-z~2X4RwCF_QMFvljEixoc^A+&v#&Q zq|hzj*}d^tPsfVdGc9WDR8Q;ihgvV*IgtIS)KvT9mg@1%R_khNv1LYHTu)b5c|V8_ zDY`wrZgcAK+OjYiXWI4t;ic>Dw*0wod}DYeL3M?;#SWf%xBZ=Mr}PWrC5`32$d8H4t`OxAF>qCOa=9K^xNVPr!0+)dzfl>{ z-_S-p*P=-+T~oTY)RHu91KBJ?>dS7+2Veay{@|2pu9r8>IKRKDy~gl%__~)9M zK3ieiTOZr6Hb?FVJvU+HMPbh!)i=Q1e>*L2nFTB0F>6`US8;{P5W}oUJ{h3ph zg()4C!t^IV{e*<>oxAbg@JT(9s@jVFsOl0r<@@}twvc4?)?+tXTg=1`_w2d&?(Rp! zJ7vqN!dhbQZeLW=dUeP@*0@3wb5Pa`gtF5Q_Z`bxyyH)Ds3>L1ubW?r-9FYZb<*|r z4a85b!7DK-ebw^(u`~K79@#q7P<8f)jn^IVhZ8gMYmVIlXQ~}SM%nFbzN4uqTko$6 zeahC|^v8E+fYcUqLMtAPnEdskjTLO6k-fO#O2d-n#^+`MQFXTWW|&5er4L%d5K5d*?Vy~Ka)Y^;3HfNshJd!hL=U!T6jOyENj5+aI_`31 zm2+Iax{#l%k<6Me^P8^#Ro$WxMeWYe{8>Htu1l@D=S#I0I{T&^sOZ)0s1({%8F#~$ lHgtzyxPUZW^YjR^aUl2Nizm-YQN#Z)vuQrjIA_T#e*wi)EQtUB diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.assets.cache b/Chromatics/obj/Debug/net5.0-windows/Chromatics.assets.cache deleted file mode 100644 index ca91deb31816c77383918f0dedd14ac0612b58ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11997 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 TBQ>^zw{vml@FP3);fVbJ_ff(+ diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.AssemblyReference.cache b/Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.AssemblyReference.cache deleted file mode 100644 index 52d688944d077a5af3f80e214e6d43815810e485..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118799 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 diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.CopyComplete b/Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.CopyComplete deleted file mode 100644 index e69de29b..00000000 diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.CoreCompileInputs.cache b/Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.CoreCompileInputs.cache deleted file mode 100644 index 8734dfb6..00000000 --- a/Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.CoreCompileInputs.cache +++ /dev/null @@ -1 +0,0 @@ -dd426ca51fef795e9c46da501bd98c89b04635cf diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.FileListAbsolute.txt b/Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.FileListAbsolute.txt deleted file mode 100644 index e47bbed2..00000000 --- a/Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.FileListAbsolute.txt +++ /dev/null @@ -1,48 +0,0 @@ -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\Chromatics.exe -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\Chromatics.dll.config -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\Chromatics.deps.json -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\Chromatics.runtimeconfig.json -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\Chromatics.runtimeconfig.dev.json -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\Chromatics.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\ref\Chromatics.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\Chromatics.pdb -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\HidSharp.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\Newtonsoft.Json.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.Core.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.Devices.Asus.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.Devices.CoolerMaster.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.Devices.Corsair.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.Devices.Logitech.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.Devices.Msi.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.Devices.Novation.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.Devices.Razer.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.Devices.SteelSeries.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.Devices.Wooting.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.HID.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.Layout.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\RGB.NET.Presets.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\Sanford.Multimedia.Midi.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\System.Management.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\runtimes\win\lib\netcoreapp2.0\System.Management.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\MetroFramework.Design.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\MetroFramework.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\MetroFramework.Fonts.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.Chromatics.resources -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.Forms.Fm_MainWindow.resources -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.Forms.Pn_LayerDisplay.resources -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.Forms.Uc_Console.resources -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.Forms.Uc_Mappings.resources -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.Forms.Uc_VirtualKeyboard.resources -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.Properties.Resources.resources -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.csproj.GenerateResource.cache -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.GeneratedMSBuildEditorConfig.editorconfig -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.AssemblyInfoInputs.cache -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.AssemblyInfo.cs -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.csproj.CoreCompileInputs.cache -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.csproj.CopyComplete -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\ref\Chromatics.dll -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.pdb -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.genruntimeconfig.cache -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\obj\Debug\net5.0-windows\Chromatics.csproj.AssemblyReference.cache -C:\Users\Dani\source\repos\roxaskeyheart\Chromatics3\Chromatics\bin\Debug\net5.0-windows\Interop.AuraServiceLib.dll diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.GenerateResource.cache b/Chromatics/obj/Debug/net5.0-windows/Chromatics.csproj.GenerateResource.cache deleted file mode 100644 index cbb5c87816231b4c906ee8939a02309727b05a78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 598 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 diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.genruntimeconfig.cache b/Chromatics/obj/Debug/net5.0-windows/Chromatics.genruntimeconfig.cache deleted file mode 100644 index 458a072b..00000000 --- a/Chromatics/obj/Debug/net5.0-windows/Chromatics.genruntimeconfig.cache +++ /dev/null @@ -1 +0,0 @@ -cb8858f744c12e76962d363ed1f3442611419c63 diff --git a/Chromatics/obj/Debug/net5.0-windows/Chromatics.pdb b/Chromatics/obj/Debug/net5.0-windows/Chromatics.pdb deleted file mode 100644 index e7f74fd421bd38ba16ce4bcd67eae9317c8b3265..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38156 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