Public Access
Implement meeting assistant v1
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using MeetingAssistant.Recording;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MeetingAssistant.Hotkeys;
|
||||
|
||||
public sealed class GlobalHotkeyService : BackgroundService
|
||||
{
|
||||
private const int HotkeyId = 0x4D41;
|
||||
private const int WmHotkey = 0x0312;
|
||||
private const int WmQuit = 0x0012;
|
||||
private readonly MeetingAssistantOptions options;
|
||||
private readonly MeetingRecordingCoordinator coordinator;
|
||||
private readonly ILogger<GlobalHotkeyService> logger;
|
||||
private TaskCompletionSource? messageLoopCompletion;
|
||||
private uint messageThreadId;
|
||||
|
||||
public GlobalHotkeyService(
|
||||
IOptions<MeetingAssistantOptions> options,
|
||||
MeetingRecordingCoordinator coordinator,
|
||||
ILogger<GlobalHotkeyService> logger)
|
||||
{
|
||||
this.options = options.Value;
|
||||
this.coordinator = coordinator;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
logger.LogInformation("Global hotkey registration is only supported on Windows");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
messageLoopCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var messageThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
RunMessageLoop(stoppingToken);
|
||||
messageLoopCompletion.TrySetResult();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
messageLoopCompletion.TrySetException(exception);
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "Meeting Assistant Hotkey Loop"
|
||||
};
|
||||
|
||||
messageThread.Start();
|
||||
return messageLoopCompletion.Task;
|
||||
}
|
||||
|
||||
private void RunMessageLoop(CancellationToken stoppingToken)
|
||||
{
|
||||
var hotkey = HotkeyDefinition.Parse(options.Hotkey.Toggle);
|
||||
messageThreadId = GetCurrentThreadId();
|
||||
using var stoppingRegistration = stoppingToken.Register(() => PostQuitToMessageThread());
|
||||
|
||||
if (!RegisterHotKey(IntPtr.Zero, HotkeyId, (uint)hotkey.Modifiers, hotkey.VirtualKey))
|
||||
{
|
||||
logger.LogWarning("Could not register global hotkey {Hotkey}", options.Hotkey.Toggle);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Registered global hotkey {Hotkey}", options.Hotkey.Toggle);
|
||||
|
||||
try
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested && GetMessage(out var message, IntPtr.Zero, 0, 0) > 0)
|
||||
{
|
||||
if (message.Message == WmHotkey)
|
||||
{
|
||||
_ = Task.Run(ToggleRecordingAsync, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnregisterHotKey(IntPtr.Zero, HotkeyId);
|
||||
messageThreadId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
PostQuitToMessageThread();
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ToggleRecordingAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = await coordinator.ToggleAsync(CancellationToken.None);
|
||||
logger.LogInformation("Hotkey toggled recording. IsRecording={IsRecording}", status.IsRecording);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Hotkey toggle failed");
|
||||
}
|
||||
}
|
||||
|
||||
private void PostQuitToMessageThread()
|
||||
{
|
||||
var threadId = messageThreadId;
|
||||
if (threadId != 0)
|
||||
{
|
||||
PostThreadMessage(threadId, WmQuit, UIntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetMessage(out NativeMessage lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern uint GetCurrentThreadId();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool PostThreadMessage(uint idThread, int msg, UIntPtr wParam, IntPtr lParam);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativeMessage
|
||||
{
|
||||
public IntPtr Hwnd;
|
||||
public int Message;
|
||||
public UIntPtr WParam;
|
||||
public IntPtr LParam;
|
||||
public uint Time;
|
||||
public NativePoint Point;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativePoint
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
namespace MeetingAssistant.Hotkeys;
|
||||
|
||||
[Flags]
|
||||
public enum HotkeyModifiers : uint
|
||||
{
|
||||
None = 0,
|
||||
Alt = 0x0001,
|
||||
Control = 0x0002,
|
||||
Shift = 0x0004,
|
||||
Windows = 0x0008
|
||||
}
|
||||
|
||||
public sealed record HotkeyDefinition(HotkeyModifiers Modifiers, uint VirtualKey)
|
||||
{
|
||||
public static HotkeyDefinition Parse(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException("Hotkey cannot be empty.", nameof(value));
|
||||
}
|
||||
|
||||
var modifiers = HotkeyModifiers.None;
|
||||
uint? virtualKey = null;
|
||||
|
||||
foreach (var rawPart in value.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var part = rawPart.ToUpperInvariant();
|
||||
modifiers |= part switch
|
||||
{
|
||||
"CTRL" or "CONTROL" => HotkeyModifiers.Control,
|
||||
"ALT" => HotkeyModifiers.Alt,
|
||||
"SHIFT" => HotkeyModifiers.Shift,
|
||||
"WIN" or "WINDOWS" => HotkeyModifiers.Windows,
|
||||
_ => HotkeyModifiers.None
|
||||
};
|
||||
|
||||
if (modifiers.HasFlagForToken(part))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
virtualKey = ParseVirtualKey(part);
|
||||
}
|
||||
|
||||
return virtualKey is null
|
||||
? throw new FormatException($"Hotkey '{value}' does not contain a key.")
|
||||
: new HotkeyDefinition(modifiers, virtualKey.Value);
|
||||
}
|
||||
|
||||
private static uint ParseVirtualKey(string part)
|
||||
{
|
||||
if (part.Length == 1 && char.IsLetterOrDigit(part[0]))
|
||||
{
|
||||
return part[0];
|
||||
}
|
||||
|
||||
if (part.Length is 2 or 3 && part[0] == 'F' && int.TryParse(part[1..], out var functionKey) && functionKey is >= 1 and <= 24)
|
||||
{
|
||||
return (uint)(0x70 + functionKey - 1);
|
||||
}
|
||||
|
||||
throw new FormatException($"Unsupported hotkey key '{part}'.");
|
||||
}
|
||||
}
|
||||
|
||||
file static class HotkeyModifierExtensions
|
||||
{
|
||||
public static bool HasFlagForToken(this HotkeyModifiers _, string token)
|
||||
{
|
||||
return token is "CTRL" or "CONTROL" or "ALT" or "SHIFT" or "WIN" or "WINDOWS";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user