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")]