using Microsoft.Extensions.Configuration; namespace MeetingAssistant.LaunchProfiles; public interface ILaunchProfileOptionsProvider { LaunchProfile GetRequiredProfile(string? name); IReadOnlyList GetProfiles(); IReadOnlyList GetHotkeys(); } public sealed record LaunchProfile(string Name, MeetingAssistantOptions Options); public enum LaunchProfileHotkeyAction { ToggleRecording, AbortRecording, CaptureScreenshot } public sealed record LaunchProfileHotkey( string ProfileName, string Hotkey, LaunchProfileHotkeyAction Action); public sealed class ConfigurationLaunchProfileOptionsProvider : ILaunchProfileOptionsProvider { public const string DefaultProfileName = "default"; private static readonly HotkeyDescriptor[] HotkeyDescriptors = [ new("Hotkey:Toggle", options => options.Hotkey.Toggle, LaunchProfileHotkeyAction.ToggleRecording), new("Hotkey:Abort", options => options.Hotkey.Abort, LaunchProfileHotkeyAction.AbortRecording), new("Screenshots:Hotkey", options => options.Screenshots.Hotkey, LaunchProfileHotkeyAction.CaptureScreenshot) ]; private readonly IConfiguration configuration; public ConfigurationLaunchProfileOptionsProvider(IConfiguration configuration) { this.configuration = configuration; } public LaunchProfile GetRequiredProfile(string? name) { var profileName = NormalizeProfileName(name); var options = BindDefaultOptions(); if (profileName.Equals(DefaultProfileName, StringComparison.OrdinalIgnoreCase)) { return new LaunchProfile(DefaultProfileName, options); } var profileSection = GetProfilesSection().GetSection(profileName); if (!profileSection.Exists()) { throw new KeyNotFoundException($"Launch profile '{profileName}' is not configured."); } profileSection.Bind(options); ApplyArrayOverrides(profileSection, options); return new LaunchProfile(profileName, options); } public IReadOnlyList GetHotkeys() { var hotkeys = GetProfileNames() .SelectMany(CreateHotkeys) .ToList(); var duplicate = hotkeys .GroupBy(hotkey => hotkey.Hotkey.Trim(), StringComparer.OrdinalIgnoreCase) .FirstOrDefault(group => group.Count() > 1); if (duplicate is not null) { throw new InvalidOperationException( $"Launch profile hotkey '{duplicate.Key}' is configured more than once for profiles {string.Join(", ", duplicate.Select(hotkey => $"{hotkey.ProfileName}:{hotkey.Action}"))}."); } return hotkeys; } public IReadOnlyList GetProfiles() { return GetProfileNames() .Select(GetRequiredProfile) .ToList(); } private IEnumerable CreateHotkeys(string profileName) { var profile = GetRequiredProfile(profileName); var isDefaultProfile = profileName.Equals(DefaultProfileName, StringComparison.OrdinalIgnoreCase); var profileSection = isDefaultProfile ? null : GetProfilesSection().GetSection(profileName); foreach (var descriptor in HotkeyDescriptors) { var hotkey = descriptor.GetValue(profile.Options); if (ShouldRegisterHotkey(isDefaultProfile, profileSection?.GetSection(descriptor.SectionPath)) && !string.IsNullOrWhiteSpace(hotkey)) { yield return new LaunchProfileHotkey( profile.Name, hotkey, descriptor.Action); } } } private static bool ShouldRegisterHotkey(bool isDefaultProfile, IConfigurationSection? profileHotkeySection) { return isDefaultProfile || profileHotkeySection?.Exists() == true; } private IReadOnlyList GetProfileNames() { return new[] { DefaultProfileName } .Concat(GetProfilesSection() .GetChildren() .Select(section => section.Key) .Where(key => !key.Equals(DefaultProfileName, StringComparison.OrdinalIgnoreCase))) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); } private MeetingAssistantOptions BindDefaultOptions() { var options = new MeetingAssistantOptions(); GetRootSection().Bind(options); return options; } private IConfigurationSection GetRootSection() { return configuration.GetSection("MeetingAssistant"); } private IConfigurationSection GetProfilesSection() { return GetRootSection().GetSection("LaunchProfiles"); } private static string NormalizeProfileName(string? name) { return string.IsNullOrWhiteSpace(name) || name.Equals(DefaultProfileName, StringComparison.OrdinalIgnoreCase) ? DefaultProfileName : name.Trim(); } private static void ApplyArrayOverrides( IConfigurationSection profileSection, MeetingAssistantOptions options) { var autoDetectLanguages = ReadArrayOverride(profileSection.GetSection("AzureSpeech:AutoDetectLanguages")); if (autoDetectLanguages is not null) { options.AzureSpeech.AutoDetectLanguages = autoDetectLanguages; } var funAsrChunkSize = ReadIntArrayOverride(profileSection.GetSection("FunAsr:ChunkSize")); if (funAsrChunkSize is not null) { options.FunAsr.ChunkSize = funAsrChunkSize; } } private static string[]? ReadArrayOverride(IConfigurationSection section) { if (!section.Exists()) { return null; } return section .GetChildren() .OrderBy(child => int.TryParse(child.Key, out var index) ? index : int.MaxValue) .Select(child => child.Value) .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value!) .ToArray(); } private static int[]? ReadIntArrayOverride(IConfigurationSection section) { if (!section.Exists()) { return null; } return section .GetChildren() .OrderBy(child => int.TryParse(child.Key, out var index) ? index : int.MaxValue) .Select(child => int.TryParse(child.Value, out var value) ? value : (int?)null) .Where(value => value.HasValue) .Select(value => value!.Value) .ToArray(); } private sealed record HotkeyDescriptor( string SectionPath, Func GetValue, LaunchProfileHotkeyAction Action); }