How the Global Hotkey Registration and Handling System Works in Shadowsocks-Windows
Shadowsocks-Windows registers system-wide keyboard shortcuts by parsing configuration strings into Key/ModifierKeys objects, mapping them to callback delegates via reflection, and dispatching actions through the HotKeyManager singleton that captures input across all application windows.
The shadowsocks/shadowsocks-windows repository implements a comprehensive global hotkey subsystem that enables users to trigger proxy toggles, server switches, and UI actions from any window context. This system operates through a three-layer architecture that cleanly separates user configuration, OS-level registration, and command execution. Understanding this flow reveals how the application maintains low-level keyboard hooks without interfering with other software.
Architecture Overview
The global hotkey implementation spans three distinct layers, each handled by specific components in the codebase:
| Layer | Responsibility | Core Component | Source File |
|---|---|---|---|
| Configuration | Stores textual hotkey definitions and startup flags | HotkeyConfig class (strings like "Ctrl+Alt+S") |
Model/HotKeyConfig.cs |
| Registration | Parses strings, converts to key codes, registers with OS | HotKeys static class, HotKeyManager (GlobalHotKey NuGet) |
Controller/System/Hotkeys/Hotkeys.cs |
| Execution | Holds concrete methods invoked when hotkeys fire | HotkeyCallbacks singleton with private delegates |
Controller/System/Hotkeys/HotkeyCallbacks.cs |
Configuration Storage
User-defined shortcuts reside in the HotkeyConfig class within Model/HotKeyConfig.cs. Each property maps to a specific action, such as SwitchSystemProxy or ShowLogs, storing the shortcut as a human-readable string (e.g., "Ctrl+Shift+L"). An empty string indicates no binding.
The configuration also includes the RegHotkeysAtStartup boolean flag. When enabled, the application attempts to register all configured shortcuts during initialization. Default values for hotkey properties are empty strings (lines 14–30 of HotKeyConfig.cs), meaning no system-wide keys are captured until the user explicitly configures them.
Registration Pipeline
Boot-Time Registration Flow
During application startup, HotkeyReg.RegAllHotkeys() (defined in Controller/HotkeyReg.cs) orchestrates the registration process. If RegHotkeysAtStartup is true, the method iterates over each configured hotkey property and invokes RegHotkeyFromString (lines 20–27).
This method performs three critical operations:
-
Callback Lookup: It calls
HotkeyCallbacks.GetCallback(callbackName)to locate the target method via reflection. This returns aHotKeyCallBackHandlerdelegate pointing to private methods in theHotkeyCallbackssingleton (lines 24–30 ofHotkeyCallbacks.cs). -
String Parsing: The shortcut string is converted to system key codes via
HotKeys.Str2HotKey. This method splits the string on the last"+"delimiter to separate modifiers (Ctrl, Alt, Shift) from the primary key, then uses .NETKeyConverterandModifierKeysConverterto generate the final objects (lines 9–26 ofHotkeys.cs). -
OS Registration:
HotKeys.RegHotkeyfirst removes any existing registration for the same callback viaUnregExistingHotkey, then calls the privateRegistermethod to bind the key combination to the Windows hotkey API (lines 40–44 and 39–50 ofHotkeys.cs).
If the OS reports that a key combination is already reserved by another application, RegHotkeyFromString returns false, triggering a message box alert: "Register hotkey failed".
Manual Registration Example
While the application handles this automatically, you can manually register a hotkey programmatically:
// Initialize the manager with the main controller instance
HotKeys.Init(controller);
// Parse the shortcut and retrieve the callback delegate
var hotKey = HotKeys.Str2HotKey("Ctrl+Alt+S");
var callback = HotkeyCallbacks.GetCallback("SwitchSystemProxyCallback")
as HotKeys.HotKeyCallBackHandler;
// Register with the OS
bool success = HotKeys.RegHotkey(hotKey, callback);
Runtime Event Handling
Global Input Capture
Once registered, the HotKeyManager from the GlobalHotKey NuGet package monitors system-wide keyboard input. When a user presses a registered combination, the manager raises the KeyPressed event. The static HotKeys.HotKeyManagerPressed handler receives this event, looks up the corresponding delegate in the internal _keymap dictionary, and synchronously invokes the callback (lines 32–38 of Hotkeys.cs).
Callback Resolution and Execution
The HotkeyCallbacks class serves as a singleton repository for all hotkey actions. It exposes private methods such as:
SwitchSystemProxyCallback(): Toggles theenabledstate via_controller.ToggleEnable()ShowLogsCallback(): Opens the logging windowServerMoveUpCallback()/ServerMoveDownCallback(): Cycles through the server configuration list
These methods operate on the ShadowsocksController instance stored within the singleton, granting them full access to the application’s model and configuration state.
// Example callback implementation from HotkeyCallbacks.cs
private void SwitchSystemProxyCallback()
{
bool enabled = _controller.GetCurrentConfiguration().enabled;
_controller.ToggleEnable(!enabled);
}
Cleanup and Resource Management
On application shutdown, HotKeys.Destroy() removes the event subscription to HotKeyManagerPressed and disposes the underlying HotKeyManager instance. This ensures the Windows OS releases all registered global shortcuts, preventing "ghost" hotkeys that persist after the application closes (lines 26–30 of Hotkeys.cs).
To unregister a specific hotkey during runtime without shutting down:
var callback = HotkeyCallbacks.GetCallback("ShowLogsCallback")
as HotKeys.HotKeyCallBackHandler;
HotKeys.UnregExistingHotkey(callback);
Summary
- Configuration: User shortcuts are stored as strings in
HotkeyConfigwithinModel/HotKeyConfig.cs, controlled by theRegHotkeysAtStartupflag. - Parsing: The
HotKeys.Str2HotKeymethod converts textual shortcuts like"Ctrl+Alt+S"intoKeyandModifierKeysobjects by splitting on the last"+"delimiter. - Registration:
HotkeyReg.RegAllHotkeys()iterates through configuration properties, uses reflection to bind callbacks, and registers combinations with the OS viaHotKeys.RegHotkey. - Dispatch: The
HotKeyManagercaptures global keypresses and routes them throughHotKeys.HotKeyManagerPressed, which invokes delegates stored in the_keymapdictionary. - Cleanup: Calling
HotKeys.Destroy()on exit unregisters all hotkeys and disposes the manager to free OS resources.
Frequently Asked Questions
How does shadowsocks-windows parse hotkey strings into system key codes?
The HotKeys.Str2HotKey method in Controller/System/Hotkeys/Hotkeys.cs handles parsing by finding the last occurrence of "+" in the string. It treats everything before as modifiers (Ctrl, Alt, Shift) and everything after as the primary key. The method uses .NET's KeyConverter and ModifierKeysConverter classes to transform these substrings into the appropriate System.Windows.Input enumeration values.
What happens if a hotkey is already registered by another application?
During the registration loop in Controller/HotkeyReg.cs, the RegHotkeyFromString method returns a boolean status. If the underlying Windows API reports that the key combination is already in use, the method returns false, and RegAllHotkeys aggregates these failures to display a single error dialog stating "Register hotkey failed". The application continues starting up, but the conflicting hotkey remains inactive.
How can I add a custom hotkey action to the codebase?
First, add a new string property to HotkeyConfig.cs to store the shortcut. Then, implement the action method in HotkeyCallbacks.cs as a private void method. Finally, update HotkeyCallbacks.GetCallback to map a string name to your new method using reflection. The registration system will automatically pick up the new hotkey during the next startup if configured in the settings.
Where does the application store hotkey configurations between sessions?
The HotkeyConfig object is serialized as part of the main configuration file (typically gui-config.json in the application directory). When Program.MainController.GetCurrentConfiguration() loads, it deserializes this file into a configuration object containing the hotkey property, which persists all shortcut strings and the RegHotkeysAtStartup boolean.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →