From f71e1a1f285f9448865fde9f1a15ac7bad4227af Mon Sep 17 00:00:00 2001 From: Filipe Moreira <108258235+lippdev@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:05:04 +0200 Subject: [PATCH] feat: diagnostics screen with a copyable report Settings gets a Diagnostics card that shows what the app sees on this PC (version and channel, Windows and .NET, screens with HDR and roles, audio outputs, controllers, Steam/Playnite/RTSS/ControlFS, the end of the log) and copies it as a plain-text report. Data is gathered off the UI thread; a section that throws reads "unavailable". The report is built and redacted (user name in paths, keys and tokens) in DiagnosticsReport, which is unit tested. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.en-US.md | 1 + CHANGELOG.md | 1 + docs/TESTING.md | 13 ++ src/ConsoleMode/Resources/Strings.de-DE.json | 4 + src/ConsoleMode/Resources/Strings.en-US.json | 4 + src/ConsoleMode/Resources/Strings.es-ES.json | 4 + src/ConsoleMode/Resources/Strings.fr-FR.json | 4 + src/ConsoleMode/Resources/Strings.pt-BR.json | 4 + src/ConsoleMode/Resources/Strings.ru-RU.json | 4 + src/ConsoleMode/Services/DiagnosticsReport.cs | 126 ++++++++++++ .../Services/LocalizationService.cs | 4 + .../ViewModels/MainViewModel.Diagnostics.cs | 182 ++++++++++++++++++ src/ConsoleMode/Views/SettingsView.xaml | 17 ++ .../ConsoleMode.Tests.csproj | 1 + .../DiagnosticsReportTests.cs | 94 +++++++++ 15 files changed, 463 insertions(+) create mode 100644 src/ConsoleMode/Services/DiagnosticsReport.cs create mode 100644 src/ConsoleMode/ViewModels/MainViewModel.Diagnostics.cs create mode 100644 tests/ConsoleMode.Tests/DiagnosticsReportTests.cs diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 876a6d8..1a42036 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -5,6 +5,7 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor ## [Unreleased] ### What's new - Update notice in Portuguese: the new version's highlight shows in the app's language when there is a changelog in it (today, Portuguese); other languages still see it in English. Applies to updates published after this version. (#158) +- Diagnostics in Settings: a new card shows, read-only, the app's version and channel, Windows, the screens (resolution, HDR, which one is the game screen and which ones get hidden), the audio outputs, the connected controllers, whether Steam, Playnite, RTSS and ControlFS were found, and the end of the log. **Copy report** copies it all as text to paste into an issue, without your user name in paths and without keys. (#159) ## [1.6.2] ### What's new diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b87ab8..1166e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH ## [Unreleased] ### Novidades - Aviso de atualização em português: o destaque da nova versão aparece no idioma do app quando há um changelog nele (hoje, português); os outros idiomas continuam vendo o destaque em inglês. Vale a partir das atualizações publicadas depois desta versão. (#158) +- Diagnóstico em Ajustes: um card novo mostra, só para leitura, a versão e o canal do app, o Windows, as telas (resolução, HDR, qual é a de jogo e quais são ocultadas), as saídas de áudio, os controles conectados, se Steam, Playnite, RTSS e ControlFS foram encontrados e o fim do log. **Copiar relatório** copia tudo em texto para colar numa issue, sem o seu nome de usuário nos caminhos e sem chaves. (#159) ## [1.6.2] ### Novidades diff --git a/docs/TESTING.md b/docs/TESTING.md index 0a5dfdb..9165b7f 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -469,3 +469,16 @@ Pré-requisito: RivaTuner Statistics Server instalado, com "Show On-Screen Displ - [ ] Interface Console → Sistema: a linha "Minimizar para a bandeja" alterna Ligado/Desligado e o modo PC mostra o mesmo valor; o valor fica salvo ao reabrir o app. - [ ] Com a sessão ativa e a opção ligada, minimizar e reabrir pela bandeja continua funcionando; o atalho do controle para abrir o app também traz a janela de volta. - [ ] Fechar no X (sem sessão ativa) continua fechando o app, com a opção ligada ou desligada. + +## Diagnóstico (Ajustes → Atalho, ajuda e dados) + +- [ ] Abrir o card "Diagnóstico": o anel de carregamento aparece e, sem travar a janela, o relatório mostra versão e canal (instalado/portátil, estável/teste), Windows com build, e a versão do .NET. +- [ ] Telas: aparecem todas as que o app vê, com resolução @ Hz, HDR ligado/desligado/sem suporte; a tela de jogo vem marcada "game screen" e as que serão ocultadas, "hidden in console mode". Trocar a tela de jogo ou as ocultas e clicar em **Atualizar** muda as marcas. +- [ ] Ligar o HDR no Windows e clicar em **Atualizar**: a tela passa a "HDR on". +- [ ] Áudio: lista as saídas, com a padrão do Windows e a escolhida no app marcadas; com "Ao conectar", nenhuma vem como escolhida. +- [ ] Controles: com um Xbox e um DualSense ligados, os dois aparecem (Xbox / PlayStation); sem nenhum, "(none)". +- [ ] Programas: Steam, Playnite, RTSS e ControlFS com caminho e versão quando instalados, e "not found" quando não; "Launches" mostra o iniciador escolhido. +- [ ] Log: as últimas ~50 linhas do `consolemode.log` aparecem no fim. +- [ ] Nenhum caminho mostra o nome do usuário do Windows (`C:\Users\…\`), nem com Playnite instalado em `%LOCALAPPDATA%`. +- [ ] **Copiar relatório** mostra o aviso de copiado e o texto colado no Bloco de Notas é igual ao da tela. +- [ ] Com uma seção falhando (ex.: sem permissão de leitura do log), ela mostra "unavailable" e o resto do relatório continua aparecendo. diff --git a/src/ConsoleMode/Resources/Strings.de-DE.json b/src/ConsoleMode/Resources/Strings.de-DE.json index 269c47c..0adf161 100644 --- a/src/ConsoleMode/Resources/Strings.de-DE.json +++ b/src/ConsoleMode/Resources/Strings.de-DE.json @@ -194,6 +194,10 @@ "ControllerDevicesHeading": "Geräte", "ControllerSampleHeading": "Live-Anzeige (Tasten drücken)", "DiagnosticsCopied": "Diagnosedaten kopiert. Füge sie in den Fehlerbericht oder das Feedbackformular ein.", + "DiagnosticsCard": "Diagnose", + "DiagnosticsDescription": "Versionen, Bildschirme, Audio, Controller, gefundene Programme und das Ende des Protokolls, zum Anhängen an einen Fehlerbericht. Pfade mit deinem Benutzernamen und Schlüssel werden ausgeblendet.", + "RefreshDiagnostics": "Aktualisieren", + "CopyReport": "Bericht kopieren", "TutorialCard": "Tutorial", "TutorialDescription": "Die Einführung auf der Startseite erneut anzeigen.", "SeeAgain": "Erneut anzeigen", diff --git a/src/ConsoleMode/Resources/Strings.en-US.json b/src/ConsoleMode/Resources/Strings.en-US.json index 2370b9e..158941e 100644 --- a/src/ConsoleMode/Resources/Strings.en-US.json +++ b/src/ConsoleMode/Resources/Strings.en-US.json @@ -194,6 +194,10 @@ "ControllerDevicesHeading": "Devices", "ControllerSampleHeading": "Live reading (press buttons)", "DiagnosticsCopied": "Diagnostics copied. Paste them into the issue or the feedback form.", + "DiagnosticsCard": "Diagnostics", + "DiagnosticsDescription": "Versions, screens, audio, controllers, programs found and the end of the log, to attach to a bug report. Paths with your user name and keys are hidden.", + "RefreshDiagnostics": "Refresh", + "CopyReport": "Copy report", "TutorialCard": "Tutorial", "TutorialDescription": "Show the home screen walkthrough again.", "SeeAgain": "Show again", diff --git a/src/ConsoleMode/Resources/Strings.es-ES.json b/src/ConsoleMode/Resources/Strings.es-ES.json index b2fbcb1..7e724c9 100644 --- a/src/ConsoleMode/Resources/Strings.es-ES.json +++ b/src/ConsoleMode/Resources/Strings.es-ES.json @@ -194,6 +194,10 @@ "ControllerDevicesHeading": "Dispositivos", "ControllerSampleHeading": "Lectura en vivo (pulsa botones)", "DiagnosticsCopied": "Diagnóstico copiado. Pégalo en la issue o en el formulario de comentarios.", + "DiagnosticsCard": "Diagnóstico", + "DiagnosticsDescription": "Versiones, pantallas, audio, mandos, programas encontrados y el final del registro, para adjuntar a un informe de error. Las rutas con tu nombre de usuario y las claves se ocultan.", + "RefreshDiagnostics": "Actualizar", + "CopyReport": "Copiar informe", "TutorialCard": "Tutorial", "TutorialDescription": "Vuelve a mostrar la guía de la pantalla de inicio.", "SeeAgain": "Ver de nuevo", diff --git a/src/ConsoleMode/Resources/Strings.fr-FR.json b/src/ConsoleMode/Resources/Strings.fr-FR.json index 58d933a..dcf72f1 100644 --- a/src/ConsoleMode/Resources/Strings.fr-FR.json +++ b/src/ConsoleMode/Resources/Strings.fr-FR.json @@ -194,6 +194,10 @@ "ControllerDevicesHeading": "Appareils", "ControllerSampleHeading": "Lecture en direct (appuyez sur des boutons)", "DiagnosticsCopied": "Diagnostic copié. Collez-le dans le ticket ou le formulaire de commentaires.", + "DiagnosticsCard": "Diagnostic", + "DiagnosticsDescription": "Versions, écrans, audio, manettes, programmes trouvés et la fin du journal, à joindre à un rapport de bug. Les chemins contenant votre nom d'utilisateur et les clés sont masqués.", + "RefreshDiagnostics": "Actualiser", + "CopyReport": "Copier le rapport", "TutorialCard": "Tutoriel", "TutorialDescription": "Revoir la présentation de l'écran d'accueil.", "SeeAgain": "Revoir", diff --git a/src/ConsoleMode/Resources/Strings.pt-BR.json b/src/ConsoleMode/Resources/Strings.pt-BR.json index b970282..0f5aaeb 100644 --- a/src/ConsoleMode/Resources/Strings.pt-BR.json +++ b/src/ConsoleMode/Resources/Strings.pt-BR.json @@ -194,6 +194,10 @@ "ControllerDevicesHeading": "Dispositivos", "ControllerSampleHeading": "Leitura ao vivo (aperte botões)", "DiagnosticsCopied": "Diagnóstico copiado. Cole na issue ou no feedback.", + "DiagnosticsCard": "Diagnóstico", + "DiagnosticsDescription": "Versões, telas, áudio, controles, programas encontrados e o fim do log, para anexar a um relato de problema. Caminhos com o seu nome de usuário e chaves ficam ocultos.", + "RefreshDiagnostics": "Atualizar", + "CopyReport": "Copiar relatório", "TutorialCard": "Tutorial", "TutorialDescription": "Mostra de novo o passo a passo da tela inicial.", "SeeAgain": "Ver de novo", diff --git a/src/ConsoleMode/Resources/Strings.ru-RU.json b/src/ConsoleMode/Resources/Strings.ru-RU.json index 7ae3f75..528de42 100644 --- a/src/ConsoleMode/Resources/Strings.ru-RU.json +++ b/src/ConsoleMode/Resources/Strings.ru-RU.json @@ -194,6 +194,10 @@ "ControllerDevicesHeading": "Устройства", "ControllerSampleHeading": "Данные в реальном времени (нажимайте кнопки)", "DiagnosticsCopied": "Диагностика скопирована. Вставьте её в issue или форму отзыва.", + "DiagnosticsCard": "Диагностика", + "DiagnosticsDescription": "Версии, экраны, звук, контроллеры, найденные программы и конец журнала — чтобы приложить к сообщению об ошибке. Пути с вашим именем пользователя и ключи скрываются.", + "RefreshDiagnostics": "Обновить", + "CopyReport": "Копировать отчёт", "TutorialCard": "Обучение", "TutorialDescription": "Снова показать обзор главного экрана.", "SeeAgain": "Показать снова", diff --git a/src/ConsoleMode/Services/DiagnosticsReport.cs b/src/ConsoleMode/Services/DiagnosticsReport.cs new file mode 100644 index 0000000..5d507e0 --- /dev/null +++ b/src/ConsoleMode/Services/DiagnosticsReport.cs @@ -0,0 +1,126 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ConsoleMode.Services; + +public sealed record DiagnosticsSystem(string AppVersion, string Channel, string Windows, string Runtime); + +/// "3840 × 2160 @ 120 Hz", or empty when unknown. +/// "on" | "off" | "unsupported" | "" (unknown). +public sealed record DiagnosticsDisplay(string Name, string Mode, string Hdr, bool IsActive, bool IsPrimary, bool IsFocus, bool IsHidden); + +public sealed record DiagnosticsAudio(string Name, bool IsDefault, bool IsConfigured); + +/// +/// One connected controller. is free text printed after the type +/// (e.g. a battery level); the collector leaves it empty for now. +/// +public sealed record DiagnosticsController(string Name, string Type, string? Detail = null); + +/// Null when the program wasn't found. +public sealed record DiagnosticsApp(string Name, string? Path, string? Version = null); + +/// Everything the report shows. A null section is one that failed to load ("unavailable"). +public sealed class DiagnosticsData +{ + public DiagnosticsSystem? System { get; init; } + public IReadOnlyList? Displays { get; init; } + public IReadOnlyList? Audio { get; init; } + public IReadOnlyList? Controllers { get; init; } + public IReadOnlyList? Apps { get; init; } + public string? FullscreenMode { get; init; } + public IReadOnlyList? Log { get; init; } +} + +/// +/// Settings → Diagnostics: the plain-text report users paste into an issue. English on purpose +/// (it's read by whoever answers the issue), and redacted: no user name in paths, no secrets. +/// +public static class DiagnosticsReport +{ + public const string Unavailable = "unavailable"; + public const int LogLines = 50; + + public static string Format(DiagnosticsData data) + { + var sb = new StringBuilder(); + + Section(sb, "Console Mode", data.System is null ? null : [ + $"Version: {data.System.AppVersion} ({data.System.Channel})", + $"Windows: {data.System.Windows}", + $".NET: {data.System.Runtime}" + ]); + + Section(sb, "Displays", data.Displays?.Select(d => + { + var tags = new List(); + if (d.IsFocus) tags.Add("game screen"); + if (d.IsHidden) tags.Add("hidden in console mode"); + if (d.IsPrimary) tags.Add("primary"); + if (!d.IsActive) tags.Add("off"); + var mode = d.Mode.Length > 0 ? d.Mode : "mode unknown"; + var hdr = d.Hdr.Length > 0 ? $", HDR {d.Hdr}" : ""; + var tagText = tags.Count > 0 ? $" [{string.Join(", ", tags)}]" : ""; + return $"- {d.Name}: {mode}{hdr}{tagText}"; + })); + + Section(sb, "Audio outputs", data.Audio?.Select(a => + { + var tags = new List(); + if (a.IsDefault) tags.Add("Windows default"); + if (a.IsConfigured) tags.Add("chosen in Console Mode"); + return $"- {a.Name}{(tags.Count > 0 ? $" [{string.Join(", ", tags)}]" : "")}"; + })); + + Section(sb, "Controllers", data.Controllers?.Select(c => + $"- {c.Name}: {c.Type}{(string.IsNullOrWhiteSpace(c.Detail) ? "" : $", {c.Detail}")}")); + + IEnumerable? apps = data.Apps?.Select(a => + a.Path is null ? $"- {a.Name}: not found" + : $"- {a.Name}: {a.Path}{(string.IsNullOrWhiteSpace(a.Version) ? "" : $" ({a.Version})")}"); + if (apps is not null && !string.IsNullOrEmpty(data.FullscreenMode)) + apps = apps.Prepend($"Launches: {data.FullscreenMode}"); + Section(sb, "Programs", apps); + + Section(sb, $"Log (last {LogLines} lines)", data.Log?.TakeLast(LogLines)); + + return Redact(sb.ToString().TrimEnd() + Environment.NewLine); + } + + private static void Section(StringBuilder sb, string title, IEnumerable? lines) + { + if (sb.Length > 0) sb.AppendLine(); + sb.AppendLine($"== {title} =="); + if (lines is null) + { + sb.AppendLine(Unavailable); + return; + } + var any = false; + foreach (var line in lines) + { + sb.AppendLine(line); + any = true; + } + if (!any) sb.AppendLine("(none)"); + } + + private static readonly Regex UserPath = new(@"(?i)\b([a-z]:[\\/]+(?:users|documents and settings)[\\/]+)[^\\/\r\n""']+", RegexOptions.Compiled); + private static readonly Regex Pem = new(@"-----BEGIN [A-Z ]+-----.*?(-----END [A-Z ]+-----|$)", RegexOptions.Compiled | RegexOptions.Singleline); + private static readonly Regex Dpapi = new(SecretPrefix + @"[A-Za-z0-9+/=]+", RegexOptions.Compiled); + private static readonly Regex NamedSecret = new( + @"(?i)\b(token|password|passwd|secret|api[_-]?key|private[_-]?key|pairing[_-]?key|auth[_-]?key|authorization)(""?\s*[:=]\s*""?)(?:bearer\s+)?[^\s"",;]+", + RegexOptions.Compiled); + + // SecretProtector.Prefix, repeated so this file stays free of DPAPI (and testable anywhere). + private const string SecretPrefix = "dpapi:"; + + /// Hides the Windows user name in profile paths and anything that looks like a key or token. + public static string Redact(string text) + { + text = Pem.Replace(text, "[redacted key]"); + text = Dpapi.Replace(text, SecretPrefix + "[redacted]"); + text = NamedSecret.Replace(text, "$1$2[redacted]"); + return UserPath.Replace(text, "$1…"); + } +} diff --git a/src/ConsoleMode/Services/LocalizationService.cs b/src/ConsoleMode/Services/LocalizationService.cs index d6d95cd..7075a19 100644 --- a/src/ConsoleMode/Services/LocalizationService.cs +++ b/src/ConsoleMode/Services/LocalizationService.cs @@ -205,6 +205,10 @@ internal void NotifyAll() public string CopyDiagnostics => LocalizationService.Get(nameof(CopyDiagnostics)); public string ControllerDevicesHeading => LocalizationService.Get(nameof(ControllerDevicesHeading)); public string ControllerSampleHeading => LocalizationService.Get(nameof(ControllerSampleHeading)); + public string DiagnosticsCard => LocalizationService.Get(nameof(DiagnosticsCard)); + public string DiagnosticsDescription => LocalizationService.Get(nameof(DiagnosticsDescription)); + public string RefreshDiagnostics => LocalizationService.Get(nameof(RefreshDiagnostics)); + public string CopyReport => LocalizationService.Get(nameof(CopyReport)); public string SessionMenuDescription => LocalizationService.Get(nameof(SessionMenuDescription)); public string ConsoleScreensHeading => LocalizationService.Get(nameof(ConsoleScreensHeading)); public string ConsoleQuickHeading => LocalizationService.Get(nameof(ConsoleQuickHeading)); diff --git a/src/ConsoleMode/ViewModels/MainViewModel.Diagnostics.cs b/src/ConsoleMode/ViewModels/MainViewModel.Diagnostics.cs new file mode 100644 index 0000000..1d9dfe8 --- /dev/null +++ b/src/ConsoleMode/ViewModels/MainViewModel.Diagnostics.cs @@ -0,0 +1,182 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using ConsoleMode.Models; +using ConsoleMode.Native; +using ConsoleMode.Services; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Win32; +using Windows.ApplicationModel.DataTransfer; +using Windows.Gaming.Input; + +namespace ConsoleMode.ViewModels; + +// Settings → Diagnostics: what the app sees on this PC, as a report to paste into an issue. +// Gathered off the UI thread; each section that throws reads "unavailable" (DiagnosticsReport). +public partial class MainViewModel +{ + private const ushort MicrosoftVendorId = 0x045E; + + [ObservableProperty] private bool _isDiagnosticsOpen; + [ObservableProperty] private bool _isLoadingDiagnostics; + [ObservableProperty] private string _diagnosticsText = ""; + + partial void OnIsDiagnosticsOpenChanged(bool value) + { + if (value && DiagnosticsText.Length == 0) _ = RefreshDiagnosticsAsync(); + } + + [RelayCommand] + private async Task RefreshDiagnosticsAsync() + { + if (IsLoadingDiagnostics) return; + IsLoadingDiagnostics = true; + try + { + var config = BuildConfig(); + var beta = BetaUpdates; + DiagnosticsText = await Task.Run(() => DiagnosticsReport.Format(CollectDiagnostics(config, beta))); + } + catch (Exception ex) + { + AppLog.Write($"Diagnóstico: {ex}"); + DiagnosticsText = DiagnosticsReport.Unavailable; + } + finally + { + IsLoadingDiagnostics = false; + } + } + + [RelayCommand] + private async Task CopyDiagnosticsReportAsync() + { + if (DiagnosticsText.Length == 0) await RefreshDiagnosticsAsync(); + try + { + var package = new DataPackage(); + package.SetText(DiagnosticsText); + Clipboard.SetContent(package); + SetStatus(LocalizationService.Get("DiagnosticsCopied"), InfoBarSeverity.Success); + } + catch (Exception ex) + { + AppLog.Write($"Diagnóstico: copiar: {ex.Message}"); + } + } + + private DiagnosticsData CollectDiagnostics(AppConfig config, bool betaUpdates) => new() + { + System = Try("sistema", () => new DiagnosticsSystem( + UpdateService.CurrentVersion, + DescribeChannel(betaUpdates), + DescribeWindows(), + $"{RuntimeInformation.FrameworkDescription} {RuntimeInformation.ProcessArchitecture}")), + Displays = Try("telas", () => Engine.Monitors.GetMonitors(forceRefresh: true).Select(m => new DiagnosticsDisplay( + $"{m.FriendlyName} ({m.Name.Replace(@"\\.\", "")})", + m.ResolutionText, + m.IsActive ? DescribeHdr(m.Name) : "", + m.IsActive, + m.IsPrimary, + m.Matches(config.FocusMonitor), + config.HideMonitors.Any(m.Matches))).ToList()), + // A fresh AudioService: the engine's cache belongs to the UI and the console loop. + Audio = Try("áudio", () => new AudioService().GetDevices().Select(d => new DiagnosticsAudio( + d.Name, + d.IsDefault, + string.Equals(d.FriendlyId, config.AudioDeviceId, StringComparison.OrdinalIgnoreCase))).ToList()), + Controllers = Try("controles", () => CollectControllers()), + Apps = Try("programas", () => new List + { + FoundApp("Steam", SteamSession.FindSteamExe()), + FoundApp("Playnite", Engine.Launch.GetPlaynitePath()), + FoundApp("RTSS", Engine.Rtss.GetInstallPath()), + FoundApp("ControlFS", ControlFsService.FindExe()) + }), + FullscreenMode = config.FullscreenMode, + Log = Try("log", () => ReadLogLines()) + }; + + /// + /// One entry per controller Windows lists. Detail is left empty here: it's where per-pad + /// extras (e.g. battery) go. + /// + private static List CollectControllers() + { + var list = new List(); + foreach (var raw in RawGameController.RawGameControllers) + { + var type = raw.HardwareVendorId switch + { + ControllerMapping.SonyVendorId => "PlayStation", + ControllerMapping.NintendoVendorId => "Nintendo", + MicrosoftVendorId => "Xbox", + _ => "Other" + }; + if (Gamepad.FromGameController(raw) is not null) type += " (gamepad)"; + list.Add(new DiagnosticsController($"{raw.DisplayName} ({raw.HardwareVendorId:X4}:{raw.HardwareProductId:X4})", type)); + } + return list; + } + + private static T? Try(string section, Func read) where T : class + { + try { return read(); } + catch (Exception ex) + { + AppLog.Write($"Diagnóstico: {section}: {ex.Message}"); + return null; + } + } + + private static DiagnosticsApp FoundApp(string name, string? path) + { + if (path is null) return new DiagnosticsApp(name, null); + string? version = null; + try { version = FileVersionInfo.GetVersionInfo(path).ProductVersion?.Trim(); } + catch { /* the path is enough */ } + return new DiagnosticsApp(name, path, version); + } + + private static string DescribeChannel(bool betaUpdates) + { + var kind = AppPaths.IsInstalled ? "installed" : "portable"; + var channel = UpdateService.SemVer.Parse(UpdateService.CurrentVersion).IsPrerelease ? "test build" + : betaUpdates ? "stable, receives test versions" + : "stable"; + return $"{kind}, {channel}"; + } + + private static string DescribeHdr(string monitorName) => CcdHelper.GetHdrStatus(monitorName) switch + { + 2 => "on", + 1 => "off", + 0 => "unsupported", + _ => "" + }; + + private static string DescribeWindows() + { + var build = Environment.OSVersion.Version.Build; + var name = build >= 22000 ? "Windows 11" : "Windows 10"; + using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion"); + var edition = key?.GetValue("EditionID") as string; + var release = key?.GetValue("DisplayVersion") as string; + var ubr = key?.GetValue("UBR") is int u ? $".{u}" : ""; + var parts = new[] { name, edition, release }.Where(p => !string.IsNullOrWhiteSpace(p)); + return $"{string.Join(" ", parts)} (build {build}{ubr}, {RuntimeInformation.OSArchitecture})"; + } + + private static List ReadLogLines() + { + var path = Path.Combine(AppPaths.DataDir, "consolemode.log"); + if (!File.Exists(path)) return []; + // AppLog may be writing at the same time. + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + using var reader = new StreamReader(stream); + var lines = new List(); + while (reader.ReadLine() is { } line) lines.Add(line); + return lines.Count > DiagnosticsReport.LogLines ? lines.GetRange(lines.Count - DiagnosticsReport.LogLines, DiagnosticsReport.LogLines) : lines; + } +} diff --git a/src/ConsoleMode/Views/SettingsView.xaml b/src/ConsoleMode/Views/SettingsView.xaml index c253e69..fa3354e 100644 --- a/src/ConsoleMode/Views/SettingsView.xaml +++ b/src/ConsoleMode/Views/SettingsView.xaml @@ -330,6 +330,23 @@ + + + + + + +