From 575047f353b035a32c019aeb02bb8ad36430f16b Mon Sep 17 00:00:00 2001 From: Filipe Moreira Date: Mon, 28 Sep 2026 11:06:28 -0300 Subject: [PATCH] feat: 1.6 is all our own code, no NirSoft fallback (#91) - Remove the MultiMonitorTool and SoundVolumeView backends, the NativeAudio / NativeDisplays flags, their paths, the "tool missing" strings (three catalogs), the SoundVolumeView volume parser and CsvReader. - MonitorService talks to a single DisplayBackend (Windows APIs only); AudioService uses Core Audio only. - On startup, delete the MultiMonitorTool.exe / SoundVolumeView.exe that 1.5 extracted into the tools folder. - Checklist and changelog updated. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 5 - CHANGELOG.en-US.md | 4 + CHANGELOG.md | 4 + docs/TESTING.md | 19 +- src/ConsoleMode/App.xaml.cs | 2 +- src/ConsoleMode/Models/Models.cs | 4 - src/ConsoleMode/Resources/Strings.en-US.json | 2 - src/ConsoleMode/Resources/Strings.es-ES.json | 2 - src/ConsoleMode/Resources/Strings.pt-BR.json | 2 - src/ConsoleMode/Services/AppPaths.cs | 24 +- src/ConsoleMode/Services/AudioService.cs | 105 +------- src/ConsoleMode/Services/ConsoleEngine.cs | 28 +-- src/ConsoleMode/Services/DisplayBackend.cs | 127 ++++++++++ src/ConsoleMode/Services/DisplayBackends.cs | 244 ------------------- src/ConsoleMode/Services/MonitorService.cs | 27 +- src/ConsoleMode/Services/ProcessRunner.cs | 63 ----- src/ConsoleMode/Services/SessionMenuMath.cs | 4 - src/ConsoleMode/ViewModels/MainViewModel.cs | 29 +-- tests/ConsoleMode.Tests/SessionMenuTests.cs | 9 - 19 files changed, 190 insertions(+), 514 deletions(-) create mode 100644 src/ConsoleMode/Services/DisplayBackend.cs delete mode 100644 src/ConsoleMode/Services/DisplayBackends.cs diff --git a/.gitignore b/.gitignore index 0758734..69ee38c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,6 @@ dist/ artifacts/ release-notes.md -# Ferramentas NirSoft (baixadas pelo build ou colocadas localmente em dev) -MultiMonitorTool.exe -MultiMonitorTool.chm -SoundVolumeView.exe -SoundVolumeView.chm rtss-cli.exe readme.txt diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 4932921..d602f3c 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -2,6 +2,10 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Before publishing a version, add a `## [VERSION]` section to **both** files: the workflow publishes the section matching the tag from each one and fails if either is missing. +## [Unreleased] +### Changes +- 1.6 is all the project's own code: the option to go back to MultiMonitorTool / SoundVolumeView is gone too, and their old files in the `tools` folder are deleted when the app opens. (#91) + ## [1.6.0-alpha.1] ### What's new - Displays and audio are now controlled through Windows' own APIs. The app no longer bundles NirSoft's MultiMonitorTool or SoundVolumeView: the package holds only the project's code and rtss-cli (MIT), which paves the way for a signed executable and fewer antivirus false positives. (#91, #97, #98, #99) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b4e64..7a40dc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CHANGELOG.en-US.md`. Antes de publicar uma versão, adicione uma seção `## [VERSÃO]` com o changelog daquela versão **nos dois arquivos**. O workflow publica as duas seções correspondentes à tag na mesma release e falha se faltar alguma. +## [Unreleased] +### Mudanças +- A 1.6 é só código do projeto: saiu também a opção de voltar ao MultiMonitorTool / SoundVolumeView, e os arquivos antigos deles na pasta `tools` são apagados ao abrir o app. (#91) + ## [1.6.0-alpha.1] ### Novidades - Telas e áudio agora são controlados pelas APIs do próprio Windows. O app não embute mais o MultiMonitorTool nem o SoundVolumeView, da NirSoft: o pacote fica só com código do projeto e o rtss-cli (MIT), o que abre caminho para assinar o executável e reduz alertas falsos de antivírus. (#91, #97, #98, #99) diff --git a/docs/TESTING.md b/docs/TESTING.md index 2fb122c..ba03f50 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -114,26 +114,26 @@ Pré-condições: Ajustes → Interface = **Automático** (padrão). - [ ] Atualização: com uma release mais nova no GitHub, o aviso aparece nas duas interfaces (na Console, como banner no topo). Resultado: ______ - [ ] Atalho "Console Mode 1 Click" e `--tray` no início do Windows continuam funcionando. Resultado: ______ -## Áudio nativo, sem SoundVolumeView (issue #91) +## Áudio com código próprio (issue #91) -Pré-condições: a partir da 1.6 o áudio nativo já vem ligado (`"NativeAudio": true` em `config.json`) e o SoundVolumeView não é mais embutido. O log (`consolemode.log`) deve mostrar `audio=native` na linha `Startup:`. Para comparar com o SoundVolumeView, coloque o `SoundVolumeView.exe` na pasta `tools` e troque para `false`. +Pré-condições: 1.6 instalada por cima da 1.5, com a configuração da 1.5 (saída de áudio escolhida). A 1.6 não usa mais o SoundVolumeView; o arquivo antigo em `tools` é apagado ao abrir o app. -- [ ] Ajustes → Saída de áudio lista as mesmas saídas que com o SoundVolumeView, com os mesmos nomes, e as desativadas aparecem com o sufixo "[Desabilitado]". Resultado: ______ -- [ ] A saída que já estava escolhida com o SoundVolumeView continua selecionada (o ID salvo é o mesmo). Resultado: ______ +- [ ] Ajustes → Saída de áudio lista as mesmas saídas que na 1.5, com os mesmos nomes, e as desativadas aparecem com o sufixo "[Desabilitado]". Resultado: ______ +- [ ] A saída que já estava escolhida com o SoundVolumeView continua selecionada depois de atualizar da 1.5 (o ID salvo é o mesmo). Resultado: ______ - [ ] Entrar no modo console com uma saída fixa (ex.: HDMI da TV): o som passa para ela, inclusive em apps de chamada (papel "comunicações"). O log mostra `Áudio: saída padrão = …`. Resultado: ______ - [ ] "A que aparecer ao conectar (TV)" com a TV começando **desligada**: quando a TV liga, o som vai para o HDMI dela. Resultado: ______ - [ ] Uma saída **desabilitada** em Configurações → Som: escolhê-la liga a saída e o som vai para ela. Resultado: ______ - [ ] Menu da sessão (Select + Y): o volume inicial é o do Windows; ◀/▶ muda de 5 em 5 e tira o mudo; A alterna o mudo. Resultado: ______ - [ ] Ao restaurar a mesa, o som volta para a saída de antes do modo console. Resultado: ______ -- [ ] Com `"NativeAudio": true` e **sem** o `SoundVolumeView.exe` na pasta `tools`, tudo acima continua funcionando. Resultado: ______ +- [ ] A pasta `tools` não tem mais `SoundVolumeView.exe` nem `MultiMonitorTool.exe` depois de abrir o app. Resultado: ______ -## Telas nativas, sem MultiMonitorTool (issue #91) +## Telas com código próprio (issue #91) -Pré-condições: a partir da 1.6 as telas nativas já vêm ligadas (`"NativeDisplays": true` em `config.json`) e o MultiMonitorTool não é mais embutido. A linha `Startup:` do log deve mostrar `displays=native`. Para comparar com o MultiMonitorTool, coloque o `MultiMonitorTool.exe` na pasta `tools` e troque para `false` (ou use a 1.5.0). +Pré-condições: 1.6 instalada por cima da 1.5, com a configuração da 1.5 (tela de jogo e telas a esconder escolhidas). Anote antes, na 1.5, como o mapa de telas aparece, para comparar. A 1.6 não usa mais o MultiMonitorTool; o arquivo antigo em `tools` é apagado ao abrir o app. ### Lista e identidade -- [ ] O mapa de telas mostra os mesmos monitores que com o MultiMonitorTool, com os mesmos nomes, resoluções e posições, inclusive a TV **desligada/desconectada**. Resultado: ______ +- [ ] O mapa de telas mostra os mesmos monitores que na 1.5, com os mesmos nomes, resoluções e posições, inclusive a TV **desligada/desconectada**. Resultado: ______ - [ ] A tela de jogo e as telas a esconder que já estavam salvas continuam marcadas (o ID estável é o mesmo). Resultado: ______ - [ ] A lista de resoluções de cada tela continua igual. Resultado: ______ @@ -154,5 +154,4 @@ Pré-condições: a partir da 1.6 as telas nativas já vêm ligadas (`"NativeDis - [ ] Big Picture ou Playnite abrindo na tela errada é movido para a TV. Resultado: ______ - [ ] Fechar o app no meio da sessão e abrir de novo: "Restaurar setup" traz a mesa de volta a partir do backup. Resultado: ______ -- [ ] Um backup antigo, feito com o MultiMonitorTool, também é restaurado com `"NativeDisplays": true`. Resultado: ______ -- [ ] Com `"NativeDisplays": true` e **sem** o `MultiMonitorTool.exe` na pasta `tools`, tudo acima continua funcionando. Resultado: ______ +- [ ] Um backup da mesa feito pela 1.5 (sessão iniciada na 1.5, app atualizado antes de restaurar) é restaurado pela 1.6. Resultado: ______ diff --git a/src/ConsoleMode/App.xaml.cs b/src/ConsoleMode/App.xaml.cs index 2e98729..a00e973 100644 --- a/src/ConsoleMode/App.xaml.cs +++ b/src/ConsoleMode/App.xaml.cs @@ -77,7 +77,7 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) AppLog.Write("Controles: " + ControllerInput.DescribeDevices().ReplaceLineEndings(" | ")); ProtocolService.EnsureRegistered(); AppLog.Write($"Startup: exe={Environment.ProcessPath}, args={string.Join(' ', Environment.GetCommandLineArgs().Skip(1))}, " + - $"mmt={AppPaths.HasMmt}, displays={(MonitorService.UseNative ? "native" : "mmt")}, svv={AppPaths.HasSvv}, audio={(AudioService.UseNative ? "native" : "svv")}, rtss-cli={AppPaths.HasRtssCli}"); + $"rtss-cli={AppPaths.HasRtssCli}"); ViewModel = new MainViewModel(); _window = new MainWindow(ViewModel); diff --git a/src/ConsoleMode/Models/Models.cs b/src/ConsoleMode/Models/Models.cs index 71887b5..ab0cb09 100644 --- a/src/ConsoleMode/Models/Models.cs +++ b/src/ConsoleMode/Models/Models.cs @@ -19,10 +19,6 @@ public sealed class AppConfig public string AudioDeviceId { get; set; } = ""; public string AudioDeviceName { get; set; } = ""; public bool AudioAutoSwitch { get; set; } - /// Core Audio instead of SoundVolumeView (issue #91). On since 1.6; false needs SoundVolumeView.exe in tools. - public bool NativeAudio { get; set; } = true; - /// Windows' display APIs instead of MultiMonitorTool (issue #91). On since 1.6; false needs MultiMonitorTool.exe in tools. - public bool NativeDisplays { get; set; } = true; public int FpsLimit { get; set; } public Dictionary MonitorModes { get; set; } = new(StringComparer.OrdinalIgnoreCase); public bool HdrEnable { get; set; } diff --git a/src/ConsoleMode/Resources/Strings.en-US.json b/src/ConsoleMode/Resources/Strings.en-US.json index 2174c40..f9a652d 100644 --- a/src/ConsoleMode/Resources/Strings.en-US.json +++ b/src/ConsoleMode/Resources/Strings.en-US.json @@ -145,12 +145,10 @@ "FpsUnavailable": "Optional. Install RivaTuner Statistics Server (RTSS) to use this feature.", "NoGameDisplayConfigured": "No gaming display is configured; unable to enter console mode.", "NoDisplayTryRefresh": "No displays found. Try refreshing.", - "MmtMissing": "MultiMonitorTool.exe was not found at {0}", "ListDisplaysError": "Could not list displays: {0}", "DisplayNoChange": "Don't change", "AudioNoChange": "Don't change", "AudioOnConnect": "The one that appears when connected (TV)", - "SvvMissing": "SoundVolumeView.exe was not found in ConsoleMode_Data/tools; audio switching is disabled.", "ShortcutCreated": "Created the \"Console Mode 1 Click\" desktop shortcut.", "ShortcutCreateError": "Could not create shortcut: {0}", "OpenFolderError": "Could not open folder: {0}", diff --git a/src/ConsoleMode/Resources/Strings.es-ES.json b/src/ConsoleMode/Resources/Strings.es-ES.json index c3d835b..53f3eb0 100644 --- a/src/ConsoleMode/Resources/Strings.es-ES.json +++ b/src/ConsoleMode/Resources/Strings.es-ES.json @@ -145,12 +145,10 @@ "FpsUnavailable": "Opcional. Instala RivaTuner Statistics Server (RTSS) para usar esta función.", "NoGameDisplayConfigured": "No hay ninguna pantalla de juego configurada; no se puede entrar en modo consola.", "NoDisplayTryRefresh": "No se encontraron pantallas. Prueba a actualizar.", - "MmtMissing": "No se encontró MultiMonitorTool.exe en {0}", "ListDisplaysError": "No se pudieron listar las pantallas: {0}", "DisplayNoChange": "No cambiar", "AudioNoChange": "No cambiar", "AudioOnConnect": "La que aparece al conectar (TV)", - "SvvMissing": "No se encontró SoundVolumeView.exe en ConsoleMode_Data/tools; el cambio de audio está desactivado.", "ShortcutCreated": "Se creó el acceso directo \"Console Mode 1 Click\" en el escritorio.", "ShortcutCreateError": "No se pudo crear el acceso directo: {0}", "OpenFolderError": "No se pudo abrir la carpeta: {0}", diff --git a/src/ConsoleMode/Resources/Strings.pt-BR.json b/src/ConsoleMode/Resources/Strings.pt-BR.json index 8b74215..7b8e612 100644 --- a/src/ConsoleMode/Resources/Strings.pt-BR.json +++ b/src/ConsoleMode/Resources/Strings.pt-BR.json @@ -145,12 +145,10 @@ "FpsUnavailable": "Opcional. Instale o RivaTuner Statistics Server (RTSS) para usar.", "NoGameDisplayConfigured": "Nenhuma tela de jogo configurada; não foi possível entrar no modo console.", "NoDisplayTryRefresh": "Nenhuma tela encontrada. Tente atualizar.", - "MmtMissing": "MultiMonitorTool.exe não encontrado em {0}", "ListDisplaysError": "Não foi possível listar as telas: {0}", "DisplayNoChange": "Não alterar", "AudioNoChange": "Não alterar", "AudioOnConnect": "A que aparecer ao conectar (TV)", - "SvvMissing": "SoundVolumeView.exe não encontrado em ConsoleMode_Data/tools; a troca de áudio fica desativada.", "ShortcutCreated": "Atalho \"Console Mode 1 Click\" criado na Área de Trabalho.", "ShortcutCreateError": "Não foi possível criar o atalho: {0}", "OpenFolderError": "Não foi possível abrir a pasta: {0}", diff --git a/src/ConsoleMode/Services/AppPaths.cs b/src/ConsoleMode/Services/AppPaths.cs index 6f280cb..701bbc5 100644 --- a/src/ConsoleMode/Services/AppPaths.cs +++ b/src/ConsoleMode/Services/AppPaths.cs @@ -50,8 +50,6 @@ public static class AppPaths public static string ExeDir { get; private set; } = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); public static string DataDir { get; private set; } = ""; public static string ToolsDir { get; private set; } = ""; - public static string MmtPath { get; private set; } = ""; - public static string SvvPath { get; private set; } = ""; public static string RtssCliPath { get; private set; } = ""; public static string ConfigPath { get; private set; } = ""; public static string BackupMonitorConfig { get; private set; } = ""; @@ -93,19 +91,19 @@ public static void Initialize() EmbeddedFiles.ExtractTools(ToolsDir); EmbeddedFiles.ExtractIcon(Path.Combine(DataDir, "icon.ico")); - foreach (var name in new[] { "MultiMonitorTool.exe", "SoundVolumeView.exe", "rtss-cli.exe" }) + var rtssBeside = Path.Combine(ExeDir, "rtss-cli.exe"); + var rtssDest = Path.Combine(ToolsDir, "rtss-cli.exe"); + if (!File.Exists(rtssDest) && File.Exists(rtssBeside)) { - var dest = Path.Combine(ToolsDir, name); - if (File.Exists(dest)) continue; - var beside = Path.Combine(ExeDir, name); - if (File.Exists(beside)) - { - try { File.Copy(beside, dest, true); } catch { /* ignore */ } - } + try { File.Copy(rtssBeside, rtssDest, true); } catch { /* ignore */ } + } + + // Up to 1.5 the NirSoft tools were extracted here; 1.6 is all our own code (issue #91). + foreach (var name in new[] { "MultiMonitorTool.exe", "SoundVolumeView.exe" }) + { + try { File.Delete(Path.Combine(ToolsDir, name)); } catch { /* in use or read-only: harmless */ } } - MmtPath = FirstExisting(Path.Combine(ToolsDir, "MultiMonitorTool.exe"), Path.Combine(ExeDir, "MultiMonitorTool.exe")); - SvvPath = FirstExisting(Path.Combine(ToolsDir, "SoundVolumeView.exe"), Path.Combine(ExeDir, "SoundVolumeView.exe")); RtssCliPath = FirstExisting(Path.Combine(ToolsDir, "rtss-cli.exe"), Path.Combine(ExeDir, "rtss-cli.exe")); ConfigPath = Path.Combine(DataDir, "config.json"); @@ -130,8 +128,6 @@ public static void Initialize() } } - public static bool HasMmt => File.Exists(MmtPath); - public static bool HasSvv => File.Exists(SvvPath); public static bool HasRtssCli => File.Exists(RtssCliPath); private static string FirstExisting(params string[] paths) diff --git a/src/ConsoleMode/Services/AudioService.cs b/src/ConsoleMode/Services/AudioService.cs index 9ab31e5..e0c31aa 100644 --- a/src/ConsoleMode/Services/AudioService.cs +++ b/src/ConsoleMode/Services/AudioService.cs @@ -5,47 +5,24 @@ namespace ConsoleMode.Services; public sealed class AudioService { - private static bool? _useNative; private List? _cache; private string? _cachedDefaultId; - // Native backend: friendly ID (what config and backups store) -> Core Audio endpoint ID. + // Friendly ID (what config and backups store) -> Core Audio endpoint ID. private Dictionary _endpointIds = new(StringComparer.OrdinalIgnoreCase); - /// - /// Core Audio instead of SoundVolumeView (issue #91). On by default since 1.6; "NativeAudio": false - /// in config.json goes back to SoundVolumeView, which is no longer bundled (put it in the tools folder). - /// - public static bool UseNative => _useNative ??= LoadNativeFlag(); - - /// Audio switching works: the native backend is on, or SoundVolumeView is present. - public static bool IsAvailable => UseNative || AppPaths.HasSvv; - - private static bool LoadNativeFlag() - { - try { return ConfigService.Load().NativeAudio; } - catch { return false; } - } - public void ClearCache() { _cache = null; _cachedDefaultId = null; } - public int InvokeSvv(params string[] args) - { - if (!AppPaths.HasSvv) throw new InvalidOperationException(LocalizationService.Get("SvvMissing")); - return ProcessRunner.Run(AppPaths.SvvPath, args); - } - public IReadOnlyList GetDevices(bool forceRefresh = false) { - if (!IsAvailable) return []; if (!forceRefresh && _cache is not null) return _cache; try { - var devices = UseNative ? ReadNativeDevices() : ReadSvvDevices(); + var devices = ReadNativeDevices(); _cache = [.. devices.OrderBy(d => d.IsActive ? 0 : 1).ThenBy(d => d.Name)]; } catch (Exception ex) @@ -80,39 +57,6 @@ private List ReadNativeDevices() return devices; } - private List ReadSvvDevices() - { - var csvPath = Path.Combine(Path.GetTempPath(), $"consolemode_audio_{Guid.NewGuid():N}.csv"); - try - { - InvokeSvv("/ShowDisabledDevices", "1", "/ShowUnpluggedDevices", "1", "/scomma", csvPath); - if (!File.Exists(csvPath)) return []; - - var devices = new List(); - foreach (var row in CsvReader.Read(csvPath)) - { - var friendlyId = row.Get("Command-Line Friendly ID"); - if (string.IsNullOrWhiteSpace(friendlyId)) continue; - if (!string.Equals(row.Get("Type"), "Device", StringComparison.OrdinalIgnoreCase)) continue; - if (!string.Equals(row.Get("Direction"), "Render", StringComparison.OrdinalIgnoreCase)) continue; - - var deviceState = row.Get("Device State"); - devices.Add(new AudioDevice - { - Name = AudioNaming.DisplayName(row.Get("Name"), row.Get("Device Name")), - FriendlyId = friendlyId, - IsDefault = row.Get("Default").Contains("Render", StringComparison.OrdinalIgnoreCase), - IsActive = deviceState.Contains("Active", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(deviceState) - }); - } - return devices; - } - finally - { - try { File.Delete(csvPath); } catch { /* ignore */ } - } - } - public string? GetDefaultId() { if (_cachedDefaultId is not null) return _cachedDefaultId; @@ -121,18 +65,10 @@ private List ReadSvvDevices() public void SetOutput(string friendlyId) { - if (UseNative) - { - var endpointId = ResolveEndpoint(friendlyId); - if (GetDevices().FirstOrDefault(d => d.FriendlyId == friendlyId) is { IsActive: false }) - CoreAudio.Enable(endpointId); - CoreAudio.SetDefault(endpointId); - } - else - { - InvokeSvv("/Enable", friendlyId); - InvokeSvv("/SetDefault", friendlyId, "all"); - } + var endpointId = ResolveEndpoint(friendlyId); + if (GetDevices().FirstOrDefault(d => d.FriendlyId == friendlyId) is { IsActive: false }) + CoreAudio.Enable(endpointId); + CoreAudio.SetDefault(endpointId); AppLog.Write($"Áudio: saída padrão = {friendlyId}"); _cachedDefaultId = friendlyId; _cache = null; @@ -140,40 +76,25 @@ public void SetOutput(string friendlyId) public void Restore(string? backupId) { - if (string.IsNullOrWhiteSpace(backupId) || !IsAvailable) return; - try - { - if (UseNative) CoreAudio.SetDefault(ResolveEndpoint(backupId)); - else InvokeSvv("/SetDefault", backupId, "all"); - } + if (string.IsNullOrWhiteSpace(backupId)) return; + try { CoreAudio.SetDefault(ResolveEndpoint(backupId)); } catch (Exception ex) { AppLog.Write($"Não foi possível restaurar o áudio: {ex.Message}"); } } /// Output volume from 0 to 100, or null when it can't be read. - public int? GetVolumePercent(string friendlyId) => UseNative - ? CoreAudio.GetVolumePercent(ResolveEndpoint(friendlyId)) - : SessionMenuMath.ParseVolumeExitCode(InvokeSvv("/GetPercent", friendlyId)); + public int? GetVolumePercent(string friendlyId) => CoreAudio.GetVolumePercent(ResolveEndpoint(friendlyId)); /// Sets the volume and unmutes, like turning the knob. public void SetVolumePercent(string friendlyId, int percent) { - if (UseNative) - { - var endpointId = ResolveEndpoint(friendlyId); - CoreAudio.SetVolumePercent(endpointId, percent); - CoreAudio.SetMute(endpointId, false); - } - else - { - InvokeSvv("/SetVolume", friendlyId, percent.ToString()); - InvokeSvv("/Unmute", friendlyId); - } + var endpointId = ResolveEndpoint(friendlyId); + CoreAudio.SetVolumePercent(endpointId, percent); + CoreAudio.SetMute(endpointId, false); } public void SetMute(string friendlyId, bool mute) { - if (UseNative) CoreAudio.SetMute(ResolveEndpoint(friendlyId), mute); - else InvokeSvv(mute ? "/Mute" : "/Unmute", friendlyId); + CoreAudio.SetMute(ResolveEndpoint(friendlyId), mute); } private string ResolveEndpoint(string friendlyId) diff --git a/src/ConsoleMode/Services/ConsoleEngine.cs b/src/ConsoleMode/Services/ConsoleEngine.cs index cd7d8bc..7e4a38e 100644 --- a/src/ConsoleMode/Services/ConsoleEngine.cs +++ b/src/ConsoleMode/Services/ConsoleEngine.cs @@ -81,12 +81,9 @@ public void Start(AppConfig config, MonitorInfo? focusInfo, Func d.FriendlyId == config.AudioDeviceId); @@ -150,16 +147,13 @@ public void Start(AppConfig config, MonitorInfo? focusInfo, Func +/// Single display commands through Windows' own APIs: CCD, ChangeDisplaySettingsEx and DDC/CI +/// (issue #91). keeps the orchestration (retries, waits, fallbacks). +/// +internal sealed class DisplayBackend +{ + public List ListMonitors() + { + var result = new List(); + foreach (var m in NativeDisplays.ListMonitors()) + { + DisplayIdentity.TryParseDevicePath(m.DevicePath, out var hardwareId, out var instanceId); + var (driverKey, edid) = ReadRegistry(instanceId); + + var current = m.IsActive ? NativeWindows.GetCurrentDisplayMode(m.GdiName) : null; + int maxW = edid.PreferredWidth, maxH = edid.PreferredHeight; + if (m.IsActive) + { + foreach (var mode in NativeWindows.EnumerateDisplayModes(m.GdiName)) + { + if ((long)mode.Width * mode.Height <= (long)maxW * maxH) continue; + maxW = mode.Width; + maxH = mode.Height; + } + } + var width = current?.Width ?? maxW; + var height = current?.Height ?? maxH; + var maxText = maxW > 0 ? $"{maxW} X {maxH}" : ""; + + result.Add(new MonitorInfo + { + Name = m.GdiName, + Resolution = current is not null ? $"{current.Width} X {current.Height}" : maxText.Length > 0 ? maxText : "N/A", + MaximumResolution = maxText, + MaxWidth = maxW, + MaxHeight = maxH, + Width = width, + Height = height, + Frequency = current?.Frequency > 0 ? current.Frequency.ToString() : "", + Colors = current?.BitsPerPel > 0 ? current.BitsPerPel.ToString() : "", + IsPrimary = m.IsActive && m.PositionX == 0 && m.PositionY == 0, + IsActive = m.IsActive, + IsDisconnected = !m.IsActive, + MonitorName = !string.IsNullOrWhiteSpace(m.FriendlyName) ? m.FriendlyName : edid.Name, + ShortId = hardwareId, + MonitorId = DisplayIdentity.MonitorId(hardwareId, driverKey), + SerialNumber = edid.Serial, + LeftTop = m.IsActive ? $"{m.PositionX}, {m.PositionY}" : "" + }); + } + return result; + } + + private static (string? DriverKey, EdidInfo Edid) ReadRegistry(string instanceId) + { + var empty = new EdidInfo("", "", 0, 0); + if (string.IsNullOrWhiteSpace(instanceId)) return (null, empty); + try + { + using var key = Registry.LocalMachine.OpenSubKey($@"SYSTEM\CurrentControlSet\Enum\{instanceId}"); + if (key is null) return (null, empty); + using var parameters = key.OpenSubKey("Device Parameters"); + return (key.GetValue("Driver") as string, DisplayIdentity.ParseEdid(parameters?.GetValue("EDID") as byte[])); + } + catch (Exception ex) + { + AppLog.Write($"Telas: registro de {instanceId}: {ex.Message}"); + return (null, empty); + } + } + + public void SaveLayout(string path) + { + var entries = ListMonitors().Select(m => + { + var current = m.IsActive ? NativeWindows.GetCurrentDisplayMode(m.Name) : null; + MonitorService.ParseLeftTop(m.LeftTop, out var x, out var y); + return new LayoutEntry(m.Name, m.MonitorId, m.SerialNumber, current?.BitsPerPel ?? 0, + current?.Width ?? 0, current?.Height ?? 0, current?.Frequency ?? 0, x ?? 0, y ?? 0); + }); + File.WriteAllText(path, DisplayIdentity.FormatLayout(entries)); + } + + public void LoadLayout(string path) + { + var specs = ResolveLayout(DisplayIdentity.ParseLayout(File.ReadAllLines(path))); + foreach (var (name, spec) in specs) + { + int.TryParse(spec.GetValueOrDefault("Width"), out var w); + int.TryParse(spec.GetValueOrDefault("Height"), out var h); + if (w <= 0 || h <= 0) continue; + int.TryParse(spec.GetValueOrDefault("DisplayFrequency"), out var f); + int.TryParse(spec.GetValueOrDefault("BitsPerPixel"), out var bits); + int? x = int.TryParse(spec.GetValueOrDefault("PositionX"), out var px) ? px : null; + int? y = int.TryParse(spec.GetValueOrDefault("PositionY"), out var py) ? py : null; + NativeDisplays.QueueMode(name, w, h, f, bits, x, y); + } + var code = NativeDisplays.ApplyPending(); + AppLog.Write($"Telas: layout restaurado => {code}"); + } + + public Dictionary> ResolveLayout(Dictionary> specs) => + DisplayIdentity.RemapLayoutNames(specs, ListMonitors().Select(m => (m.MonitorId, m.Name))); + + public void Enable(IReadOnlyList names) => NativeDisplays.Enable(names); + public void Disable(IReadOnlyList names) => NativeDisplays.Disable(names); + public void SetPrimary(string name) => CcdHelper.SetPrimary(name); + public void PowerOn(string name) => NativeDisplays.SetPower(name, on: true); + public void PowerOff(string name) => NativeDisplays.SetPower(name, on: false); + + public void SetMode(string name, int width, int height, int frequency, string? colors, int? x, int? y, bool primary) + { + int.TryParse(colors, out var bits); + NativeDisplays.QueueMode(name, width, height, frequency, bits, x, y); + NativeDisplays.ApplyPending(); + if (primary) CcdHelper.SetPrimary(name); + } + + public void MoveProcessWindows(string monitorName, string processName, ScreenRect rect) => + NativeDisplays.MoveProcessWindows(processName, rect); +} diff --git a/src/ConsoleMode/Services/DisplayBackends.cs b/src/ConsoleMode/Services/DisplayBackends.cs deleted file mode 100644 index 72fb992..0000000 --- a/src/ConsoleMode/Services/DisplayBackends.cs +++ /dev/null @@ -1,244 +0,0 @@ -using ConsoleMode.Models; -using ConsoleMode.Native; -using Microsoft.Win32; - -namespace ConsoleMode.Services; - -/// -/// The low-level display operations needs. MonitorService keeps the -/// orchestration (retries, waits, fallbacks); a backend only runs single commands. -/// -internal interface IDisplayBackend -{ - string Name { get; } - bool IsAvailable { get; } - List ListMonitors(); - void SaveLayout(string path); - void LoadLayout(string path); - /// Layout backup sections keyed by the names monitors have now. - Dictionary> ResolveLayout(Dictionary> specs); - void Enable(IReadOnlyList names); - void Disable(IReadOnlyList names); - void SetPrimary(string name); - void SetMode(string name, int width, int height, int frequency, string? colors, int? x, int? y, bool primary); - void PowerOn(string name); - void PowerOff(string name); - void MoveProcessWindows(string monitorName, string processName, ScreenRect rect); -} - -/// NirSoft MultiMonitorTool, the original backend. -internal sealed class MmtDisplayBackend : IDisplayBackend -{ - public string Name => "mmt"; - public bool IsAvailable => AppPaths.HasMmt; - - private static int Run(params string[] args) - { - if (!AppPaths.HasMmt) throw new InvalidOperationException(LocalizationService.Get("MmtMissing", AppPaths.MmtPath)); - return ProcessRunner.Run(AppPaths.MmtPath, args); - } - - public List ListMonitors() - { - if (!AppPaths.HasMmt) return []; - var csvPath = Path.Combine(Path.GetTempPath(), $"consolemode_monitors_{Guid.NewGuid():N}.csv"); - try - { - Run("/HideInactiveMonitors", "0", "/scomma", csvPath); - if (!File.Exists(csvPath)) return []; - - var monitors = new List(); - foreach (var row in CsvReader.Read(csvPath)) - { - var name = row.Get("Name"); - if (string.IsNullOrWhiteSpace(name)) continue; - var isActive = string.Equals(row.Get("Active"), "Yes", StringComparison.OrdinalIgnoreCase); - var maxResolution = row.Get("Maximum Resolution"); - var resolution = row.Get("Resolution"); - if (string.IsNullOrWhiteSpace(resolution)) - resolution = string.IsNullOrWhiteSpace(maxResolution) ? "N/A" : maxResolution; - - MonitorService.ParseRes(resolution, out var width, out var height); - MonitorService.ParseRes(maxResolution, out var maxW, out var maxH); - if (maxW <= 0 && width > 0) { maxW = width; maxH = height; } - - monitors.Add(new MonitorInfo - { - Name = name, - Resolution = resolution, - MaximumResolution = maxResolution, - MaxWidth = maxW, - MaxHeight = maxH, - Width = width, - Height = height, - Frequency = row.Get("Frequency"), - Colors = row.Get("Colors"), - IsPrimary = string.Equals(row.Get("Primary"), "Yes", StringComparison.OrdinalIgnoreCase), - IsActive = isActive, - IsDisconnected = string.Equals(row.Get("Disconnected"), "Yes", StringComparison.OrdinalIgnoreCase) || !isActive, - MonitorName = row.Get("Monitor Name"), - ShortId = row.Get("Short Monitor ID"), - MonitorId = row.Get("Monitor ID"), - SerialNumber = row.Get("Monitor Serial Number"), - LeftTop = row.Get("Left-Top") - }); - } - return monitors; - } - finally - { - try { File.Delete(csvPath); } catch { /* ignore */ } - } - } - - public void SaveLayout(string path) => Run("/SaveConfig", path); - public void LoadLayout(string path) => Run("/LoadConfig", path); - public Dictionary> ResolveLayout(Dictionary> specs) => specs; - public void Enable(IReadOnlyList names) => Run(["/enable", .. names]); - public void Disable(IReadOnlyList names) => Run(["/disable", .. names]); - public void SetPrimary(string name) => Run("/SetPrimary", name); - public void PowerOn(string name) => Run("/TurnOn", name); - public void PowerOff(string name) => Run("/TurnOff", name); - - public void SetMode(string name, int width, int height, int frequency, string? colors, int? x, int? y, bool primary) - { - var spec = primary ? $"Name={name} Primary=Yes" : $"Name={name}"; - if (width > 0) spec += $" Width={width}"; - if (height > 0) spec += $" Height={height}"; - if (frequency > 0) spec += $" DisplayFrequency={frequency}"; - if (!string.IsNullOrWhiteSpace(colors)) spec += $" BitsPerPixel={colors}"; - if (x is not null) spec += $" PositionX={x}"; - if (y is not null) spec += $" PositionY={y}"; - Run("/SetMonitors", spec); - } - - public void MoveProcessWindows(string monitorName, string processName, ScreenRect rect) => - Run("/MoveWindow", monitorName, - "Process", processName, - "/WindowLeft", rect.X.ToString(), - "/WindowTop", rect.Y.ToString(), - "/WindowWidth", rect.Width.ToString(), - "/WindowHeight", rect.Height.ToString()); -} - -/// Windows' own display APIs (issue #91): CCD, ChangeDisplaySettingsEx and DDC/CI. -internal sealed class NativeDisplayBackend : IDisplayBackend -{ - public string Name => "native"; - public bool IsAvailable => true; - - public List ListMonitors() - { - var result = new List(); - foreach (var m in NativeDisplays.ListMonitors()) - { - DisplayIdentity.TryParseDevicePath(m.DevicePath, out var hardwareId, out var instanceId); - var (driverKey, edid) = ReadRegistry(instanceId); - - var current = m.IsActive ? NativeWindows.GetCurrentDisplayMode(m.GdiName) : null; - int maxW = edid.PreferredWidth, maxH = edid.PreferredHeight; - if (m.IsActive) - { - foreach (var mode in NativeWindows.EnumerateDisplayModes(m.GdiName)) - { - if ((long)mode.Width * mode.Height <= (long)maxW * maxH) continue; - maxW = mode.Width; - maxH = mode.Height; - } - } - var width = current?.Width ?? maxW; - var height = current?.Height ?? maxH; - var maxText = maxW > 0 ? $"{maxW} X {maxH}" : ""; - - result.Add(new MonitorInfo - { - Name = m.GdiName, - Resolution = current is not null ? $"{current.Width} X {current.Height}" : maxText.Length > 0 ? maxText : "N/A", - MaximumResolution = maxText, - MaxWidth = maxW, - MaxHeight = maxH, - Width = width, - Height = height, - Frequency = current?.Frequency > 0 ? current.Frequency.ToString() : "", - Colors = current?.BitsPerPel > 0 ? current.BitsPerPel.ToString() : "", - IsPrimary = m.IsActive && m.PositionX == 0 && m.PositionY == 0, - IsActive = m.IsActive, - IsDisconnected = !m.IsActive, - MonitorName = !string.IsNullOrWhiteSpace(m.FriendlyName) ? m.FriendlyName : edid.Name, - ShortId = hardwareId, - MonitorId = DisplayIdentity.MonitorId(hardwareId, driverKey), - SerialNumber = edid.Serial, - LeftTop = m.IsActive ? $"{m.PositionX}, {m.PositionY}" : "" - }); - } - return result; - } - - private static (string? DriverKey, EdidInfo Edid) ReadRegistry(string instanceId) - { - var empty = new EdidInfo("", "", 0, 0); - if (string.IsNullOrWhiteSpace(instanceId)) return (null, empty); - try - { - using var key = Registry.LocalMachine.OpenSubKey($@"SYSTEM\CurrentControlSet\Enum\{instanceId}"); - if (key is null) return (null, empty); - using var parameters = key.OpenSubKey("Device Parameters"); - return (key.GetValue("Driver") as string, DisplayIdentity.ParseEdid(parameters?.GetValue("EDID") as byte[])); - } - catch (Exception ex) - { - AppLog.Write($"Telas: registro de {instanceId}: {ex.Message}"); - return (null, empty); - } - } - - public void SaveLayout(string path) - { - var entries = ListMonitors().Select(m => - { - var current = m.IsActive ? NativeWindows.GetCurrentDisplayMode(m.Name) : null; - MonitorService.ParseLeftTop(m.LeftTop, out var x, out var y); - return new LayoutEntry(m.Name, m.MonitorId, m.SerialNumber, current?.BitsPerPel ?? 0, - current?.Width ?? 0, current?.Height ?? 0, current?.Frequency ?? 0, x ?? 0, y ?? 0); - }); - File.WriteAllText(path, DisplayIdentity.FormatLayout(entries)); - } - - public void LoadLayout(string path) - { - var specs = ResolveLayout(DisplayIdentity.ParseLayout(File.ReadAllLines(path))); - foreach (var (name, spec) in specs) - { - int.TryParse(spec.GetValueOrDefault("Width"), out var w); - int.TryParse(spec.GetValueOrDefault("Height"), out var h); - if (w <= 0 || h <= 0) continue; - int.TryParse(spec.GetValueOrDefault("DisplayFrequency"), out var f); - int.TryParse(spec.GetValueOrDefault("BitsPerPixel"), out var bits); - int? x = int.TryParse(spec.GetValueOrDefault("PositionX"), out var px) ? px : null; - int? y = int.TryParse(spec.GetValueOrDefault("PositionY"), out var py) ? py : null; - NativeDisplays.QueueMode(name, w, h, f, bits, x, y); - } - var code = NativeDisplays.ApplyPending(); - AppLog.Write($"Telas: layout restaurado => {code}"); - } - - public Dictionary> ResolveLayout(Dictionary> specs) => - DisplayIdentity.RemapLayoutNames(specs, ListMonitors().Select(m => (m.MonitorId, m.Name))); - - public void Enable(IReadOnlyList names) => NativeDisplays.Enable(names); - public void Disable(IReadOnlyList names) => NativeDisplays.Disable(names); - public void SetPrimary(string name) => CcdHelper.SetPrimary(name); - public void PowerOn(string name) => NativeDisplays.SetPower(name, on: true); - public void PowerOff(string name) => NativeDisplays.SetPower(name, on: false); - - public void SetMode(string name, int width, int height, int frequency, string? colors, int? x, int? y, bool primary) - { - int.TryParse(colors, out var bits); - NativeDisplays.QueueMode(name, width, height, frequency, bits, x, y); - NativeDisplays.ApplyPending(); - if (primary) CcdHelper.SetPrimary(name); - } - - public void MoveProcessWindows(string monitorName, string processName, ScreenRect rect) => - NativeDisplays.MoveProcessWindows(processName, rect); -} diff --git a/src/ConsoleMode/Services/MonitorService.cs b/src/ConsoleMode/Services/MonitorService.cs index 2ba00fe..ef9c9f9 100644 --- a/src/ConsoleMode/Services/MonitorService.cs +++ b/src/ConsoleMode/Services/MonitorService.cs @@ -7,8 +7,7 @@ namespace ConsoleMode.Services; public sealed class MonitorService { - private static bool? _useNative; - private readonly IDisplayBackend _backend = UseNative ? new NativeDisplayBackend() : new MmtDisplayBackend(); + private readonly DisplayBackend _backend = new(); private readonly object _gate = new(); private List? _cache; private readonly Dictionary> _modesCache = new(StringComparer.OrdinalIgnoreCase); @@ -19,21 +18,6 @@ public void ClearCache() lock (_modesCache) _modesCache.Clear(); } - /// - /// Windows' own display APIs instead of MultiMonitorTool (issue #91). On by default since 1.6; - /// "NativeDisplays": false goes back to MultiMonitorTool, which is no longer bundled. - /// - public static bool UseNative => _useNative ??= LoadNativeFlag(); - - /// Display control works: the native backend is on, or MultiMonitorTool is present. - public static bool IsAvailable => UseNative || AppPaths.HasMmt; - - private static bool LoadNativeFlag() - { - try { return ConfigService.Load().NativeDisplays; } - catch { return false; } - } - public IReadOnlyList GetMonitors(bool forceRefresh = false) { // Called from the UI load task and the console loop at the same time. @@ -51,11 +35,6 @@ public string ResolveName(string idOrName) => private IReadOnlyList ReadMonitors(bool forceRefresh) { if (!forceRefresh && _cache is not null) return _cache; - if (!_backend.IsAvailable) - { - _cache = []; - return _cache; - } var monitors = _backend.ListMonitors(); // Windows' "Display n": active monitors first, in the order they were listed. @@ -321,7 +300,7 @@ private List ReadDisplayModes(string monitorName, MonitorInfo return RectFromInfo(info); } - public bool UpdateFocusRect(string monitorName, ConsoleRuntimeState state, bool allowMmtFallback) + public bool UpdateFocusRect(string monitorName, ConsoleRuntimeState state, bool allowListFallback) { var screen = DisplayScreens.GetBounds(monitorName); if (screen is not null) @@ -330,7 +309,7 @@ public bool UpdateFocusRect(string monitorName, ConsoleRuntimeState state, bool return true; } - if (!allowMmtFallback) return false; + if (!allowListFallback) return false; var rect = RectFromInfo(GetMonitors().FirstOrDefault(m => m.Name == monitorName)); if (rect is null) return false; state.FocusMonitorRect = rect; diff --git a/src/ConsoleMode/Services/ProcessRunner.cs b/src/ConsoleMode/Services/ProcessRunner.cs index f8368ff..864fc20 100644 --- a/src/ConsoleMode/Services/ProcessRunner.cs +++ b/src/ConsoleMode/Services/ProcessRunner.cs @@ -77,66 +77,3 @@ public static void StartDetached(string fileName, string? arguments = null) }); } } - -public static class CsvReader -{ - public static List> Read(string path) - { - var rows = new List>(); - if (!File.Exists(path)) return rows; - - using var reader = new StreamReader(path, Encoding.UTF8); - var headerLine = reader.ReadLine(); - if (headerLine is null) return rows; - var headers = Split(headerLine); - while (reader.ReadLine() is { } line) - { - if (string.IsNullOrWhiteSpace(line)) continue; - var cols = Split(line); - var row = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (var i = 0; i < headers.Count; i++) - { - row[headers[i]] = i < cols.Count ? cols[i] : ""; - } - rows.Add(row); - } - return rows; - } - - public static string Get(this Dictionary row, string key) => - row.TryGetValue(key, out var v) ? v : ""; - - private static List Split(string line) - { - var result = new List(); - var sb = new StringBuilder(); - var inQuotes = false; - for (var i = 0; i < line.Length; i++) - { - var c = line[i]; - if (c == '"') - { - if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') - { - sb.Append('"'); - i++; - } - else - { - inQuotes = !inQuotes; - } - } - else if (c == ',' && !inQuotes) - { - result.Add(sb.ToString()); - sb.Clear(); - } - else - { - sb.Append(c); - } - } - result.Add(sb.ToString()); - return result; - } -} diff --git a/src/ConsoleMode/Services/SessionMenuMath.cs b/src/ConsoleMode/Services/SessionMenuMath.cs index c09ba4a..9e01305 100644 --- a/src/ConsoleMode/Services/SessionMenuMath.cs +++ b/src/ConsoleMode/Services/SessionMenuMath.cs @@ -8,10 +8,6 @@ public static class SessionMenuMath public static int StepVolume(int current, int direction) => Math.Clamp(current + Math.Sign(direction) * VolumeStep, 0, 100); - /// SoundVolumeView /GetPercent returns the level ×10 as its exit code; -1 for errors. - public static int? ParseVolumeExitCode(int exitCode) => - exitCode is >= 0 and <= 1000 ? (int)Math.Round(exitCode / 10.0) : null; - /// "12 min" under an hour, "1 h 05 min" after. public static string FormatElapsed(TimeSpan elapsed) { diff --git a/src/ConsoleMode/ViewModels/MainViewModel.cs b/src/ConsoleMode/ViewModels/MainViewModel.cs index 7a53b7b..26837b6 100644 --- a/src/ConsoleMode/ViewModels/MainViewModel.cs +++ b/src/ConsoleMode/ViewModels/MainViewModel.cs @@ -204,20 +204,13 @@ private void BuildAudioOptions(string selectedValue) AudioOptions.Clear(); AudioOptions.Add(new ComboOption { Text = LocalizationService.Get("AudioNoChange"), Value = "" }); AudioOptions.Add(new ComboOption { Text = LocalizationService.Get("AudioOnConnect"), Value = ConsoleEngine.AudioOnConnectId }); - if (AudioService.IsAvailable) - { - foreach (var device in _availableAudioDevices) - AudioOptions.Add(new ComboOption - { - Text = device.IsActive ? device.Name : $"{device.Name}{LocalizationService.Get("AudioDisabledSuffix")}", - Value = device.FriendlyId - }); - AudioHintText = LocalizationService.Get("AudioHint"); - } - else - { - AudioHintText = LocalizationService.Get("SvvMissing"); - } + foreach (var device in _availableAudioDevices) + AudioOptions.Add(new ComboOption + { + Text = device.IsActive ? device.Name : $"{device.Name}{LocalizationService.Get("AudioDisabledSuffix")}", + Value = device.FriendlyId + }); + AudioHintText = LocalizationService.Get("AudioHint"); SelectedAudio = AudioOptions.FirstOrDefault(x => x.Value == selectedValue) ?? AudioOptions[0]; } @@ -250,9 +243,7 @@ private static LoadResult LoadData(ConsoleEngine engine) monitors.Add(new LoadedMonitor(monitor, engine.Monitors.GetDisplayModes(monitor.Name, monitor))); if (list.Count == 0) { - error = MonitorService.IsAvailable - ? LocalizationService.Get("NoDisplayTryRefresh") - : LocalizationService.Get("MmtMissing", AppPaths.MmtPath); + error = LocalizationService.Get("NoDisplayTryRefresh"); } } catch (Exception ex) @@ -264,7 +255,7 @@ private static LoadResult LoadData(ConsoleEngine engine) List audio = []; try { - if (AudioService.IsAvailable) audio = [.. engine.Audio.GetDevices(true)]; + audio = [.. engine.Audio.GetDevices(true)]; } catch (Exception ex) { @@ -890,8 +881,6 @@ private AppConfig BuildConfig() AudioDeviceId = auto ? "" : audioId, AudioDeviceName = auto ? "" : SelectedAudio?.Text ?? "", AudioAutoSwitch = auto, - NativeAudio = _loadedConfig.NativeAudio, - NativeDisplays = _loadedConfig.NativeDisplays, FpsLimit = ReadFpsLimit(), HdrEnable = HdrEnable, VrrEnable = VrrEnable, diff --git a/tests/ConsoleMode.Tests/SessionMenuTests.cs b/tests/ConsoleMode.Tests/SessionMenuTests.cs index ec9a52b..459b5e0 100644 --- a/tests/ConsoleMode.Tests/SessionMenuTests.cs +++ b/tests/ConsoleMode.Tests/SessionMenuTests.cs @@ -13,15 +13,6 @@ public class SessionMenuTests public void Volume_steps_by_five_within_bounds(int current, int direction, int expected) => Assert.Equal(expected, SessionMenuMath.StepVolume(current, direction)); - [Theory] - [InlineData(500, 50)] - [InlineData(1000, 100)] - [InlineData(0, 0)] - [InlineData(-1, null)] - [InlineData(1001, null)] - public void GetPercent_exit_code_is_the_level_times_ten(int exitCode, int? expected) => - Assert.Equal(expected, SessionMenuMath.ParseVolumeExitCode(exitCode)); - [Theory] [InlineData(0, 12, "12 min")] [InlineData(1, 5, "1 h 05 min")]