forked from Manuel/meeting-assistant
feat(macos): add native desktop controls and hotkey integration
- Introduced MacOsDesktopControlManifest to manage desktop control configurations. - Implemented MacOsDesktopControlService to handle the lifecycle of the macOS helper process. - Created MacOsWorkflowRulesEditorWindowService for managing workflow rules through a native window. - Developed a Swift helper for macOS that integrates with AppKit and Carbon for menu-bar controls and global hotkeys. - Added support for recording lifecycle actions via HTTP endpoints, maintaining consistency with existing Windows functionality. - Enhanced user experience by allowing macOS users to start/stop recordings and access settings through a menu-bar interface. - Documented design decisions, implementation evidence, and verification tasks for the new macOS features.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using MeetingAssistant.LaunchProfiles;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MeetingAssistant.MacOs;
|
||||
|
||||
public sealed class MacOsDesktopControlService : IHostedService, IDisposable
|
||||
{
|
||||
private readonly ILaunchProfileOptionsProvider launchProfiles;
|
||||
private readonly IOptions<MeetingAssistantOptions> options;
|
||||
private readonly IHostApplicationLifetime applicationLifetime;
|
||||
private readonly ILogger<MacOsDesktopControlService> logger;
|
||||
private Process? helper;
|
||||
private string? manifestPath;
|
||||
|
||||
public MacOsDesktopControlService(
|
||||
ILaunchProfileOptionsProvider launchProfiles,
|
||||
IOptions<MeetingAssistantOptions> options,
|
||||
IHostApplicationLifetime applicationLifetime,
|
||||
ILogger<MacOsDesktopControlService> logger)
|
||||
{
|
||||
this.launchProfiles = launchProfiles;
|
||||
this.options = options;
|
||||
this.applicationLifetime = applicationLifetime;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!OperatingSystem.IsMacOS())
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var helperPath = Path.Combine(AppContext.BaseDirectory, "Native", "macos-desktop-controls");
|
||||
if (!File.Exists(helperPath))
|
||||
{
|
||||
logger.LogWarning("macOS desktop controls helper was not found at {HelperPath}", helperPath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var manifest = MacOsDesktopControlManifest.Create(
|
||||
options.Value.Api.PublicBaseUrl,
|
||||
launchProfiles.GetProfiles(),
|
||||
launchProfiles.GetHotkeys());
|
||||
manifestPath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"meeting-assistant-desktop-{Environment.ProcessId}.json");
|
||||
File.WriteAllText(
|
||||
manifestPath,
|
||||
JsonSerializer.Serialize(
|
||||
manifest,
|
||||
new JsonSerializerOptions(JsonSerializerDefaults.Web)));
|
||||
|
||||
helper = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = helperPath,
|
||||
ArgumentList = { manifestPath },
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
});
|
||||
|
||||
if (helper is null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not start the macOS desktop controls helper.");
|
||||
}
|
||||
|
||||
helper.EnableRaisingEvents = true;
|
||||
helper.Exited += (_, _) =>
|
||||
{
|
||||
if (!applicationLifetime.ApplicationStopping.IsCancellationRequested)
|
||||
{
|
||||
logger.LogWarning("macOS desktop controls helper exited unexpectedly");
|
||||
}
|
||||
};
|
||||
logger.LogInformation("Started macOS menu-bar controls with {HotkeyCount} global hotkeys", manifest.Hotkeys.Count);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (helper is { HasExited: false })
|
||||
{
|
||||
helper.Kill();
|
||||
helper.WaitForExit(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
DeleteManifest();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
helper?.Dispose();
|
||||
DeleteManifest();
|
||||
}
|
||||
|
||||
private void DeleteManifest()
|
||||
{
|
||||
if (manifestPath is not null && File.Exists(manifestPath))
|
||||
{
|
||||
File.Delete(manifestPath);
|
||||
manifestPath = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user