diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..820f11d
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,39 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ build:
+ name: Build
+ env:
+ version: 1.0.0.0
+ strategy:
+ matrix:
+ os: [windows-latest, ubuntu-latest]
+ runtime: [win-x64, linux-x64]
+ exclude:
+ - os: windows-latest
+ runtime: linux-x64
+ - os: ubuntu-latest
+ runtime: win-x64
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4.1.7
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4.0.0
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Publish 'MSI Keyboard Illuminator'
+ run: dotnet publish 'MSI.Keyboard.Illuminator/MSI.Keyboard.Illuminator.csproj' -o 'binaries' -c 'Release' -r '${{ matrix.runtime }}' -v 'normal' -p:Version=${{ env.version }}
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4.3.3
+ with:
+ name: MSI.Keyboard.Illuminator_${{ matrix.runtime }}_${{ env.version }}
+ path: binaries/
diff --git a/.gitignore b/.gitignore
index 259148f..834faf6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,32 +1,10 @@
-# Prerequisites
-*.d
-
-# Compiled Object files
-*.slo
-*.lo
-*.o
-*.obj
-
-# Precompiled Headers
-*.gch
-*.pch
-
-# Compiled Dynamic libraries
-*.so
-*.dylib
-*.dll
-
-# Fortran module files
-*.mod
-*.smod
-
-# Compiled Static libraries
-*.lai
-*.la
-*.a
-*.lib
-
-# Executables
-*.exe
-*.out
-*.app
+# C#
+*.suo
+*.user
+.vs/
+/MSI.Keyboard.Illuminator/bin/
+/MSI.Keyboard.Illuminator/obj/
+
+# Additional
+/binaries/
+*appsettings.xml
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator.sln b/MSI.Keyboard.Illuminator.sln
new file mode 100644
index 0000000..94a441c
--- /dev/null
+++ b/MSI.Keyboard.Illuminator.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.35013.160
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSI.Keyboard.Illuminator", "MSI.Keyboard.Illuminator\MSI.Keyboard.Illuminator.csproj", "{A3D78733-DFF8-4CB0-9F97-559342929D10}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A3D78733-DFF8-4CB0-9F97-559342929D10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A3D78733-DFF8-4CB0-9F97-559342929D10}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A3D78733-DFF8-4CB0-9F97-559342929D10}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A3D78733-DFF8-4CB0-9F97-559342929D10}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {32C97D6A-0418-44A3-8579-2E9F12DC261B}
+ EndGlobalSection
+EndGlobal
diff --git a/MSI.Keyboard.Illuminator/App.axaml b/MSI.Keyboard.Illuminator/App.axaml
new file mode 100644
index 0000000..bbb41bd
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/App.axaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/App.axaml.cs b/MSI.Keyboard.Illuminator/App.axaml.cs
new file mode 100644
index 0000000..a4a7213
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/App.axaml.cs
@@ -0,0 +1,116 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+using MSI.Keyboard.Illuminator.Helpers;
+using MSI.Keyboard.Illuminator.Providers;
+using MSI.Keyboard.Illuminator.Services;
+using MSI.Keyboard.Illuminator.ViewModels;
+
+using System;
+using System.CommandLine;
+using System.IO;
+
+namespace MSI.Keyboard.Illuminator;
+
+public partial class App : Application
+{
+ protected IAppSettingsManager appSettingsManager;
+
+ protected IKeyboardService keyboardService;
+
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime application)
+ {
+ application.ShutdownMode = ShutdownMode.OnExplicitShutdown;
+
+ keyboardService = GetKeyboardService();
+ WarnIfDeviceIsNotSupported();
+
+ appSettingsManager = GetAppSettingsManager(application.Args);
+ InitializeSettings();
+ application.Exit += (s, e) => FinalizeSettings();
+
+ var colorProfilesViewModel = new ColorProfilesViewModel(appSettingsManager);
+
+ DataContext = new TrayViewModel(
+ application,
+ colorProfilesViewModel,
+ keyboardService,
+ appSettingsManager);
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+ protected static IKeyboardService GetKeyboardService()
+ {
+ var keyboardDevice = new KeyboardDevice();
+
+ return new KeyboardService(keyboardDevice);
+ }
+
+ protected void WarnIfDeviceIsNotSupported()
+ {
+ if (keyboardService.IsDeviceSupported())
+ return;
+
+ WindowHelper.ShowMessageWindow(
+ "MSI keyboard not found!",
+ "The supported \"MSI EPF USB\" SteelSeries keyboard has not been found!" +
+ Environment.NewLine +
+ "Exit an application as it is is not going to work properly anyway.");
+ }
+
+ protected static IAppSettingsManager GetAppSettingsManager(params string[] args)
+ {
+ var fileOption = new Option(
+ name: "--settings",
+ getDefaultValue: () => new FileInfo("appsettings.xml"),
+ description: "A full path to the settings file.");
+
+ var settingsFile = fileOption.Parse(args).GetValueForOption(fileOption);
+
+ var appSettingsStreamer = new AppSettingsStreamer(settingsFile);
+
+ return new AppSettingsManager(appSettingsStreamer);
+ }
+
+ protected void InitializeSettings()
+ {
+ try
+ {
+ appSettingsManager.LoadSettings();
+ }
+ catch (FileNotFoundException)
+ {
+ // supress and use default settings
+ }
+ catch (Exception ex)
+ {
+ WindowHelper.ShowMessageWindow(
+ "Loading application settings failed!",
+ ex.Message);
+ }
+ }
+
+ protected void FinalizeSettings()
+ {
+ try
+ {
+ appSettingsManager.SaveSettings();
+ }
+ catch (Exception ex)
+ {
+ WindowHelper.ShowMessageWindow(
+ "Saving application settings failed!",
+ ex.Message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Assets/arrow-down16.png b/MSI.Keyboard.Illuminator/Assets/arrow-down16.png
new file mode 100644
index 0000000..416d81f
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/arrow-down16.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/arrow-down22.png b/MSI.Keyboard.Illuminator/Assets/arrow-down22.png
new file mode 100644
index 0000000..53204cb
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/arrow-down22.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/arrow-down32.png b/MSI.Keyboard.Illuminator/Assets/arrow-down32.png
new file mode 100644
index 0000000..81f686f
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/arrow-down32.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/arrow-up16.png b/MSI.Keyboard.Illuminator/Assets/arrow-up16.png
new file mode 100644
index 0000000..1a2561e
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/arrow-up16.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/arrow-up22.png b/MSI.Keyboard.Illuminator/Assets/arrow-up22.png
new file mode 100644
index 0000000..d61de14
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/arrow-up22.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/arrow-up32.png b/MSI.Keyboard.Illuminator/Assets/arrow-up32.png
new file mode 100644
index 0000000..617029a
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/arrow-up32.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/checked-mark16.png b/MSI.Keyboard.Illuminator/Assets/checked-mark16.png
new file mode 100644
index 0000000..9a2cd67
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/checked-mark16.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/checked-mark32.png b/MSI.Keyboard.Illuminator/Assets/checked-mark32.png
new file mode 100644
index 0000000..bdbc109
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/checked-mark32.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/color-palette16.png b/MSI.Keyboard.Illuminator/Assets/color-palette16.png
new file mode 100644
index 0000000..a08ff54
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/color-palette16.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/color-palette32.png b/MSI.Keyboard.Illuminator/Assets/color-palette32.png
new file mode 100644
index 0000000..3b80617
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/color-palette32.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/exit16.png b/MSI.Keyboard.Illuminator/Assets/exit16.png
new file mode 100644
index 0000000..1c16187
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/exit16.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/exit32.png b/MSI.Keyboard.Illuminator/Assets/exit32.png
new file mode 100644
index 0000000..b2ee9e3
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/exit32.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/logo.ico b/MSI.Keyboard.Illuminator/Assets/logo.ico
new file mode 100644
index 0000000..f3c35b9
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/logo.ico differ
diff --git a/MSI.Keyboard.Illuminator/Assets/minus16.png b/MSI.Keyboard.Illuminator/Assets/minus16.png
new file mode 100644
index 0000000..d240368
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/minus16.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/minus22.png b/MSI.Keyboard.Illuminator/Assets/minus22.png
new file mode 100644
index 0000000..ace24df
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/minus22.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/minus32.png b/MSI.Keyboard.Illuminator/Assets/minus32.png
new file mode 100644
index 0000000..9e34c80
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/minus32.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/palette16.png b/MSI.Keyboard.Illuminator/Assets/palette16.png
new file mode 100644
index 0000000..bcaeffe
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/palette16.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/palette32.png b/MSI.Keyboard.Illuminator/Assets/palette32.png
new file mode 100644
index 0000000..99879c1
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/palette32.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/plus16.png b/MSI.Keyboard.Illuminator/Assets/plus16.png
new file mode 100644
index 0000000..85597e9
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/plus16.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/plus22.png b/MSI.Keyboard.Illuminator/Assets/plus22.png
new file mode 100644
index 0000000..ac941e1
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/plus22.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/plus32.png b/MSI.Keyboard.Illuminator/Assets/plus32.png
new file mode 100644
index 0000000..8b0781e
Binary files /dev/null and b/MSI.Keyboard.Illuminator/Assets/plus32.png differ
diff --git a/MSI.Keyboard.Illuminator/Assets/svg/arrow-down.svg b/MSI.Keyboard.Illuminator/Assets/svg/arrow-down.svg
new file mode 100644
index 0000000..4a6dff6
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Assets/svg/arrow-down.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Assets/svg/arrow-up.svg b/MSI.Keyboard.Illuminator/Assets/svg/arrow-up.svg
new file mode 100644
index 0000000..f2948a9
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Assets/svg/arrow-up.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Assets/svg/checked-mark.svg b/MSI.Keyboard.Illuminator/Assets/svg/checked-mark.svg
new file mode 100644
index 0000000..9f8f1d0
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Assets/svg/checked-mark.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Assets/svg/color-palette.svg b/MSI.Keyboard.Illuminator/Assets/svg/color-palette.svg
new file mode 100644
index 0000000..a6ce8f6
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Assets/svg/color-palette.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Assets/svg/exit.svg b/MSI.Keyboard.Illuminator/Assets/svg/exit.svg
new file mode 100644
index 0000000..2d0c4ef
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Assets/svg/exit.svg
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Assets/svg/keyboard.svg b/MSI.Keyboard.Illuminator/Assets/svg/keyboard.svg
new file mode 100644
index 0000000..39e4f0e
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Assets/svg/keyboard.svg
@@ -0,0 +1,67 @@
+
+
+
+
diff --git a/MSI.Keyboard.Illuminator/Assets/svg/minus.svg b/MSI.Keyboard.Illuminator/Assets/svg/minus.svg
new file mode 100644
index 0000000..8764244
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Assets/svg/minus.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Assets/svg/palette.svg b/MSI.Keyboard.Illuminator/Assets/svg/palette.svg
new file mode 100644
index 0000000..7b32666
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Assets/svg/palette.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Assets/svg/plus.svg b/MSI.Keyboard.Illuminator/Assets/svg/plus.svg
new file mode 100644
index 0000000..80f9191
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Assets/svg/plus.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Helpers/AssetsHelper.cs b/MSI.Keyboard.Illuminator/Helpers/AssetsHelper.cs
new file mode 100644
index 0000000..3253d37
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Helpers/AssetsHelper.cs
@@ -0,0 +1,21 @@
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+
+using System;
+using System.IO;
+
+namespace MSI.Keyboard.Illuminator.Helpers;
+
+public static class AssetsHelper
+{
+ public static Bitmap GetImageFromAssets(string pathToImage)
+ {
+ var basePath = @"avares://MSI.Keyboard.Illuminator/Assets";
+ var fullPath = Path.Combine(basePath, pathToImage);
+ var uri = new Uri(fullPath);
+
+ using var assetStream = AssetLoader.Open(uri);
+
+ return new(assetStream);
+ }
+}
diff --git a/MSI.Keyboard.Illuminator/Helpers/ColorExtensions.cs b/MSI.Keyboard.Illuminator/Helpers/ColorExtensions.cs
new file mode 100644
index 0000000..2af42ae
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Helpers/ColorExtensions.cs
@@ -0,0 +1,10 @@
+namespace MSI.Keyboard.Illuminator.Helpers;
+
+public static class ColorExtensions
+{
+ public static Avalonia.Media.Color ToAvaloniaColor(this System.Drawing.Color color) =>
+ Avalonia.Media.Color.FromArgb(color.A, color.R, color.G, color.B);
+
+ public static System.Drawing.Color ToSystemColor(this Avalonia.Media.Color color) =>
+ System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B);
+}
diff --git a/MSI.Keyboard.Illuminator/Helpers/WindowHelper.cs b/MSI.Keyboard.Illuminator/Helpers/WindowHelper.cs
new file mode 100644
index 0000000..2916cd7
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Helpers/WindowHelper.cs
@@ -0,0 +1,45 @@
+using Avalonia.Controls;
+
+using MSI.Keyboard.Illuminator.ViewModels;
+using MSI.Keyboard.Illuminator.Views;
+
+namespace MSI.Keyboard.Illuminator.Helpers;
+
+public static class WindowHelper
+{
+ public static Window GetColorProfilesWindow(ColorProfilesViewModel colorProfilesViewModel)
+ {
+ var colorProfilesWindow = new ColorProfilesWindow()
+ {
+ DataContext = colorProfilesViewModel,
+ };
+
+ return colorProfilesWindow;
+ }
+
+ public static void ShowColorProfilesWindow(ColorProfilesViewModel colorProfilesViewModel)
+ {
+ var colorProfilesWindow = GetColorProfilesWindow(colorProfilesViewModel);
+
+ colorProfilesWindow.Show();
+ }
+
+ public static Window GetMessageWindow(string title, string message)
+ {
+ var messageViewModel = new MessageViewModel(title, message);
+
+ var window = new MessageWindow()
+ {
+ DataContext = messageViewModel,
+ };
+
+ return window;
+ }
+
+ public static void ShowMessageWindow(string title, string message)
+ {
+ var messageWindow = GetMessageWindow(title, message);
+
+ messageWindow.Show();
+ }
+}
diff --git a/MSI.Keyboard.Illuminator/MSI.Keyboard.Illuminator.csproj b/MSI.Keyboard.Illuminator/MSI.Keyboard.Illuminator.csproj
new file mode 100644
index 0000000..ad7d997
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/MSI.Keyboard.Illuminator.csproj
@@ -0,0 +1,38 @@
+
+
+ WinExe
+ net8.0
+ disable
+ true
+ app.manifest
+ true
+ Assets/logo.ico
+ true
+
+
+
+ full
+ true
+
+
+
+ none
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MSI.Keyboard.Illuminator/Models/AppSettings.cs b/MSI.Keyboard.Illuminator/Models/AppSettings.cs
new file mode 100644
index 0000000..427d978
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Models/AppSettings.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace MSI.Keyboard.Illuminator.Models;
+
+public class AppSettings
+{
+ public ColorProfile ActiveColorProfile { get; set; }
+
+ public List ColorProfiles { get; set; } = [];
+}
diff --git a/MSI.Keyboard.Illuminator/Models/AppSettingsStreamerOptions.cs b/MSI.Keyboard.Illuminator/Models/AppSettingsStreamerOptions.cs
new file mode 100644
index 0000000..3de99be
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Models/AppSettingsStreamerOptions.cs
@@ -0,0 +1,10 @@
+namespace MSI.Keyboard.Illuminator.Models;
+
+public class AppSettingsStreamerOptions(
+ string appSettingsFilePath,
+ bool isXmlIndented = true)
+{
+ public string AppSettingsFilePath { get; } = appSettingsFilePath;
+
+ public bool IsXmlIndented { get; } = isXmlIndented;
+}
diff --git a/MSI.Keyboard.Illuminator/Models/BlinkingMode.cs b/MSI.Keyboard.Illuminator/Models/BlinkingMode.cs
new file mode 100644
index 0000000..97f6c2e
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Models/BlinkingMode.cs
@@ -0,0 +1,10 @@
+namespace MSI.Keyboard.Illuminator.Models;
+
+public enum BlinkingMode
+{
+ Normal = 0x01,
+ Gaming = 0x02,
+ Breathe = 0x03,
+ Demo = 0x04,
+ Wave = 0x05,
+}
diff --git a/MSI.Keyboard.Illuminator/Models/ColorProfile.cs b/MSI.Keyboard.Illuminator/Models/ColorProfile.cs
new file mode 100644
index 0000000..078fe67
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Models/ColorProfile.cs
@@ -0,0 +1,111 @@
+using Avalonia.Media;
+
+using ReactiveUI;
+
+using System;
+
+namespace MSI.Keyboard.Illuminator.Models;
+
+public class ColorProfile : ReactiveObject, ICloneable
+{
+ protected string name;
+
+ public string Name
+ {
+ get => name;
+ set => this.RaiseAndSetIfChanged(ref name, value);
+ }
+
+ protected BlinkingMode blinkingMode;
+
+ public BlinkingMode BlinkingMode
+ {
+ get => blinkingMode;
+ set => this.RaiseAndSetIfChanged(ref blinkingMode, value);
+ }
+
+ protected Color leftColor;
+
+ public Color LeftColor
+ {
+ get => leftColor;
+ set => this.RaiseAndSetIfChanged(ref leftColor, value);
+ }
+
+ protected Color centerColor;
+
+ public Color CenterColor
+ {
+ get => centerColor;
+ set => this.RaiseAndSetIfChanged(ref centerColor, value);
+ }
+
+ protected Color rightColor;
+
+ public Color RightColor
+ {
+ get => rightColor;
+ set => this.RaiseAndSetIfChanged(ref rightColor, value);
+ }
+
+ public ColorProfile() { }
+
+ public ColorProfile(
+ string name,
+ BlinkingMode blinkingMode,
+ Color leftColor,
+ Color centerColor,
+ Color rightColor)
+ {
+ Name = name;
+ BlinkingMode = blinkingMode;
+ LeftColor = leftColor;
+ CenterColor = centerColor;
+ RightColor = rightColor;
+ }
+
+ public static ColorProfile GetDefault() => new(
+ "Default",
+ BlinkingMode.Normal,
+ new(255, 255, 0, 0),
+ new(255, 255, 0, 0),
+ new(255, 255, 0, 0)
+ );
+
+ public static bool operator ==(ColorProfile left, ColorProfile right)
+ {
+ if (left is null)
+ return right is null;
+
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(ColorProfile left, ColorProfile right) =>
+ !(left == right);
+
+ public object Clone() => new ColorProfile(
+ (string)Name.Clone(),
+ BlinkingMode,
+ LeftColor,
+ CenterColor,
+ RightColor);
+
+ public override bool Equals(object obj)
+ {
+ if (obj is not ColorProfile other)
+ return false;
+
+ return Name == other.Name
+ && BlinkingMode == other.BlinkingMode
+ && LeftColor == other.LeftColor
+ && CenterColor == other.CenterColor
+ && RightColor == other.RightColor;
+ }
+
+ public override int GetHashCode() => HashCode.Combine(
+ Name,
+ BlinkingMode,
+ LeftColor,
+ CenterColor,
+ RightColor);
+}
diff --git a/MSI.Keyboard.Illuminator/Models/IlluminationConfiguration.cs b/MSI.Keyboard.Illuminator/Models/IlluminationConfiguration.cs
new file mode 100644
index 0000000..d8636a7
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Models/IlluminationConfiguration.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Drawing;
+
+namespace MSI.Keyboard.Illuminator.Models;
+
+public class IlluminationConfiguration(
+ IReadOnlyDictionary regionColors,
+ BlinkingMode blinkingMode)
+{
+ public IReadOnlyDictionary RegionColors { get; } = regionColors;
+
+ public BlinkingMode BlinkingMode { get; } = blinkingMode;
+}
diff --git a/MSI.Keyboard.Illuminator/Models/KeyboardRegion.cs b/MSI.Keyboard.Illuminator/Models/KeyboardRegion.cs
new file mode 100644
index 0000000..4436527
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Models/KeyboardRegion.cs
@@ -0,0 +1,8 @@
+namespace MSI.Keyboard.Illuminator.Models;
+
+public enum KeyboardRegion
+{
+ Start = 1,
+ Center = 2,
+ End = 3,
+}
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Models/Region.cs b/MSI.Keyboard.Illuminator/Models/Region.cs
new file mode 100644
index 0000000..bd52296
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Models/Region.cs
@@ -0,0 +1,8 @@
+namespace MSI.Keyboard.Illuminator.Models;
+
+public enum Region
+{
+ Left = 0x01,
+ Center = 0x02,
+ Right = 0x03,
+}
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Program.cs b/MSI.Keyboard.Illuminator/Program.cs
new file mode 100644
index 0000000..f1745ac
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Program.cs
@@ -0,0 +1,24 @@
+using Avalonia;
+using Avalonia.ReactiveUI;
+
+using System;
+
+namespace MSI.Keyboard.Illuminator;
+
+sealed class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace()
+ .UseReactiveUI();
+}
diff --git a/MSI.Keyboard.Illuminator/Properties/launchSettings.json b/MSI.Keyboard.Illuminator/Properties/launchSettings.json
new file mode 100644
index 0000000..17b6309
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "MSI.Keyboard.Illuminator": {
+ "commandName": "Project",
+ "commandLineArgs": "--settings \"appsettings.xml\""
+ }
+ }
+}
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Providers/AppSettingsStreamer.cs b/MSI.Keyboard.Illuminator/Providers/AppSettingsStreamer.cs
new file mode 100644
index 0000000..959cd42
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Providers/AppSettingsStreamer.cs
@@ -0,0 +1,148 @@
+using Avalonia.Media;
+
+using MSI.Keyboard.Illuminator.Models;
+
+using System;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Linq;
+
+namespace MSI.Keyboard.Illuminator.Providers;
+
+public class AppSettingsStreamer(
+ FileInfo sourceFile) : IAppSettingsStreamer
+{
+ protected readonly FileInfo sourceFile = sourceFile;
+
+ protected readonly Encoding encoding = Encoding.UTF8;
+
+ public FileInfo GetSourceFile() => sourceFile;
+
+ public AppSettings LoadSettings()
+ {
+ using var stream = GetStreamReader();
+
+ var xml = XDocument.Load(stream, LoadOptions.None);
+
+ return GetAppSettings(xml.Root);
+ }
+
+ public async Task LoadSettingsAsync() =>
+ await LoadSettingsAsync(CancellationToken.None);
+
+ public async Task LoadSettingsAsync(CancellationToken cancellationToken)
+ {
+ using var stream = GetStreamReader();
+
+ var xml = await XDocument.LoadAsync(stream, LoadOptions.None, cancellationToken);
+
+ return GetAppSettings(xml.Root);
+ }
+
+ public void SaveSettings(AppSettings appSettings)
+ {
+ var xml = GetXDocument(appSettings);
+
+ using var writer = GetXmlTextWriter();
+
+ xml.Save(writer);
+ }
+
+ public async Task SaveSettingsAsync(AppSettings appSettings) =>
+ await SaveSettingsAsync(appSettings, CancellationToken.None);
+
+ public async Task SaveSettingsAsync(AppSettings appSettings, CancellationToken cancellationToken)
+ {
+ var xml = GetXDocument(appSettings);
+
+ using var writer = GetXmlTextWriter();
+
+ await xml.SaveAsync(writer, cancellationToken);
+ }
+
+ protected StreamReader GetStreamReader() => new(sourceFile.FullName, encoding);
+
+ protected XmlTextWriter GetXmlTextWriter() => new(sourceFile.FullName, encoding)
+ {
+ Formatting = Formatting.Indented,
+ };
+
+ protected static XElement GetXElement(Color color, string name) => new(
+ name,
+ new XElement(nameof(Color.A), color.A),
+ new XElement(nameof(Color.R), color.R),
+ new XElement(nameof(Color.G), color.G),
+ new XElement(nameof(Color.B), color.B));
+
+ protected static XElement GetXElement(ColorProfile colorProfile, string name = null) => new(
+ name ?? nameof(ColorProfile),
+ new XElement(nameof(ColorProfile.Name), colorProfile.Name),
+ new XElement(nameof(ColorProfile.BlinkingMode), colorProfile.BlinkingMode),
+ GetXElement(colorProfile.LeftColor, nameof(colorProfile.LeftColor)),
+ GetXElement(colorProfile.CenterColor, nameof(colorProfile.CenterColor)),
+ GetXElement(colorProfile.RightColor, nameof(colorProfile.RightColor)));
+
+ protected static XElement GetXElement(AppSettings appSettings)
+ {
+ var appSettingsElement = new XElement(nameof(AppSettings));
+
+ if (appSettings.ActiveColorProfile != null)
+ {
+ var activeColorProfileElement = GetXElement(
+ appSettings.ActiveColorProfile,
+ nameof(appSettings.ActiveColorProfile));
+
+ appSettingsElement.Add(activeColorProfileElement);
+ }
+
+ var colorProfilesElement = new XElement(nameof(AppSettings.ColorProfiles));
+
+ foreach (var colorProfile in appSettings.ColorProfiles)
+ colorProfilesElement.Add(GetXElement(colorProfile));
+
+ appSettingsElement.Add(colorProfilesElement);
+
+ return appSettingsElement;
+ }
+
+ protected static XDocument GetXDocument(AppSettings appSettings) => new(
+ new XDeclaration("1.0", "utf-8", "yes"),
+ GetXElement(appSettings));
+
+ protected static Color GetColor(XElement element) => new(
+ byte.Parse(element.Element(nameof(Color.A)).Value),
+ byte.Parse(element.Element(nameof(Color.R)).Value),
+ byte.Parse(element.Element(nameof(Color.G)).Value),
+ byte.Parse(element.Element(nameof(Color.B)).Value));
+
+ protected static ColorProfile GetColorProfile(XElement element) => new(
+ element.Element(nameof(ColorProfile.Name)).Value,
+ Enum.Parse(
+ element.Element(nameof(ColorProfile.BlinkingMode)).Value),
+ GetColor(element.Element(nameof(ColorProfile.LeftColor))),
+ GetColor(element.Element(nameof(ColorProfile.CenterColor))),
+ GetColor(element.Element(nameof(ColorProfile.RightColor))));
+
+ protected static AppSettings GetAppSettings(XElement element)
+ {
+ var appSettings = new AppSettings();
+
+ var activeColorProfileElement = element.Element(nameof(AppSettings.ActiveColorProfile));
+
+ if (activeColorProfileElement != null)
+ appSettings.ActiveColorProfile = GetColorProfile(activeColorProfileElement);
+
+
+ var colorProfilesElements = element
+ .Element(nameof(AppSettings.ColorProfiles))
+ .Elements();
+
+ foreach (var colorProfileElement in colorProfilesElements)
+ appSettings.ColorProfiles.Add(GetColorProfile(colorProfileElement));
+
+ return appSettings;
+ }
+}
diff --git a/MSI.Keyboard.Illuminator/Providers/IAppSettingsStreamer.cs b/MSI.Keyboard.Illuminator/Providers/IAppSettingsStreamer.cs
new file mode 100644
index 0000000..3590477
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Providers/IAppSettingsStreamer.cs
@@ -0,0 +1,24 @@
+using MSI.Keyboard.Illuminator.Models;
+
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MSI.Keyboard.Illuminator.Providers;
+
+public interface IAppSettingsStreamer
+{
+ public FileInfo GetSourceFile();
+
+ AppSettings LoadSettings();
+
+ Task LoadSettingsAsync();
+
+ Task LoadSettingsAsync(CancellationToken cancellationToken);
+
+ void SaveSettings(AppSettings appSettings);
+
+ Task SaveSettingsAsync(AppSettings appSettings);
+
+ Task SaveSettingsAsync(AppSettings appSettings, CancellationToken cancellationToken);
+}
diff --git a/MSI.Keyboard.Illuminator/Providers/IIlluminationConfigurationBuilder.cs b/MSI.Keyboard.Illuminator/Providers/IIlluminationConfigurationBuilder.cs
new file mode 100644
index 0000000..6651726
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Providers/IIlluminationConfigurationBuilder.cs
@@ -0,0 +1,16 @@
+using MSI.Keyboard.Illuminator.Models;
+
+using System.Drawing;
+
+namespace MSI.Keyboard.Illuminator.Providers;
+
+public interface IIlluminationConfigurationBuilder
+{
+ IIlluminationConfigurationBuilder ForAllRegions(BlinkingMode blinkingMode);
+
+ IIlluminationConfigurationBuilder ForAllRegions(Color color);
+
+ IIlluminationConfigurationBuilder ForRegion(Region region, Color color);
+
+ IlluminationConfiguration Build();
+}
diff --git a/MSI.Keyboard.Illuminator/Providers/IlluminationConfigurationBuilder.cs b/MSI.Keyboard.Illuminator/Providers/IlluminationConfigurationBuilder.cs
new file mode 100644
index 0000000..8937954
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Providers/IlluminationConfigurationBuilder.cs
@@ -0,0 +1,52 @@
+using MSI.Keyboard.Illuminator.Models;
+
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+
+namespace MSI.Keyboard.Illuminator.Providers;
+
+public class IlluminationConfigurationBuilder : IIlluminationConfigurationBuilder
+{
+ private readonly Dictionary regionColors;
+
+ private BlinkingMode blinkingMode;
+
+ public IlluminationConfigurationBuilder()
+ {
+ regionColors = new Dictionary()
+ {
+ { Region.Left, Color.Red },
+ { Region.Center, Color.Red },
+ { Region.Right, Color.Red },
+ };
+
+ blinkingMode = BlinkingMode.Normal;
+ }
+
+ public IIlluminationConfigurationBuilder ForAllRegions(BlinkingMode blinkingMode)
+ {
+ this.blinkingMode = blinkingMode;
+
+ return this;
+ }
+
+ public IIlluminationConfigurationBuilder ForAllRegions(Color color)
+ {
+ foreach (var region in regionColors.Keys.ToList())
+ {
+ ForRegion(region, color);
+ }
+
+ return this;
+ }
+
+ public IIlluminationConfigurationBuilder ForRegion(Region region, Color color)
+ {
+ regionColors[region] = color;
+
+ return this;
+ }
+
+ public IlluminationConfiguration Build() => new(regionColors, blinkingMode);
+}
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Providers/IlluminationConfigurationFactory.cs b/MSI.Keyboard.Illuminator/Providers/IlluminationConfigurationFactory.cs
new file mode 100644
index 0000000..b8d761f
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Providers/IlluminationConfigurationFactory.cs
@@ -0,0 +1,24 @@
+using MSI.Keyboard.Illuminator.Helpers;
+using MSI.Keyboard.Illuminator.Models;
+
+using System;
+
+namespace MSI.Keyboard.Illuminator.Providers;
+
+public static class IlluminationConfigurationFactory
+{
+ public static IlluminationConfiguration GetIlluminationConfiguration(ColorProfile colorProfile)
+ {
+ ArgumentNullException.ThrowIfNull(colorProfile);
+
+ var configuration = new IlluminationConfigurationBuilder()
+ .ForAllRegions(colorProfile.BlinkingMode)
+ .ForRegion(Region.Left, colorProfile.LeftColor.ToSystemColor())
+ .ForRegion(Region.Center, colorProfile.CenterColor.ToSystemColor())
+ .ForRegion(Region.Right, colorProfile.RightColor.ToSystemColor())
+ .Build();
+
+ return configuration;
+ }
+
+}
diff --git a/MSI.Keyboard.Illuminator/Services/AppSettingsManager.cs b/MSI.Keyboard.Illuminator/Services/AppSettingsManager.cs
new file mode 100644
index 0000000..8aed024
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Services/AppSettingsManager.cs
@@ -0,0 +1,37 @@
+using MSI.Keyboard.Illuminator.Models;
+using MSI.Keyboard.Illuminator.Providers;
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+
+namespace MSI.Keyboard.Illuminator.Services;
+
+public class AppSettingsManager(
+ AppSettingsStreamer appSettingsStreamer) : IAppSettingsManager
+{
+ protected AppSettingsStreamer appSettingsStreamer = appSettingsStreamer;
+
+ protected AppSettings appSettings = new();
+
+ public ColorProfile GetActiveColorProfile() =>
+ appSettings.ActiveColorProfile?.Clone() as ColorProfile;
+
+ public void UpdateActiveColorProfile(ColorProfile colorProfile) =>
+ appSettings.ActiveColorProfile = colorProfile?.Clone() as ColorProfile;
+
+ public List GetColorProfiles() =>
+ GetDeepCopy(appSettings.ColorProfiles);
+
+ public void UpdateColorProfiles(IEnumerable colorProfiles) =>
+ appSettings.ColorProfiles = GetDeepCopy(colorProfiles);
+
+ public void LoadSettings() =>
+ appSettings = appSettingsStreamer.LoadSettings();
+
+ public void SaveSettings() =>
+ appSettingsStreamer.SaveSettings(appSettings);
+
+ protected static List GetDeepCopy(IEnumerable colorProfiles) =>
+ colorProfiles.Select(s => s?.Clone() as ColorProfile).ToList();
+}
diff --git a/MSI.Keyboard.Illuminator/Services/IAppSettingsManager.cs b/MSI.Keyboard.Illuminator/Services/IAppSettingsManager.cs
new file mode 100644
index 0000000..06f82b8
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Services/IAppSettingsManager.cs
@@ -0,0 +1,20 @@
+using MSI.Keyboard.Illuminator.Models;
+
+using System.Collections.Generic;
+
+namespace MSI.Keyboard.Illuminator.Services;
+
+public interface IAppSettingsManager
+{
+ ColorProfile GetActiveColorProfile();
+
+ void UpdateActiveColorProfile(ColorProfile colorProfile);
+
+ List GetColorProfiles();
+
+ void UpdateColorProfiles(IEnumerable colorProfiles);
+
+ void LoadSettings();
+
+ void SaveSettings();
+}
diff --git a/MSI.Keyboard.Illuminator/Services/IKeyboardDevice.cs b/MSI.Keyboard.Illuminator/Services/IKeyboardDevice.cs
new file mode 100644
index 0000000..289ca9a
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Services/IKeyboardDevice.cs
@@ -0,0 +1,31 @@
+using MSI.Keyboard.Illuminator.Models;
+
+using System.Drawing;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MSI.Keyboard.Illuminator.Services;
+
+public interface IKeyboardDevice
+{
+ ///
+ /// Changes the color of a keyboard for specific region.
+ ///
+ /// The region of a keyboard.
+ /// The color of a region's buttons.
+ /// Thrown when keyboard device has not been found.
+ Task ChangeColorAsync(Region region, Color color);
+
+ ///
+ /// Checks if the current device is supported.
+ ///
+ /// true if a device is supported, otherwise false.
+ bool IsDeviceSupported();
+
+ ///
+ /// Sets the mode of keyboard's leds blinking.
+ ///
+ /// The specific blinking effect.
+ /// Thrown when keyboard device has not been found.
+ Task ChangeModeAsync(BlinkingMode blinkingMode);
+}
diff --git a/MSI.Keyboard.Illuminator/Services/IKeyboardService.cs b/MSI.Keyboard.Illuminator/Services/IKeyboardService.cs
new file mode 100644
index 0000000..a9f9c7b
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Services/IKeyboardService.cs
@@ -0,0 +1,29 @@
+using MSI.Keyboard.Illuminator.Models;
+
+using System;
+using System.Threading.Tasks;
+
+namespace MSI.Keyboard.Illuminator.Services;
+
+public interface IKeyboardService
+{
+ ///
+ /// Gets the current illumination configuration.
+ ///
+ /// An instance of object.
+ IlluminationConfiguration GetCurrentConfiguration();
+
+ ///
+ /// Applies an illumination configuration.
+ ///
+ /// An illumination configuration.
+ /// Thrown when keyboard device not found.
+ /// If unrecognized IO exception occured.
+ Task ApplyConfigurationAsync(IlluminationConfiguration configuration);
+
+ ///
+ /// Checks if the current device is supported.
+ ///
+ /// true if a device is supported, otherwise false.
+ bool IsDeviceSupported();
+}
diff --git a/MSI.Keyboard.Illuminator/Services/KeyboardDevice.cs b/MSI.Keyboard.Illuminator/Services/KeyboardDevice.cs
new file mode 100644
index 0000000..0ac5a46
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Services/KeyboardDevice.cs
@@ -0,0 +1,70 @@
+using HidSharp;
+
+using MSI.Keyboard.Illuminator.Models;
+
+using System;
+using System.Drawing;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MSI.Keyboard.Illuminator.Services;
+
+public class KeyboardDevice : IKeyboardDevice
+{
+ protected const int VendorId = 6000;
+
+ protected const int ProductId = 65280;
+
+ public async Task ChangeColorAsync(Region region, Color color)
+ {
+ var intensity = (int)Math.Round(100.0 / 255 * color.A, MidpointRounding.AwayFromZero);
+
+ var colorFragmentValue = new Func(c => (byte)(c * (intensity / 100d)));
+
+ var data = new byte[]
+ {
+ 1,
+ 2,
+ 64,
+ (byte)region,
+ colorFragmentValue(color.R),
+ colorFragmentValue(color.G),
+ colorFragmentValue(color.B),
+ 0,
+ };
+
+ await SetFeature(data);
+ }
+
+ public async Task ChangeModeAsync(BlinkingMode blinkingMode)
+ {
+ var data = new byte[]
+ {
+ 1,
+ 2,
+ 65,
+ (byte)blinkingMode,
+ 0,
+ 0,
+ 0,
+ 236,
+ };
+
+ await SetFeature(data);
+ }
+
+ public bool IsDeviceSupported() =>
+ DeviceList.Local.TryGetHidDevice(out _, VendorId, ProductId);
+
+ protected static Task SetFeature(byte[] data)
+ {
+ var device = DeviceList.Local
+ .GetHidDeviceOrNull(VendorId, ProductId)
+ ?? throw new IOException("MSI keyboard has not been found!");
+
+ using var stream = device.Open();
+ stream.SetFeature(data);
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/MSI.Keyboard.Illuminator/Services/KeyboardService.cs b/MSI.Keyboard.Illuminator/Services/KeyboardService.cs
new file mode 100644
index 0000000..ac74520
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Services/KeyboardService.cs
@@ -0,0 +1,39 @@
+using MSI.Keyboard.Illuminator.Models;
+
+using System.Collections.Generic;
+using System.Drawing;
+using System.Threading.Tasks;
+
+namespace MSI.Keyboard.Illuminator.Services;
+
+public class KeyboardService(IKeyboardDevice keyboardDevice) : IKeyboardService
+{
+ private readonly IKeyboardDevice keyboardDevice = keyboardDevice;
+
+ private IlluminationConfiguration configuration;
+
+ public async Task ApplyConfigurationAsync(IlluminationConfiguration configuration)
+ {
+ this.configuration = configuration;
+
+ await ApplyBlinkingModeAsync(this.configuration.BlinkingMode);
+ await ApplyRegionColors(this.configuration.RegionColors);
+ }
+
+ public bool IsDeviceSupported() => keyboardDevice.IsDeviceSupported();
+
+ public IlluminationConfiguration GetCurrentConfiguration() => configuration;
+
+ protected async Task ApplyRegionColors(IReadOnlyDictionary regionColors)
+ {
+ foreach (var regionColor in regionColors)
+ {
+ await keyboardDevice.ChangeColorAsync(
+ regionColor.Key,
+ regionColor.Value);
+ }
+ }
+
+ protected async Task ApplyBlinkingModeAsync(BlinkingMode blinkingMode) =>
+ await keyboardDevice.ChangeModeAsync(blinkingMode);
+}
diff --git a/MSI.Keyboard.Illuminator/ViewModels/ColorProfileViewModel.cs b/MSI.Keyboard.Illuminator/ViewModels/ColorProfileViewModel.cs
new file mode 100644
index 0000000..8375ade
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/ViewModels/ColorProfileViewModel.cs
@@ -0,0 +1,17 @@
+using MSI.Keyboard.Illuminator.Models;
+
+using ReactiveUI;
+
+namespace MSI.Keyboard.Illuminator.ViewModels;
+
+public class ColorProfileViewModel(ColorProfile colorProfile) : ReactiveObject
+{
+ public static BlinkingMode[] BlinkingModes => [
+ BlinkingMode.Normal,
+ BlinkingMode.Gaming,
+ BlinkingMode.Breathe,
+ BlinkingMode.Demo,
+ BlinkingMode.Wave, ];
+
+ public ColorProfile ColorProfile { get; } = colorProfile;
+}
diff --git a/MSI.Keyboard.Illuminator/ViewModels/ColorProfilesViewModel.cs b/MSI.Keyboard.Illuminator/ViewModels/ColorProfilesViewModel.cs
new file mode 100644
index 0000000..9eeb84e
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/ViewModels/ColorProfilesViewModel.cs
@@ -0,0 +1,124 @@
+using Avalonia.Controls.Selection;
+
+using MSI.Keyboard.Illuminator.Models;
+using MSI.Keyboard.Illuminator.Services;
+
+using ReactiveUI;
+
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+
+namespace MSI.Keyboard.Illuminator.ViewModels;
+
+public class ColorProfilesViewModel : ReactiveObject
+{
+ protected readonly IAppSettingsManager appSettingsManager;
+
+ protected ObservableCollection colorProfileViewModels;
+
+ public ObservableCollection ColorProfileViewModels
+ {
+ get => colorProfileViewModels;
+ set => this.RaiseAndSetIfChanged(ref colorProfileViewModels, value);
+ }
+
+ protected SelectionModel selection;
+
+ public SelectionModel Selection
+ {
+ get => selection;
+ set => this.RaiseAndSetIfChanged(ref selection, value);
+ }
+
+ public ReactiveCommand Save { get; }
+
+ public ReactiveCommand Cancel { get; }
+
+ public ReactiveCommand AddNewColorProfile { get; }
+
+ public ReactiveCommand RemoveSelectedColorProfile { get; }
+
+ public ReactiveCommand MoveSelectedColorProfileUp { get; }
+
+ public ReactiveCommand MoveSelectedColorProfileDown { get; }
+
+ public ColorProfilesViewModel(
+ IAppSettingsManager appSettingsManager)
+ {
+ this.appSettingsManager = appSettingsManager;
+
+ Selection = new();
+
+ LoadColorProfiles();
+
+ Save = ReactiveCommand.Create(() =>
+ {
+ appSettingsManager.UpdateColorProfiles(
+ ColorProfileViewModels.Select(s => s.ColorProfile).Distinct());
+ });
+
+ Cancel = ReactiveCommand.Create(LoadColorProfiles);
+
+ AddNewColorProfile = ReactiveCommand.Create(() =>
+ {
+ var newColorProfile = ColorProfile.GetDefault();
+ newColorProfile.Name = $"Profile {ColorProfileViewModels?.Count + 1 ?? 1}";
+
+ ColorProfileViewModels.Add(new ColorProfileViewModel(newColorProfile));
+ });
+
+ var canRemoveSelectedColorProfile = this
+ .WhenAnyValue(s => s.Selection.SelectedItem)
+ .Select(s => s != null);
+
+ RemoveSelectedColorProfile = ReactiveCommand.Create(() =>
+ {
+ var idx = Selection.SelectedIndex;
+ ColorProfileViewModels.Remove(Selection.SelectedItem);
+
+ idx = ColorProfileViewModels.Count > idx ? idx : ColorProfileViewModels.Count - 1;
+
+ selection.SelectedIndex = idx;
+
+ }, canRemoveSelectedColorProfile);
+
+ void MoveSelectedColorProfile(bool moveUp)
+ {
+ var oldIdx = Selection.SelectedIndex;
+ var newIdx = Selection.SelectedIndex + (moveUp ? -1 : 1);
+
+ ColorProfileViewModels.Move(oldIdx, newIdx);
+ Selection.SelectedIndex = newIdx;
+ }
+
+ var canMoveSelectedColorProfileUp = this
+ .WhenAnyValue(s => s.Selection.SelectedIndex)
+ .Select(s => s > 0);
+
+ MoveSelectedColorProfileUp = ReactiveCommand.Create(
+ () => MoveSelectedColorProfile(true),
+ canMoveSelectedColorProfileUp);
+
+ var canMoveSelectedColorProfileDown = this
+ .WhenAnyValue(s => s.Selection.SelectedIndex)
+ .Select(s => s >= 0 && s < ColorProfileViewModels.Count - 1);
+
+ MoveSelectedColorProfileDown = ReactiveCommand.Create(
+ () => MoveSelectedColorProfile(false),
+ canMoveSelectedColorProfileDown);
+ }
+
+ protected void LoadColorProfiles()
+ {
+ var colorProfiles = appSettingsManager.GetColorProfiles()
+ .Select(cp => new ColorProfileViewModel(cp))
+ .ToList();
+
+ ColorProfileViewModels = new ObservableCollection(colorProfiles);
+
+ if (ColorProfileViewModels.Any())
+ Selection.SelectedIndex = 0;
+ }
+}
diff --git a/MSI.Keyboard.Illuminator/ViewModels/MessageViewModel.cs b/MSI.Keyboard.Illuminator/ViewModels/MessageViewModel.cs
new file mode 100644
index 0000000..c85bc0b
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/ViewModels/MessageViewModel.cs
@@ -0,0 +1,36 @@
+using Avalonia.Controls;
+
+using ReactiveUI;
+
+using System.Reactive;
+
+namespace MSI.Keyboard.Illuminator.ViewModels;
+
+public class MessageViewModel : ReactiveObject
+{
+ protected string title;
+
+ public string Title
+ {
+ get => title;
+ set => this.RaiseAndSetIfChanged(ref title, value);
+ }
+
+ protected string message;
+
+ public string Message
+ {
+ get => message;
+ set => this.RaiseAndSetIfChanged(ref message, value);
+ }
+
+ public ReactiveCommand Close { get; }
+
+ public MessageViewModel(string title, string message)
+ {
+ Title = title;
+ Message = message;
+
+ Close = ReactiveCommand.Create(window => window?.Close());
+ }
+}
diff --git a/MSI.Keyboard.Illuminator/ViewModels/TrayViewModel.cs b/MSI.Keyboard.Illuminator/ViewModels/TrayViewModel.cs
new file mode 100644
index 0000000..295c3fb
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/ViewModels/TrayViewModel.cs
@@ -0,0 +1,163 @@
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+
+using MSI.Keyboard.Illuminator.Helpers;
+using MSI.Keyboard.Illuminator.Models;
+using MSI.Keyboard.Illuminator.Providers;
+using MSI.Keyboard.Illuminator.Services;
+using MSI.Keyboard.Illuminator.Views;
+
+using ReactiveUI;
+
+using System;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+
+namespace MSI.Keyboard.Illuminator.ViewModels;
+
+public class TrayViewModel : ReactiveObject
+{
+ protected readonly IKeyboardService keyboardService;
+
+ protected readonly IAppSettingsManager appSettingsManager;
+
+ public ColorProfilesViewModel ColorProfilesViewModel { get; }
+
+
+ protected NativeMenu trayMenu;
+
+ public NativeMenu TrayMenu
+ {
+ get => trayMenu;
+ set => this.RaiseAndSetIfChanged(ref trayMenu, value);
+ }
+
+ public ReactiveCommand SelectColorProfile { get; }
+
+ public ReactiveCommand ShowColorProfiles { get; }
+
+ public ReactiveCommand Exit { get; }
+
+ public TrayViewModel(
+ IClassicDesktopStyleApplicationLifetime application,
+ ColorProfilesViewModel colorProfilesViewModel,
+ IKeyboardService keyboardService,
+ IAppSettingsManager appSettingsManager)
+ {
+ ColorProfilesViewModel = colorProfilesViewModel;
+ this.keyboardService = keyboardService;
+ this.appSettingsManager = appSettingsManager;
+
+ SelectColorProfile = ReactiveCommand.CreateFromTask(async colorProfile =>
+ {
+ var configuration = IlluminationConfigurationFactory
+ .GetIlluminationConfiguration(colorProfile);
+
+ await keyboardService.ApplyConfigurationAsync(configuration);
+ appSettingsManager.UpdateActiveColorProfile(colorProfile);
+
+ SelectColorProfileOnTrayMenu(colorProfile);
+ });
+
+ SelectColorProfile.ThrownExceptions.Subscribe(ex =>
+ {
+ WindowHelper.ShowMessageWindow(
+ "Changing keyboard colors failed!",
+ ex.Message);
+ });
+
+ ShowColorProfiles = ReactiveCommand.Create(() =>
+ {
+ if (application.Windows.Any(w => w is ColorProfilesWindow))
+ return;
+
+ WindowHelper.ShowColorProfilesWindow(colorProfilesViewModel);
+ });
+
+ Exit = ReactiveCommand.Create(() => application.Shutdown(0));
+
+ ColorProfilesViewModel.Save.Subscribe(x =>
+ {
+ GenerateTrayMenu();
+ SelectColorProfileOnTrayMenu(appSettingsManager.GetActiveColorProfile());
+
+ CloseAllColorProfilesWindows(application);
+ });
+
+ ColorProfilesViewModel.Cancel.Subscribe(x =>
+ CloseAllColorProfilesWindows(application));
+
+ GenerateTrayMenu();
+
+ var activeColorProfile = appSettingsManager.GetActiveColorProfile();
+ if (activeColorProfile != null)
+ SelectColorProfile.Execute(activeColorProfile).Subscribe();
+ }
+
+ ///
+ /// Generates tray menu.
+ ///
+ protected void GenerateTrayMenu()
+ {
+ var newTrayMenu = new NativeMenu();
+
+ foreach (var colorProfile in appSettingsManager.GetColorProfiles())
+ {
+ newTrayMenu.Add(new NativeMenuItem()
+ {
+ Header = colorProfile.Name,
+ Command = SelectColorProfile,
+ CommandParameter = colorProfile,
+ });
+ }
+
+ if (newTrayMenu.Items.Any())
+ newTrayMenu.Add(new NativeMenuItemSeparator());
+
+ newTrayMenu.Add(new NativeMenuItem()
+ {
+ Header = "Profiles",
+ Command = ShowColorProfiles,
+ Icon = AssetsHelper.GetImageFromAssets("palette16.png"),
+ });
+ newTrayMenu.Add(new NativeMenuItemSeparator());
+ newTrayMenu.Add(new NativeMenuItem()
+ {
+ Header = "Exit",
+ Command = Exit,
+ Icon = AssetsHelper.GetImageFromAssets("exit16.png"),
+ });
+
+ TrayMenu = newTrayMenu;
+ }
+
+ ///
+ /// Selects a given on tray menu, if it is present there.
+ ///
+ /// A color profile to be selected.
+ protected void SelectColorProfileOnTrayMenu(ColorProfile colorProfile)
+ {
+ foreach (var item in TrayMenu.Items.SkipLast(4).Cast())
+ {
+ if (item == null) continue;
+
+ item.Icon = item.CommandParameter as ColorProfile == colorProfile
+ ? AssetsHelper.GetImageFromAssets("checked-mark16.png")
+ : null;
+ }
+ }
+
+ ///
+ /// Closes all windows.
+ ///
+ protected static void CloseAllColorProfilesWindows(
+ IClassicDesktopStyleApplicationLifetime application)
+ {
+ for (int i = application.Windows.Count - 1; i >= 0; i--)
+ {
+ var colorProfilesWindow = application.Windows[i] as ColorProfilesWindow;
+ colorProfilesWindow?.Close();
+ }
+ }
+}
diff --git a/MSI.Keyboard.Illuminator/Views/ColorProfileView.axaml b/MSI.Keyboard.Illuminator/Views/ColorProfileView.axaml
new file mode 100644
index 0000000..44bafd6
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Views/ColorProfileView.axaml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MSI.Keyboard.Illuminator/Views/ColorProfileView.axaml.cs b/MSI.Keyboard.Illuminator/Views/ColorProfileView.axaml.cs
new file mode 100644
index 0000000..9365eba
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Views/ColorProfileView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace MSI.Keyboard.Illuminator.Views;
+
+public partial class ColorProfileView : UserControl
+{
+ public ColorProfileView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Views/ColorProfilesView.axaml b/MSI.Keyboard.Illuminator/Views/ColorProfilesView.axaml
new file mode 100644
index 0000000..4619c5b
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Views/ColorProfilesView.axaml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+ WhiteSmoke
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MSI.Keyboard.Illuminator/Views/ColorProfilesView.axaml.cs b/MSI.Keyboard.Illuminator/Views/ColorProfilesView.axaml.cs
new file mode 100644
index 0000000..2236ac2
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Views/ColorProfilesView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace MSI.Keyboard.Illuminator.Views;
+
+public partial class ColorProfilesView : UserControl
+{
+ public ColorProfilesView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Views/ColorProfilesWindow.axaml b/MSI.Keyboard.Illuminator/Views/ColorProfilesWindow.axaml
new file mode 100644
index 0000000..4918cb2
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Views/ColorProfilesWindow.axaml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/MSI.Keyboard.Illuminator/Views/ColorProfilesWindow.axaml.cs b/MSI.Keyboard.Illuminator/Views/ColorProfilesWindow.axaml.cs
new file mode 100644
index 0000000..b51999a
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Views/ColorProfilesWindow.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace MSI.Keyboard.Illuminator.Views;
+
+public partial class ColorProfilesWindow : Window
+{
+ public ColorProfilesWindow()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/Views/MessageWindow.axaml b/MSI.Keyboard.Illuminator/Views/MessageWindow.axaml
new file mode 100644
index 0000000..bf0ba19
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Views/MessageWindow.axaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MSI.Keyboard.Illuminator/Views/MessageWindow.axaml.cs b/MSI.Keyboard.Illuminator/Views/MessageWindow.axaml.cs
new file mode 100644
index 0000000..96f289d
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/Views/MessageWindow.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace MSI.Keyboard.Illuminator.Views;
+
+public partial class MessageWindow : Window
+{
+ public MessageWindow()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/MSI.Keyboard.Illuminator/app.manifest b/MSI.Keyboard.Illuminator/app.manifest
new file mode 100644
index 0000000..5494c6e
--- /dev/null
+++ b/MSI.Keyboard.Illuminator/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 46a1051..1007d28 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,16 @@
# MSI.Keyboard.Illuminator
-An application for changing colors of SteelSeries keyboard in MSI laptop. An application for changing colors of SteelSeries keyboard in MSI laptop. An application for changing colors of SteelSeries keyboard in MSI laptop.
+An application for changing colors of the "MSI EPF USB" SteelSeries keyboard mounted to several MSI gaming laptops.
+
+## Features
+ - changing colors of the left, center and right sections for the keyboard
+ - defining and managing color profiles
+ - quickly and conveniently selecting a color profile from the tray menu
+
+## Command line arguments
+| Argument name | Description | Default value |
+| :---: | :---: | :---: |
+| settings | Full path to settings file (witch all color profiles, etc.). | appsettings.xml |
+
+## Credits
+This project is partially based on [msi-keyboard-backlight](https://github.com/dpozimski/msi-keyboard-backlight) library.\
+This project uses icons from [SVG Repo](https://www.svgrepo.com).
\ No newline at end of file