chore: consume shortcut keys on windows

This commit is contained in:
nchopra 2025-12-21 11:39:08 +05:30
parent e769c00f37
commit bf4fe4ff6d
6 changed files with 314 additions and 1 deletions

View file

@ -20,6 +20,8 @@
// var muteSystemAudioResult = MuteSystemAudioResult.FromJson(jsonString);
// var restoreSystemAudioParams = RestoreSystemAudioParams.FromJson(jsonString);
// var restoreSystemAudioResult = RestoreSystemAudioResult.FromJson(jsonString);
// var setShortcutsParams = SetShortcutsParams.FromJson(jsonString);
// var setShortcutsResult = SetShortcutsResult.FromJson(jsonString);
// var keyDownEvent = KeyDownEvent.FromJson(jsonString);
// var keyUpEvent = KeyUpEvent.FromJson(jsonString);
// var flagsChangedEvent = FlagsChangedEvent.FromJson(jsonString);
@ -224,6 +226,21 @@ namespace WindowsHelper.Models
public bool Success { get; set; }
}
public partial class SetShortcutsParams
{
[JsonPropertyName("pushToTalk")]
public List<string> PushToTalk { get; set; }
[JsonPropertyName("toggleRecording")]
public List<string> ToggleRecording { get; set; }
}
public partial class SetShortcutsResult
{
[JsonPropertyName("success")]
public bool Success { get; set; }
}
public partial class KeyDownEvent
{
[JsonPropertyName("payload")]
@ -510,6 +527,16 @@ namespace WindowsHelper.Models
public static RestoreSystemAudioResult FromJson(string json) => JsonSerializer.Deserialize<RestoreSystemAudioResult>(json, WindowsHelper.Models.Converter.Settings);
}
public partial class SetShortcutsParams
{
public static SetShortcutsParams FromJson(string json) => JsonSerializer.Deserialize<SetShortcutsParams>(json, WindowsHelper.Models.Converter.Settings);
}
public partial class SetShortcutsResult
{
public static SetShortcutsResult FromJson(string json) => JsonSerializer.Deserialize<SetShortcutsResult>(json, WindowsHelper.Models.Converter.Settings);
}
public partial class KeyDownEvent
{
public static KeyDownEvent FromJson(string json) => JsonSerializer.Deserialize<KeyDownEvent>(json, WindowsHelper.Models.Converter.Settings);
@ -543,6 +570,8 @@ namespace WindowsHelper.Models
public static string ToJson(this object self) => JsonSerializer.Serialize(self, WindowsHelper.Models.Converter.Settings);
public static string ToJson(this MuteSystemAudioResult self) => JsonSerializer.Serialize(self, WindowsHelper.Models.Converter.Settings);
public static string ToJson(this RestoreSystemAudioResult self) => JsonSerializer.Serialize(self, WindowsHelper.Models.Converter.Settings);
public static string ToJson(this SetShortcutsParams self) => JsonSerializer.Serialize(self, WindowsHelper.Models.Converter.Settings);
public static string ToJson(this SetShortcutsResult self) => JsonSerializer.Serialize(self, WindowsHelper.Models.Converter.Settings);
public static string ToJson(this KeyDownEvent self) => JsonSerializer.Serialize(self, WindowsHelper.Models.Converter.Settings);
public static string ToJson(this KeyUpEvent self) => JsonSerializer.Serialize(self, WindowsHelper.Models.Converter.Settings);
public static string ToJson(this FlagsChangedEvent self) => JsonSerializer.Serialize(self, WindowsHelper.Models.Converter.Settings);

View file

@ -98,7 +98,11 @@ namespace WindowsHelper
case Method.RestoreSystemAudio:
response = HandleRestoreSystemAudio(request);
break;
case Method.SetShortcuts:
response = HandleSetShortcuts(request);
break;
default:
LogToStderr($"Method not found: {request.Method} for ID: {request.Id}");
response = new RpcResponse
@ -338,6 +342,54 @@ namespace WindowsHelper
audioCompletionHandler?.Invoke(requestId);
}
private RpcResponse HandleSetShortcuts(RpcRequest request)
{
LogToStderr($"[RpcHandler] Handling setShortcuts for ID: {request.Id}");
try
{
var paramsJson = JsonSerializer.Serialize(request.Params, jsonOptions);
var setShortcutsParams = JsonSerializer.Deserialize<SetShortcutsParams>(paramsJson, jsonOptions);
if (setShortcutsParams == null)
{
return new RpcResponse
{
Id = request.Id.ToString(),
Error = new Error
{
Code = -32602,
Message = "Invalid params: could not deserialize SetShortcutsParams"
}
};
}
ShortcutManager.Instance.SetShortcuts(
setShortcutsParams.PushToTalk?.ToArray() ?? Array.Empty<string>(),
setShortcutsParams.ToggleRecording?.ToArray() ?? Array.Empty<string>()
);
return new RpcResponse
{
Id = request.Id.ToString(),
Result = new SetShortcutsResult { Success = true }
};
}
catch (Exception ex)
{
LogToStderr($"[RpcHandler] Error in setShortcuts: {ex.Message}");
return new RpcResponse
{
Id = request.Id.ToString(),
Error = new Error
{
Code = -32603,
Message = $"Internal error: {ex.Message}"
}
};
}
}
private void SendRpcResponse(RpcResponse response)
{
try

View file

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace WindowsHelper
{
/// <summary>
/// Represents the state of modifier keys at a given moment.
/// </summary>
public struct ModifierState
{
public bool Win;
public bool Ctrl;
public bool Alt;
public bool Shift;
}
/// <summary>
/// Manages configured shortcuts and determines if key events should be consumed.
/// Thread-safe singleton that can be updated from RpcHandler (background thread)
/// and queried from ShortcutMonitor hook callback (main thread).
/// Mirrors swift-helper/Sources/SwiftHelper/ShortcutManager.swift
/// </summary>
public class ShortcutManager
{
private static readonly Lazy<ShortcutManager> _instance = new(() => new ShortcutManager());
public static ShortcutManager Instance => _instance.Value;
private readonly object _lock = new();
private string[] _pushToTalkKeys = Array.Empty<string>();
private string[] _toggleRecordingKeys = Array.Empty<string>();
private ShortcutManager() { }
private void LogToStderr(string message)
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
Console.Error.WriteLine($"[{timestamp}] [ShortcutManager] {message}");
Console.Error.Flush();
}
/// <summary>
/// Update the configured shortcuts.
/// Called from RpcHandler when setShortcuts RPC is received.
/// </summary>
public void SetShortcuts(string[] pushToTalk, string[] toggleRecording)
{
lock (_lock)
{
_pushToTalkKeys = pushToTalk ?? Array.Empty<string>();
_toggleRecordingKeys = toggleRecording ?? Array.Empty<string>();
LogToStderr($"Shortcuts updated - PTT: [{string.Join(", ", _pushToTalkKeys)}], Toggle: [{string.Join(", ", _toggleRecordingKeys)}]");
}
}
/// <summary>
/// Check if this key event should be consumed (prevent default behavior).
/// Called from ShortcutMonitor hook callback for keyDown/keyUp events only.
/// </summary>
public bool ShouldConsumeKey(int vkCode, ModifierState modifiers)
{
lock (_lock)
{
// Early exit if no shortcuts configured
if (_pushToTalkKeys.Length == 0 && _toggleRecordingKeys.Length == 0)
{
return false;
}
// Build set of currently active keys (modifiers + this regular key)
var activeKeys = new HashSet<string>();
if (modifiers.Win) activeKeys.Add("Win");
if (modifiers.Ctrl) activeKeys.Add("Ctrl");
if (modifiers.Alt) activeKeys.Add("Alt");
if (modifiers.Shift) activeKeys.Add("Shift");
// Add the regular key being pressed
var keyName = VirtualKeyMap.GetKeyName(vkCode);
if (keyName != null)
{
activeKeys.Add(keyName);
}
// PTT: subset match (all PTT keys pressed, possibly with extras)
var pttKeys = new HashSet<string>(_pushToTalkKeys);
var pttMatch = pttKeys.Count > 0 && pttKeys.IsSubsetOf(activeKeys);
// Toggle: exact match (only these keys pressed)
var toggleKeys = new HashSet<string>(_toggleRecordingKeys);
var toggleMatch = toggleKeys.Count > 0 && toggleKeys.SetEquals(activeKeys);
return pttMatch || toggleMatch;
}
}
}
}

View file

@ -328,6 +328,22 @@ namespace WindowsHelper
{
// Send regular key event
KeyEventOccurred?.Invoke(this, keyEvent);
// Check if this key event should be consumed (prevent default behavior)
// Only for regular key events, not modifiers
var modifierState = new ModifierState
{
Win = winPressed,
Ctrl = ctrlPressed,
Alt = altPressed,
Shift = shiftPressed
};
if (ShortcutManager.Instance.ShouldConsumeKey((int)kbStruct.vkCode, modifierState))
{
// Consume - prevent default behavior (e.g., cursor movement for arrow keys)
return (IntPtr)1;
}
}
}
}

View file

@ -0,0 +1,118 @@
using System.Collections.Generic;
namespace WindowsHelper
{
/// <summary>
/// Windows Virtual Key code to key name mapping.
/// Matches the Swift KeycodeMap.swift for cross-platform shortcut consistency.
/// </summary>
public static class VirtualKeyMap
{
// Virtual Key code constants
private const int VK_BACK = 0x08;
private const int VK_TAB = 0x09;
private const int VK_RETURN = 0x0D;
private const int VK_ESCAPE = 0x1B;
private const int VK_SPACE = 0x20;
private const int VK_LEFT = 0x25;
private const int VK_UP = 0x26;
private const int VK_RIGHT = 0x27;
private const int VK_DOWN = 0x28;
// Letters: 0x41-0x5A (A-Z)
// Numbers: 0x30-0x39 (0-9)
// Function keys: 0x70-0x7B (F1-F12)
private static readonly Dictionary<int, string> VkCodeToKey = new()
{
// Letters (A-Z: 0x41-0x5A)
{ 0x41, "A" },
{ 0x42, "B" },
{ 0x43, "C" },
{ 0x44, "D" },
{ 0x45, "E" },
{ 0x46, "F" },
{ 0x47, "G" },
{ 0x48, "H" },
{ 0x49, "I" },
{ 0x4A, "J" },
{ 0x4B, "K" },
{ 0x4C, "L" },
{ 0x4D, "M" },
{ 0x4E, "N" },
{ 0x4F, "O" },
{ 0x50, "P" },
{ 0x51, "Q" },
{ 0x52, "R" },
{ 0x53, "S" },
{ 0x54, "T" },
{ 0x55, "U" },
{ 0x56, "V" },
{ 0x57, "W" },
{ 0x58, "X" },
{ 0x59, "Y" },
{ 0x5A, "Z" },
// Numbers (0-9: 0x30-0x39)
{ 0x30, "0" },
{ 0x31, "1" },
{ 0x32, "2" },
{ 0x33, "3" },
{ 0x34, "4" },
{ 0x35, "5" },
{ 0x36, "6" },
{ 0x37, "7" },
{ 0x38, "8" },
{ 0x39, "9" },
// Special keys
{ VK_TAB, "Tab" },
{ VK_SPACE, "Space" },
{ VK_BACK, "Delete" }, // Backspace = Delete (same as Swift)
{ VK_RETURN, "Enter" },
{ VK_ESCAPE, "Escape" },
// Function keys (F1-F12: 0x70-0x7B)
{ 0x70, "F1" },
{ 0x71, "F2" },
{ 0x72, "F3" },
{ 0x73, "F4" },
{ 0x74, "F5" },
{ 0x75, "F6" },
{ 0x76, "F7" },
{ 0x77, "F8" },
{ 0x78, "F9" },
{ 0x79, "F10" },
{ 0x7A, "F11" },
{ 0x7B, "F12" },
// Arrow keys
{ VK_LEFT, "Left" },
{ VK_RIGHT, "Right" },
{ VK_DOWN, "Down" },
{ VK_UP, "Up" },
// Punctuation and symbols
{ 0xBD, "-" }, // VK_OEM_MINUS
{ 0xBB, "=" }, // VK_OEM_PLUS (equals sign without shift)
{ 0xDB, "[" }, // VK_OEM_4
{ 0xDD, "]" }, // VK_OEM_6
{ 0xDC, "\\" }, // VK_OEM_5
{ 0xBA, ";" }, // VK_OEM_1
{ 0xDE, "'" }, // VK_OEM_7
{ 0xBC, "," }, // VK_OEM_COMMA
{ 0xBE, "." }, // VK_OEM_PERIOD
{ 0xBF, "/" }, // VK_OEM_2
{ 0xC0, "`" }, // VK_OEM_3
};
/// <summary>
/// Convert a Windows Virtual Key code to a key name string.
/// Returns null if the keycode is not mapped.
/// </summary>
public static string? GetKeyName(int vkCode)
{
return VkCodeToKey.TryGetValue(vkCode, out var name) ? name : null;
}
}
}

View file

@ -37,6 +37,8 @@ try {
"generated/json-schemas/methods/mute-system-audio-result.schema.json " +
"generated/json-schemas/methods/restore-system-audio-params.schema.json " +
"generated/json-schemas/methods/restore-system-audio-result.schema.json " +
"generated/json-schemas/methods/set-shortcuts-params.schema.json " +
"generated/json-schemas/methods/set-shortcuts-result.schema.json " +
"generated/json-schemas/events/key-down-event.schema.json " +
"generated/json-schemas/events/key-up-event.schema.json " +
"generated/json-schemas/events/flags-changed-event.schema.json " +