From fac01b0f63da12548da32f20f78f7c81174cfeaa Mon Sep 17 00:00:00 2001 From: matt-edmondson Date: Sat, 26 Sep 2026 10:24:59 +0000 Subject: [PATCH] Skip unregistered commands when executing a shared chord [patch] ExecuteChord took the first binding for the chord and only then checked whether its command was registered, so an unregistered command bound first hid a registered command sharing the same chord. Filter candidate bindings by IsCommandRegistered before picking one. Fixes #116 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CZB6C9rAp2hA5WQDcw2NDp --- Keybinding.Test/ExecuteChordTests.cs | 70 ++++++++++++++++++++++++ Keybinding/Services/KeybindingService.cs | 19 +++++-- 2 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 Keybinding.Test/ExecuteChordTests.cs diff --git a/Keybinding.Test/ExecuteChordTests.cs b/Keybinding.Test/ExecuteChordTests.cs new file mode 100644 index 0000000..5368849 --- /dev/null +++ b/Keybinding.Test/ExecuteChordTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Keybinding.Test; + +using ktsu.Keybinding.Core.Models; +using ktsu.Keybinding.Core.Services; + +[TestClass] +public class ExecuteChordTests +{ + private CommandRegistry _registry = null!; + private KeybindingService _service = null!; + + [TestInitialize] + public void Setup() + { + _registry = new CommandRegistry(); + ProfileManager profiles = new(); + profiles.CreateProfile("p", "Profile"); + profiles.SetActiveProfile("p"); + _service = new KeybindingService(_registry, profiles); + + _registry.RegisterCommand(new Command("a", "A")); + _registry.RegisterCommand(new Command("b", "B")); + } + + [TestMethod] + public void ExecuteChord_FirstBindingUnregistered_RunsTheRegisteredCommandSharingTheChord() + { + Chord chord = Chord.Parse("Ctrl+S"); + Assert.IsTrue(_service.BindChord("a", chord)); + Assert.IsTrue(_service.BindChord("b", chord)); + + _registry.UnregisterCommand("a"); + + Assert.AreEqual("b", _service.ExecuteChord(chord), "The only registered command bound to the chord should run."); + Assert.AreEqual("b", _service.ExecuteChord("p", chord)); + } + + [TestMethod] + public void ExecuteChord_BothRegistered_RunsTheFirstBinding() + { + Chord chord = Chord.Parse("Ctrl+S"); + _service.BindChord("a", chord); + _service.BindChord("b", chord); + + Assert.AreEqual(_service.FindCommandByChord(chord), _service.ExecuteChord(chord)); + } + + [TestMethod] + public void ExecuteChord_OnlyBindingUnregistered_ReturnsNull() + { + Chord chord = Chord.Parse("Ctrl+S"); + _service.BindChord("a", chord); + + _registry.UnregisterCommand("a"); + + Assert.IsNull(_service.ExecuteChord(chord)); + } + + [TestMethod] + public void ExecuteChord_UnknownProfile_ReturnsNull() + { + Chord chord = Chord.Parse("Ctrl+S"); + _service.BindChord("a", chord); + + Assert.IsNull(_service.ExecuteChord("missing", chord)); + Assert.IsNull(_service.ExecuteChord(" ", chord)); + } +} diff --git a/Keybinding/Services/KeybindingService.cs b/Keybinding/Services/KeybindingService.cs index c0f9a14..d73f78f 100644 --- a/Keybinding/Services/KeybindingService.cs +++ b/Keybinding/Services/KeybindingService.cs @@ -228,15 +228,22 @@ public Chord ParseChord(string chordString) { Ensure.NotNull(chord); - string? commandId = FindCommandByChord(profileId, chord); - if (commandId is not null && _commandRegistry.IsCommandRegistered(commandId)) + if (string.IsNullOrWhiteSpace(profileId)) { - // In a real implementation, this would trigger command execution - // For now, we just return the command ID that would be executed - return commandId; + return null; } - return null; + // A chord can be bound to more than one command, and unregistering a command leaves its + // bindings in place, so skip bindings whose command is no longer registered rather than + // giving up on the first match. + Profile? profile = _profileManager.GetProfile(profileId); + string? commandId = profile?.Chords + .FirstOrDefault(kvp => kvp.Value.Equals(chord) && _commandRegistry.IsCommandRegistered(kvp.Key)) + .Key; + + // In a real implementation, this would trigger command execution + // For now, we just return the command ID that would be executed + return commandId; } ///