From cf70805208b324a62606cfd6830184464f21aecb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 1 Oct 2026 10:04:55 +0000
Subject: [PATCH 1/3] Initial plan
From 9b190964a36763d7d419498ca5ccb1f34a71dd17 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 1 Oct 2026 10:07:03 +0000
Subject: [PATCH 2/3] Add key alias regression tests
Co-authored-by: matt-edmondson <19528727+matt-edmondson@users.noreply.github.com>
---
.editorconfig | 4 +-
.gitignore | 18 +++++++
.../ModifierAliasNormalizationTests.cs | 54 +++++++++++++++++++
3 files changed, 74 insertions(+), 2 deletions(-)
diff --git a/.editorconfig b/.editorconfig
index 92356f0..fb1feb2 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -19,7 +19,7 @@ trim_trailing_whitespace = true
[*.{cs,csx,cake,fs,fsx,vb,vbx}]
indent_style = tab
-file_header_template = Copyright (c) ktsu.dev\nAll rights reserved.\nLicensed under the MIT license.
+file_header_template = Copyright (c) 2023-2026 ktsu-dev contributors
# Default severity for all .NET Code Style rules
dotnet_analyzer_diagnostic.severity = error
@@ -382,7 +382,7 @@ dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
# C++ files
-[*.cpp,*.h,*.hpp,*.cc,*.hh,*.cxx,*.hxx]
+[*.{cpp,h,hpp,cc,hh,cxx,hxx}]
indent_style = tab
# Naming convention rules (note: currently need to be ordered from more to less specific)
diff --git a/.gitignore b/.gitignore
index dc0470a..e043c9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -203,6 +203,11 @@ PublishScripts/
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
+# and except a Unity project's Packages/, which is source: Unity's package manifest and its
+# resolved lock file are both meant to be committed, and a NuGet restore folder never contains
+# a file by either name.
+!**/[Pp]ackages/manifest.json
+!**/[Pp]ackages/packages-lock.json
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
@@ -651,3 +656,16 @@ Temporary Items
# ImGui.ini files
imgui.ini
+
+# Game engine projects
+#
+# Godot: the import cache, and the mono/temp bin+obj a C# build writes.
+.godot/
+
+# Unity: .meta files are source, not the Visual Studio C++ build artifact that the `*.meta` rule
+# further up targets. Unity generates one per asset and it carries the GUID that scenes, prefabs
+# and serialized references point at, so ignoring them gives every clone fresh GUIDs and silently
+# breaks those references - including for a plug-in whose .dll is itself a build output. This
+# negation has to come after that rule to win, and is scoped to the asset tree so the Visual
+# Studio artifact stays ignored everywhere else.
+!**/[Aa]ssets/**/*.meta
diff --git a/Keybinding.Test/ModifierAliasNormalizationTests.cs b/Keybinding.Test/ModifierAliasNormalizationTests.cs
index bdd46f0..449fd09 100644
--- a/Keybinding.Test/ModifierAliasNormalizationTests.cs
+++ b/Keybinding.Test/ModifierAliasNormalizationTests.cs
@@ -66,6 +66,44 @@ public void EveryConstructionPath_NormalizesModifierAliases(string alias, string
}
}
+ [TestMethod]
+ [DataRow("Esc", "Escape")]
+ [DataRow("Return", "Enter")]
+ [DataRow("Del", "Delete")]
+ [DataRow("Ins", "Insert")]
+ [DataRow("PgUp", "PageUp")]
+ [DataRow("PgDn", "PageDown")]
+ [DataRow("Up", "ArrowUp")]
+ [DataRow("UpArrow", "ArrowUp")]
+ [DataRow("Down", "ArrowDown")]
+ [DataRow("DownArrow", "ArrowDown")]
+ [DataRow("Left", "ArrowLeft")]
+ [DataRow("LeftArrow", "ArrowLeft")]
+ [DataRow("Right", "ArrowRight")]
+ [DataRow("RightArrow", "ArrowRight")]
+ [DataRow("Option", "Alt")]
+ [DataRow("Super", "Meta")]
+ [DataRow("D0", "0")]
+ [DataRow("D1", "1")]
+ [DataRow("D2", "2")]
+ [DataRow("D3", "3")]
+ [DataRow("D4", "4")]
+ [DataRow("D5", "5")]
+ [DataRow("D6", "6")]
+ [DataRow("D7", "7")]
+ [DataRow("D8", "8")]
+ [DataRow("D9", "9")]
+ public void ChordParse_KeyAliasesMatchCanonicalName(string alias, string canonical)
+ {
+ ArgumentNullException.ThrowIfNull(alias);
+ Chord expected = Chord.Parse($"Ctrl+{canonical}");
+ Chord actual = Chord.Parse($"Ctrl+{alias}");
+
+ Assert.AreEqual(expected, actual);
+ Assert.AreEqual(expected.GetHashCode(), actual.GetHashCode());
+ Assert.AreEqual(new Note(canonical), new Note(NoteName.Create(alias.ToUpperInvariant())));
+ }
+
[TestMethod]
[DataRow("control")]
[DataRow("Control")]
@@ -125,4 +163,20 @@ public async Task StoredProfileWithAliasSpelling_MatchesAfterReload()
Assert.AreEqual(Chord.Parse("Ctrl+S"), profile.GetChord("save"));
Assert.AreEqual(Chord.Parse("Meta+F"), profile.GetChord("find"));
}
+
+ [TestMethod]
+ public async Task StoredProfileWithKeyAlias_ExecutesCanonicalChordAfterLoad()
+ {
+ string json = """
+ [{"id":"p","name":"Profile","chords":{"save":{"notes":["CTRL","ESC"]}}}]
+ """;
+ await File.WriteAllTextAsync(Path.Combine(_testDataDirectory, Constants.Files.ProfilesFileName), json).ConfigureAwait(false);
+
+ using KeybindingManager manager = new(_testDataDirectory);
+ await manager.InitializeAsync().ConfigureAwait(false);
+ manager.Commands.RegisterCommand(new Command("save", "Save"));
+ manager.Profiles.SetActiveProfile("p");
+
+ Assert.AreEqual("save", manager.Keybindings.ExecuteChord(Chord.Parse("Ctrl+Escape")));
+ }
}
From 1b2f521a7cc991b8ec9e1ad750401309a2defa90 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 1 Oct 2026 10:08:22 +0000
Subject: [PATCH 3/3] Canonicalize key aliases and remove unrelated metadata
edits
Co-authored-by: matt-edmondson <19528727+matt-edmondson@users.noreply.github.com>
---
.editorconfig | 4 ++--
.gitignore | 18 -----------------
Keybinding/Models/MusicalTypes.cs | 33 ++++++++++++++++++++++++-------
3 files changed, 28 insertions(+), 27 deletions(-)
diff --git a/.editorconfig b/.editorconfig
index fb1feb2..92356f0 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -19,7 +19,7 @@ trim_trailing_whitespace = true
[*.{cs,csx,cake,fs,fsx,vb,vbx}]
indent_style = tab
-file_header_template = Copyright (c) 2023-2026 ktsu-dev contributors
+file_header_template = Copyright (c) ktsu.dev\nAll rights reserved.\nLicensed under the MIT license.
# Default severity for all .NET Code Style rules
dotnet_analyzer_diagnostic.severity = error
@@ -382,7 +382,7 @@ dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
# C++ files
-[*.{cpp,h,hpp,cc,hh,cxx,hxx}]
+[*.cpp,*.h,*.hpp,*.cc,*.hh,*.cxx,*.hxx]
indent_style = tab
# Naming convention rules (note: currently need to be ordered from more to less specific)
diff --git a/.gitignore b/.gitignore
index e043c9f..dc0470a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -203,11 +203,6 @@ PublishScripts/
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
-# and except a Unity project's Packages/, which is source: Unity's package manifest and its
-# resolved lock file are both meant to be committed, and a NuGet restore folder never contains
-# a file by either name.
-!**/[Pp]ackages/manifest.json
-!**/[Pp]ackages/packages-lock.json
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
@@ -656,16 +651,3 @@ Temporary Items
# ImGui.ini files
imgui.ini
-
-# Game engine projects
-#
-# Godot: the import cache, and the mono/temp bin+obj a C# build writes.
-.godot/
-
-# Unity: .meta files are source, not the Visual Studio C++ build artifact that the `*.meta` rule
-# further up targets. Unity generates one per asset and it carries the GUID that scenes, prefabs
-# and serialized references point at, so ignoring them gives every clone fresh GUIDs and silently
-# breaks those references - including for a plug-in whose .dll is itself a build output. This
-# negation has to come after that rule to win, and is scoped to the asset tree so the Visual
-# Studio artifact stays ignored everywhere else.
-!**/[Aa]ssets/**/*.meta
diff --git a/Keybinding/Models/MusicalTypes.cs b/Keybinding/Models/MusicalTypes.cs
index 16e514c..0ceac48 100644
--- a/Keybinding/Models/MusicalTypes.cs
+++ b/Keybinding/Models/MusicalTypes.cs
@@ -59,17 +59,36 @@ private static string ValidateKey(string key) =>
: key;
///
- /// Maps a modifier alias to its canonical key name, so that every way of building a note
+ /// Maps key aliases to canonical key names, so that every way of building a note
/// (parsing, constructing directly, or loading a stored profile) compares and hashes the same.
///
/// An uppercase key name
/// The canonical key name
- private static string CanonicalizeKey(string key) => key switch
+ private static string CanonicalizeKey(string key)
{
- "CONTROL" => "CTRL",
- "WIN" or "WINDOWS" or "CMD" or "COMMAND" => "META",
- _ => key
- };
+ if (key.Length == 2 && key[0] == 'D' && key[1] is >= '0' and <= '9')
+ {
+ return key[1..];
+ }
+
+ return key switch
+ {
+ "CONTROL" => "CTRL",
+ "OPTION" => "ALT",
+ "WIN" or "WINDOWS" or "CMD" or "COMMAND" or "SUPER" => "META",
+ "ESC" => nameof(SpecialKeys.Escape).ToUpperInvariant(),
+ "RETURN" => nameof(SpecialKeys.Enter).ToUpperInvariant(),
+ "DEL" => nameof(SpecialKeys.Delete).ToUpperInvariant(),
+ "INS" => nameof(SpecialKeys.Insert).ToUpperInvariant(),
+ "PGUP" => nameof(SpecialKeys.PageUp).ToUpperInvariant(),
+ "PGDN" => nameof(SpecialKeys.PageDown).ToUpperInvariant(),
+ "UP" or "UPARROW" => nameof(SpecialKeys.ArrowUp).ToUpperInvariant(),
+ "DOWN" or "DOWNARROW" => nameof(SpecialKeys.ArrowDown).ToUpperInvariant(),
+ "LEFT" or "LEFTARROW" => nameof(SpecialKeys.ArrowLeft).ToUpperInvariant(),
+ "RIGHT" or "RIGHTARROW" => nameof(SpecialKeys.ArrowRight).ToUpperInvariant(),
+ _ => key
+ };
+ }
///
/// Gets the key that this note represents
@@ -309,7 +328,7 @@ public static Chord Parse(string value)
foreach (string part in parts)
{
- // The Note constructor normalizes modifier aliases such as "Control" and "Cmd"
+ // The Note constructor normalizes key aliases such as "Control" and "Esc"
notes.Add(new Note(part));
}