Public Access
223 lines
7.3 KiB
C#
223 lines
7.3 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using MeetingAssistant.LaunchProfiles;
|
|
using MeetingAssistant.Recording;
|
|
|
|
namespace MeetingAssistant.Hotkeys;
|
|
|
|
public sealed class GlobalHotkeyService : BackgroundService
|
|
{
|
|
private const int HotkeyIdBase = 0x4D41;
|
|
private const int WmHotkey = 0x0312;
|
|
private const int WmQuit = 0x0012;
|
|
private readonly ILaunchProfileOptionsProvider launchProfiles;
|
|
private readonly MeetingRecordingCoordinator coordinator;
|
|
private readonly ILogger<GlobalHotkeyService> logger;
|
|
private readonly Dictionary<int, LaunchProfileHotkey> registeredHotkeys = [];
|
|
private TaskCompletionSource? messageLoopCompletion;
|
|
private uint messageThreadId;
|
|
|
|
public GlobalHotkeyService(
|
|
ILaunchProfileOptionsProvider launchProfiles,
|
|
MeetingRecordingCoordinator coordinator,
|
|
ILogger<GlobalHotkeyService> logger)
|
|
{
|
|
this.launchProfiles = launchProfiles;
|
|
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 hotkeys = launchProfiles.GetHotkeys();
|
|
messageThreadId = GetCurrentThreadId();
|
|
using var stoppingRegistration = stoppingToken.Register(() => PostQuitToMessageThread());
|
|
|
|
for (var index = 0; index < hotkeys.Count; index++)
|
|
{
|
|
var profileHotkey = hotkeys[index];
|
|
var hotkey = HotkeyDefinition.Parse(profileHotkey.Hotkey);
|
|
var hotkeyId = HotkeyIdBase + index;
|
|
if (!RegisterHotKey(IntPtr.Zero, hotkeyId, (uint)hotkey.Modifiers, hotkey.VirtualKey))
|
|
{
|
|
logger.LogWarning(
|
|
"Could not register global hotkey {Hotkey} for launch profile {LaunchProfile} action {Action}",
|
|
profileHotkey.Hotkey,
|
|
profileHotkey.ProfileName,
|
|
profileHotkey.Action);
|
|
continue;
|
|
}
|
|
|
|
registeredHotkeys[hotkeyId] = profileHotkey;
|
|
logger.LogInformation(
|
|
"Registered global hotkey {Hotkey} for launch profile {LaunchProfile} action {Action}",
|
|
profileHotkey.Hotkey,
|
|
profileHotkey.ProfileName,
|
|
profileHotkey.Action);
|
|
}
|
|
|
|
try
|
|
{
|
|
while (!stoppingToken.IsCancellationRequested && GetMessage(out var message, IntPtr.Zero, 0, 0) > 0)
|
|
{
|
|
if (message.Message == WmHotkey && registeredHotkeys.TryGetValue((int)message.WParam, out var hotkey))
|
|
{
|
|
_ = Task.Run(() => HandleHotkeyAsync(hotkey), CancellationToken.None);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
foreach (var hotkeyId in registeredHotkeys.Keys)
|
|
{
|
|
UnregisterHotKey(IntPtr.Zero, hotkeyId);
|
|
}
|
|
|
|
registeredHotkeys.Clear();
|
|
messageThreadId = 0;
|
|
}
|
|
}
|
|
|
|
public override Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
PostQuitToMessageThread();
|
|
return base.StopAsync(cancellationToken);
|
|
}
|
|
|
|
private async Task HandleHotkeyAsync(LaunchProfileHotkey hotkey)
|
|
{
|
|
switch (hotkey.Action)
|
|
{
|
|
case LaunchProfileHotkeyAction.ToggleRecording:
|
|
await ToggleRecordingAsync(hotkey.ProfileName);
|
|
break;
|
|
case LaunchProfileHotkeyAction.AbortRecording:
|
|
await AbortRecordingAsync(hotkey.ProfileName);
|
|
break;
|
|
case LaunchProfileHotkeyAction.CaptureScreenshot:
|
|
await CaptureScreenshotAsync(hotkey.ProfileName);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private async Task ToggleRecordingAsync(string profileName)
|
|
{
|
|
try
|
|
{
|
|
var status = await coordinator.ToggleAsync(profileName, CancellationToken.None);
|
|
logger.LogInformation(
|
|
"Hotkey toggled recording for launch profile {LaunchProfile}. IsRecording={IsRecording}",
|
|
profileName,
|
|
status.IsRecording);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
logger.LogError(exception, "Hotkey toggle failed");
|
|
}
|
|
}
|
|
|
|
private async Task AbortRecordingAsync(string profileName)
|
|
{
|
|
try
|
|
{
|
|
var status = await coordinator.AbortAsync(CancellationToken.None);
|
|
logger.LogInformation(
|
|
"Hotkey aborted recording for launch profile {LaunchProfile}. IsRecording={IsRecording}",
|
|
profileName,
|
|
status.IsRecording);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
logger.LogError(exception, "Hotkey abort failed");
|
|
}
|
|
}
|
|
|
|
private async Task CaptureScreenshotAsync(string profileName)
|
|
{
|
|
try
|
|
{
|
|
var result = await coordinator.CaptureScreenshotAsync(profileName, CancellationToken.None);
|
|
logger.LogInformation(
|
|
"Hotkey captured screenshot for launch profile {LaunchProfile}: {ScreenshotPath}",
|
|
profileName,
|
|
result.ScreenshotPath);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
logger.LogError(exception, "Hotkey screenshot capture 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;
|
|
}
|
|
}
|