diff --git a/Keybinding.Test/CommandCategoryFilterTests.cs b/Keybinding.Test/CommandCategoryFilterTests.cs
new file mode 100644
index 0000000..5f767b1
--- /dev/null
+++ b/Keybinding.Test/CommandCategoryFilterTests.cs
@@ -0,0 +1,56 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Keybinding.Test;
+
+using ktsu.Keybinding.Core.Models;
+using ktsu.Keybinding.Core.Services;
+
+[TestClass]
+public class CommandCategoryFilterTests
+{
+ private static readonly string[] UncategorizedIds = ["file.save"];
+ private static readonly string[] EditIds = ["edit.copy"];
+
+ private CommandRegistry _registry = null!;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _registry = new CommandRegistry();
+ _registry.RegisterCommand(new Command("file.save", "Save"));
+ _registry.RegisterCommand(new Command("edit.copy", "Copy", null, "Edit"));
+ }
+
+ [TestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ public void GetCommandsByCategory_BlankCategory_ReturnsOnlyUncategorizedCommands(string? category)
+ {
+ string[] ids = [.. _registry.GetCommandsByCategory(category).Select(c => (string)c.Id)];
+
+ CollectionAssert.AreEquivalent(UncategorizedIds, ids);
+ }
+
+ [TestMethod]
+ [DataRow("Edit")]
+ [DataRow("edit")]
+ [DataRow(" Edit ")]
+ public void GetCommandsByCategory_NamedCategory_ReturnsOnlyThatCategory(string category)
+ {
+ string[] ids = [.. _registry.GetCommandsByCategory(category).Select(c => (string)c.Id)];
+
+ CollectionAssert.AreEquivalent(EditIds, ids);
+ }
+
+ [TestMethod]
+ public void GetCommandsByCategory_PassingACommandsOwnCategoryBack_FindsThatCommand()
+ {
+ foreach (Command command in _registry.GetAllCommands())
+ {
+ Assert.IsTrue(
+ _registry.GetCommandsByCategory(command.Category).Contains(command),
+ $"{command.Id} should be in the group for its own category.");
+ }
+ }
+}
diff --git a/Keybinding/Contracts/ICommandRegistry.cs b/Keybinding/Contracts/ICommandRegistry.cs
index 1ed012f..04f3545 100644
--- a/Keybinding/Contracts/ICommandRegistry.cs
+++ b/Keybinding/Contracts/ICommandRegistry.cs
@@ -39,7 +39,7 @@ public interface ICommandRegistry
///
/// Gets commands by category
///
- /// The category to filter by
+ /// The category to filter by, compared case-insensitively. Null, empty or whitespace selects the uncategorized commands.
/// Collection of commands in the specified category
public IReadOnlyCollection GetCommandsByCategory(string? category);
diff --git a/Keybinding/Services/CommandRegistry.cs b/Keybinding/Services/CommandRegistry.cs
index 324b84a..34dce10 100644
--- a/Keybinding/Services/CommandRegistry.cs
+++ b/Keybinding/Services/CommandRegistry.cs
@@ -57,9 +57,13 @@ public IReadOnlyCollection GetCommandsByCategory(string? category)
{
lock (_lock)
{
- string? normalizedCategory = category?.Trim();
+ // Compare explicitly: a null CommandCategory converts to "" rather than null, so passing
+ // it straight to string.Equals made null find nothing while "" found the uncategorized commands.
+ string? wanted = string.IsNullOrWhiteSpace(category) ? null : category.Trim();
return _commands.Values
- .Where(c => string.Equals(c.Category, normalizedCategory, StringComparison.OrdinalIgnoreCase))
+ .Where(c => wanted is null
+ ? string.IsNullOrWhiteSpace(c.Category)
+ : string.Equals(c.Category?.ToString(), wanted, StringComparison.OrdinalIgnoreCase))
.ToList()
.AsReadOnly();
}