A comprehensive .NET library for managing keyboard shortcuts and keybindings with support for multiple profiles, command registration, and persistent storage.
- Multi-Profile Support: Create and manage multiple keybinding profiles
- Command Registry: Register and organize commands with categories
- Flexible Key Combinations: Support for complex key combinations with multiple modifiers
- Persistent Storage: Automatic saving and loading of profiles and commands
- Thread-Safe Operations: Concurrent access support for multi-threaded applications
- SOLID Architecture: Clean, extensible design following SOLID principles
- Comprehensive API: Full-featured interfaces for all operations
Add a reference to the Keybinding project in your application.
using ktsu.Keybinding.Core;
using ktsu.Keybinding.Core.Models;
// Initialize the keybinding manager
var manager = new KeybindingManager("./keybinding-data");
await manager.InitializeAsync();
// Create a default profile if none exist
manager.CreateDefaultProfile();
// Register some commands
var commands = new[]
{
new Command("file.new", "New File", "Create a new file", "File"),
new Command("file.save", "Save File", "Save the current file", "File"),
new Command("edit.copy", "Copy", "Copy selected text", "Edit")
};
manager.RegisterCommands(commands);
// Set chord bindings
manager.Keybindings.BindChord("file.new", manager.Keybindings.ParseChord("Ctrl+N"));
manager.Keybindings.BindChord("file.save", manager.Keybindings.ParseChord("Ctrl+S"));
manager.Keybindings.BindChord("edit.copy", manager.Keybindings.ParseChord("Ctrl+C"));
// Find commands by chord
var chord = manager.Keybindings.ParseChord("Ctrl+S");
var commandId = manager.Keybindings.FindCommandByChord(chord);
// Save changes
await manager.SaveAsync();Commands represent actions that can be bound to keyboard shortcuts:
var command = new Command(
id: "file.save", // Unique identifier
name: "Save File", // Display name
description: "Save the current file", // Optional description
category: "File" // Optional category for organization
);Chords represent musical-style key combinations using the Note and Chord classes:
// Parse from string
var chord = manager.Keybindings.ParseChord("Ctrl+Alt+S");
// Create programmatically
var chord = new Chord([
new Note("CTRL"),
new Note("ALT"),
new Note("S")
]);
// Supported modifiers: Ctrl, Alt, Shift, Meta (Windows/Cmd key)Key names are canonicalized, so every common spelling of a key compares equal: Control, LeftCtrl and
LControlKey are all Ctrl; Win, Cmd and LWin are Meta; Plus, Comma, Minus and Period are
+, ,, - and .; and framework names such as Back, Prior, Next and Capital are Backspace,
PageUp, PageDown and CapsLock. A chord built from a UI framework's key names matches one parsed from a
profile.
A chord runs one command per profile. Binding a chord that another command already uses moves it to the new
command, as rebinding a shortcut does in most editors. This holds whether you bind through BindChord or set the chord on the
Profile directly with Profile.SetChord. Stored data that already binds one chord to several commands still
loads as it is; the command with the ordinally first ID runs.
Profiles allow different keybinding configurations:
// Create a new profile
var profile = new Profile("gaming", "Gaming Profile", "Keybindings for gaming");
manager.Profiles.CreateProfile(profile);
// Switch active profile
manager.Profiles.SetActiveProfile("gaming");
// Duplicate a profile
var newProfile = manager.Profiles.DuplicateProfile("default", "custom", "Custom Profile");The library follows a clean architecture with clear separation of concerns:
Command: Represents a named action that can be bound to keysChord: Represents a keyboard shortcut with modifiers and primary key using musical paradigmNote: Represents individual keys in a chordPhrase: Represents sequences of chords for complex key combinationsProfile: Contains a set of chord bindings for commands
ICommandRegistry: Command management operationsIProfileManager: Profile management operationsIKeybindingService: Keybinding coordination and validationIKeybindingRepository: Persistence operations
CommandRegistry: Thread-safe command storage and retrievalProfileManager: Profile lifecycle managementKeybindingService: Coordinates keybinding operations across profiles and commandsJsonKeybindingRepository: JSON-based persistence implementation
KeybindingManager: Main entry point that coordinates all services
Implement your own storage mechanism:
public class DatabaseKeybindingRepository : IKeybindingRepository
{
// Implement async persistence methods
public async Task SaveProfileAsync(Profile profile) { /* ... */ }
public async Task<IEnumerable<Profile>> LoadAllProfilesAsync() { /* ... */ }
// ... other methods
}
// Use with custom repository
var manager = new KeybindingManager(
new CommandRegistry(),
new ProfileManager(),
new DatabaseKeybindingRepository()
);// Register multiple commands at once
var commands = new[]
{
new Command("edit.cut", "Cut", "Cut to clipboard", "Edit"),
new Command("edit.copy", "Copy", "Copy to clipboard", "Edit"),
new Command("edit.paste", "Paste", "Paste from clipboard", "Edit")
};
int registered = manager.RegisterCommands(commands);
// Set multiple chord bindings
var chords = new Dictionary<string, Chord>
{
{ "edit.cut", manager.Keybindings.ParseChord("Ctrl+X") },
{ "edit.copy", manager.Keybindings.ParseChord("Ctrl+C") },
{ "edit.paste", manager.Keybindings.ParseChord("Ctrl+V") }
};
int set = manager.SetChords(chords);
// A chord runs one command, so a later binding takes it from an earlier one.
// The count is the bindings that remain; the out overload names the commands that lost their chord.
int kept = manager.SetChords(chords, out IReadOnlyList<string> displaced);// Create specialized profiles
var vimProfile = new Profile("vim", "Vim Emulation", "Vim-style keybindings");
manager.Profiles.CreateProfile(vimProfile);
// Switch between profiles
manager.Profiles.SetActiveProfile("vim");
// ... set vim-style keybindings
manager.Profiles.SetActiveProfile("default");
// ... back to default keybindings
// Duplicate and customize
var customProfile = manager.Profiles.DuplicateProfile("default", "custom", "My Custom Profile");// Search commands by name
var fileCommands = manager.Commands.SearchCommands("file");
// Filter by category
var editCommands = manager.Commands.GetCommandsByCategory("Edit");
// Check if command exists
if (manager.Commands.CommandExists("file.save"))
{
// Command is registered
}An interactive console application demonstrating all library features:
dotnet run --project Keybinding.DemoThe demo includes:
- Status overview
- Profile management
- Command registration
- Keybinding configuration
- Phrase binding and lookup
- Profile switching demonstration
By default, the library stores data in JSON format in the specified directory:
data-directory/
├── commands.json # Registered commands
├── profiles.json # Every profile and its chord bindings
└── active-profile.json # Currently active profile ID
Each file is written to a temporary file beside it and moved into place, so a crash during a save leaves the
old file or the new one, never half of one. Saves from any number of managers in one process are serialized
per directory. A file that is not valid JSON loads as empty but is first moved aside to
<name>.corrupt-<timestamp>, so the next save cannot overwrite the only copy; a file with one bad entry loses
only that entry.
All services are designed to be thread-safe:
CommandRegistryusesConcurrentDictionaryfor command storageProfileManageruses locking for profile operationsKeybindingServicecoordinates safely across services
The library provides comprehensive error handling:
- Invalid key combinations are rejected during parsing
- Duplicate command IDs are prevented
- Profile operations validate existence and constraints
- Repository operations handle I/O errors gracefully
The solution includes comprehensive unit tests in the Keybinding.Test project:
dotnet testTests cover:
- Key combination parsing and validation
- Command registration and retrieval
- Profile management operations
- Keybinding service coordination
- Repository persistence operations
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE.md file for details.
This library follows SOLID principles:
- Single Responsibility: Each class has a focused purpose
- Open/Closed: Extensible through interfaces without modification
- Liskov Substitution: Implementations are interchangeable
- Interface Segregation: Focused, specific interfaces
- Dependency Inversion: Depends on abstractions, not concretions
The design also follows DRY (Don't Repeat Yourself) principles with consistent patterns across the codebase.