chore: consume shortcut keys on windows
This commit is contained in:
parent
e769c00f37
commit
bf4fe4ff6d
6 changed files with 314 additions and 1 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
118
packages/native-helpers/windows-helper/src/VirtualKeyMap.cs
Normal file
118
packages/native-helpers/windows-helper/src/VirtualKeyMap.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 " +
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue