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)); }