mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-05 02:36:19 +02:00
CmdPal: GEH per partes; part 1: error report builder, sanitizer and internals tools setting page (#44140)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request This PR adds three parts of the original big bad global error handler (error report builder, sanitization and internal tools UI). ### Error Report Generation - `ErrorReportBuilder`: Produces a detailed, technical report with system context. - Comprehensive data: OS version, architecture, culture, app version, elevation status, etc. - Exception analysis: Coalesces nested exception messages and HRESULT details for clearer diagnostics. <details><summary>Example</summary> <pre> This is an error report generated by Windows Command Palette. If you are seeing this, it means something went a little sideways in the app. You can help us fix it by filing a report at https://aka.ms/powerToysReportBug. (While you’re at it, give the details below a quick skim — just to make sure there’s nothing personal you’d prefer not to share. It’s rare, but sometimes little surprises sneak in.) ============================================================ Summary: Message: Test exception; thrown from the UI thread Type: System.NotImplementedException Source: Microsoft.CmdPal.UI Time: 2025-08-25 18:54:44.3854569 HRESULT: 0x80004001 (-2147467263) Context: MainThreadException Application: App version: 0.0.1.0 Is elevated: no Environment: OS version: Microsoft Windows 10.0.26120 OS architecture: X64 Runtime identifier: win-x64 Framework: .NET 9.0.8 Process architecture: X64 Culture: cs-CZ UI culture: en-US Stack Trace: at Microsoft.CmdPal.UI.Settings.InternalPage.ThrowPlainMainThreadException_Click(Object sender, RoutedEventArgs e) at WinRT._EventSource_global__Microsoft_UI_Xaml_RoutedEventHandler.EventState.<GetEventInvoke>b__1_0(Object sender, RoutedEventArgs e) at ABI.Microsoft.UI.Xaml.RoutedEventHandler.Do_Abi_Invoke(IntPtr thisPtr, IntPtr sender, IntPtr e) ------------------ Full Exception Details ------------------ System.NotImplementedException: Test exception; thrown from the UI thread at Microsoft.CmdPal.UI.Settings.InternalPage.ThrowPlainMainThreadException_Click(Object sender, RoutedEventArgs e) at WinRT._EventSource_global__Microsoft_UI_Xaml_RoutedEventHandler.EventState.<GetEventInvoke>b__1_0(Object sender, RoutedEventArgs e) at ABI.Microsoft.UI.Xaml.RoutedEventHandler.Do_Abi_Invoke(IntPtr thisPtr, IntPtr sender, IntPtr e) ============================================================ </pre> </details> Real-world example: #41362 ### PII Sanitization Framework - `ErrorReportSanitizer`: Multi-layer sanitization pipeline for sensitive data. - Nine specialized rule providers: - `PiiRuleProvider`: Personally identifiable information (emails, phone numbers, SSNs). - `ProfilePathAndUsernameRuleProvider`: Windows user profiles and usernames. - `NetworkRuleProvider`: IP addresses, MAC addresses, network identifiers. - `SecretKeyValueRulesProvider`: API keys, tokens, passwords in key/value formats. - `FilenameMaskRuleProvider`: Sensitive file paths and extensions. - `UrlRuleProvider`: URLs and web addresses. - `TokenRuleProvider`: JWT and other auth tokens. - `ConnectionStringRuleProvider`: Database connection strings. - `EnvironmentPropertiesRuleProvider`: Environment variables and system properties. ### Internals Tools Page A page in settings available in non-CI-builds: <img width="1305" height="745" alt="image" src="https://github.com/user-attachments/assets/3145ecfd-997f-491d-8c8a-6096634b6045" /> <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] Closes: #xxx <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
This commit is contained in:
@@ -41,7 +41,7 @@ namespace Microsoft.CmdPal.UI;
|
||||
/// <summary>
|
||||
/// Provides application-specific behavior to supplement the default Application class.
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private readonly GlobalErrorHandler _globalErrorHandler = new();
|
||||
|
||||
@@ -67,7 +67,7 @@ public partial class App : Application
|
||||
public App()
|
||||
{
|
||||
#if !CMDPAL_DISABLE_GLOBAL_ERROR_HANDLER
|
||||
_globalErrorHandler.Register(this);
|
||||
_globalErrorHandler.Register(this, GlobalErrorHandler.Options.Default);
|
||||
#endif
|
||||
|
||||
Services = ConfigureServices();
|
||||
@@ -203,4 +203,11 @@ public partial class App : Application
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_globalErrorHandler.Dispose();
|
||||
EtwTrace.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ public sealed partial class CommandBar : UserControl,
|
||||
|
||||
private void SettingsIcon_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
|
||||
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage());
|
||||
}
|
||||
|
||||
private void MoreCommandsButton_Clicked(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -203,6 +203,12 @@
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- More section -->
|
||||
<TextBlock Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}" Text="More" />
|
||||
<Border>
|
||||
<Button Command="{x:Bind ViewModel.OpenInternalToolsCommand}" Content="Open internal tools" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.Common.Services.Reports;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
@@ -15,14 +15,22 @@ namespace Microsoft.CmdPal.UI.Helpers;
|
||||
/// <summary>
|
||||
/// Global error handler for Command Palette.
|
||||
/// </summary>
|
||||
internal sealed partial class GlobalErrorHandler
|
||||
internal sealed partial class GlobalErrorHandler : IDisposable
|
||||
{
|
||||
private readonly ErrorReportBuilder _errorReportBuilder = new();
|
||||
private Options? _options;
|
||||
private App? _app;
|
||||
|
||||
// GlobalErrorHandler is designed to be self-contained; it can be registered and invoked before a service provider is available.
|
||||
internal void Register(App app)
|
||||
internal void Register(App app, Options options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(app);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
app.UnhandledException += App_UnhandledException;
|
||||
_options = options;
|
||||
|
||||
_app = app;
|
||||
_app.UnhandledException += App_UnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
}
|
||||
@@ -54,21 +62,15 @@ internal sealed partial class GlobalErrorHandler
|
||||
HandleException(e.Exception, Context.UnobservedTaskException);
|
||||
}
|
||||
|
||||
private static void HandleException(Exception ex, Context context)
|
||||
private void HandleException(Exception ex, Context context)
|
||||
{
|
||||
Logger.LogError($"Unhandled exception detected ({context})", ex);
|
||||
|
||||
if (context == Context.MainThreadException)
|
||||
{
|
||||
var error = DiagnosticsHelper.BuildExceptionMessage(ex, null);
|
||||
var report = $"""
|
||||
This is an error report generated by Windows Command Palette.
|
||||
If you are seeing this message, it means the application has encountered an unexpected issue.
|
||||
You can help us fix it by filing a report at https://aka.ms/powerToysReportBug.
|
||||
{error}
|
||||
""";
|
||||
var report = _errorReportBuilder.BuildReport(ex, context.ToString(), _options?.RedactPii ?? true);
|
||||
|
||||
StoreReport(report, storeOnDesktop: false);
|
||||
StoreReport(report, storeOnDesktop: _options?.StoreReportOnUserDesktop == true);
|
||||
|
||||
string message;
|
||||
string caption;
|
||||
@@ -138,6 +140,13 @@ internal sealed partial class GlobalErrorHandler
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_app?.UnhandledException -= App_UnhandledException;
|
||||
TaskScheduler.UnobservedTaskException -= TaskScheduler_UnobservedTaskException;
|
||||
AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException;
|
||||
}
|
||||
|
||||
private enum Context
|
||||
{
|
||||
Unknown = 0,
|
||||
@@ -146,4 +155,26 @@ internal sealed partial class GlobalErrorHandler
|
||||
UnobservedTaskException,
|
||||
AppDomainUnhandledException,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options controlling how <see cref="GlobalErrorHandler"/> reacts to exceptions
|
||||
/// (what to log, what to show to the user, and where to store reports).
|
||||
/// </summary>
|
||||
internal sealed record Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default configuration.
|
||||
/// </summary>
|
||||
public static Options Default { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Personally Identifiable Information (PII) should be redacted in error reports.
|
||||
/// </summary>
|
||||
public bool RedactPii { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to store the error report on the user's desktop in addition to the log directory.
|
||||
/// </summary>
|
||||
public bool StoreReportOnUserDesktop { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ internal sealed partial class TrayIconService
|
||||
{
|
||||
if (wParam == PInvoke.WM_USER + 1)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
|
||||
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage());
|
||||
}
|
||||
else if (wParam == PInvoke.WM_USER + 2)
|
||||
{
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
<None Remove="Pages\Settings\GeneralPage.xaml" />
|
||||
<None Remove="SettingsWindow.xaml" />
|
||||
<None Remove="Settings\AppearancePage.xaml" />
|
||||
<None Remove="Settings\InternalPage.xaml" />
|
||||
<None Remove="ShellPage.xaml" />
|
||||
<None Remove="Styles\Colors.xaml" />
|
||||
<None Remove="Styles\Settings.xaml" />
|
||||
@@ -264,6 +265,11 @@
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Settings\InternalPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\Colors.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -257,11 +257,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
OpenSettings();
|
||||
OpenSettings(message.SettingsPageTag);
|
||||
});
|
||||
}
|
||||
|
||||
public void OpenSettings()
|
||||
public void OpenSettings(string pageTag)
|
||||
{
|
||||
if (_settingsWindow is null)
|
||||
{
|
||||
@@ -270,6 +270,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
_settingsWindow.Activate();
|
||||
_settingsWindow.BringToFront();
|
||||
_settingsWindow.Navigate(pageTag);
|
||||
}
|
||||
|
||||
public void Receive(ShowDetailsMessage message)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Settings;
|
||||
|
||||
public partial class InternalPage
|
||||
{
|
||||
internal static class SampleData
|
||||
{
|
||||
internal static string ExceptionMessageWithPii { get; } =
|
||||
$"""
|
||||
Test exception with personal information; thrown from the UI thread
|
||||
|
||||
Here is e-mail address <jane.doe@contoso.com>
|
||||
IPv4 address: 192.168.100.1
|
||||
IPv4 loopback address: 127.0.0.1
|
||||
MAC address: 00-14-22-01-23-45
|
||||
IPv6 address: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
|
||||
IPv6 loopback address: ::1
|
||||
Password: P@ssw0rd123!
|
||||
Password=secret
|
||||
Api key: 1234567890abcdef
|
||||
PostgreSQL connection string: Host=localhost;Username=postgres;Password=secret;Database=mydb
|
||||
InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com;
|
||||
X-API-key: 1234567890abcdef
|
||||
Pet-Shop-Subscription-Key: 1234567890abcdef
|
||||
Here is a user name {Environment.UserName}
|
||||
And here is a profile path {Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\Pictures
|
||||
Here is a local app data path {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal
|
||||
Here is machine name {Environment.MachineName}
|
||||
JWT token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
|
||||
User email john.doe@company.com failed validation
|
||||
File not found: {Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)}\\secret.txt
|
||||
Connection string: Server=localhost;User ID=admin;Password=secret123;Database=test
|
||||
Phone number 555-123-4567 is invalid
|
||||
API key abc123def456ghi789jkl012mno345pqr678 expired
|
||||
Failed to connect to https://api.internal-company.com/users/12345?token=secret_abc123
|
||||
Error accessing file://C:/Users/john.doe/Documents/confidential.pdf
|
||||
JDBC connection failed: jdbc://database-server:5432/userdb?user=admin&password=secret
|
||||
FTP upload error: ftp://internal-server.company.com/uploads/user_data.csv
|
||||
Email service error: mailto:admin@internal-company.com?subject=Alert
|
||||
""";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Microsoft.CmdPal.UI.Settings.InternalPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<Grid Padding="16">
|
||||
<StackPanel
|
||||
MaxWidth="1000"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="{StaticResource SettingsCardSpacing}">
|
||||
|
||||
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="Tools on this page are for internal use only. This page is not visible in CI builds." />
|
||||
|
||||
<!-- Exception Handling Section -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Exception Handling" />
|
||||
<controls:SettingsExpander
|
||||
Description="Actions for testing global exception handling from the application"
|
||||
Header="Throw exceptions"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<controls:SettingsExpander.Items>
|
||||
<controls:SettingsCard Header="Throw an unhandled exception from the UI thread">
|
||||
<Button Click="ThrowPlainMainThreadException_Click" Content="Throw" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard Header="Throw an unhandled exception from the UI thread (with PII)">
|
||||
<Button Click="ThrowPlainMainThreadExceptionPii_Click" Content="Throw" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard Description="Throw with delay, when the task is collected by the GC" Header="Throw unobserved exception from a task">
|
||||
<Button Click="ThrowExceptionInUnobservedTask_Click" Content="Throw" />
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<!-- Diagnostics Section -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Diagnostics" />
|
||||
<controls:SettingsCard
|
||||
x:Name="LogsSettingsCard"
|
||||
Header="Logs folder"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<Button Click="OpenLogsCardClicked" Content="Open folder" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard
|
||||
x:Name="CurrentLogFileSettingsCard"
|
||||
Header="Current log file"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<Button Click="OpenCurrentLogCardClicked" Content="Open log" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Data Section -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Data and Files" />
|
||||
<controls:SettingsCard
|
||||
x:Name="ConfigurationFolderSettingsCard"
|
||||
Header="Configuration folder"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<Button Click="OpenConfigFolderCardClick" Content="Open folder" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.System;
|
||||
using Page = Microsoft.UI.Xaml.Controls.Page;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class InternalPage : Page
|
||||
{
|
||||
public InternalPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void ThrowPlainMainThreadException_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Logger.LogDebug("Throwing test exception from the UI thread");
|
||||
throw new NotImplementedException("Test exception; thrown from the UI thread");
|
||||
}
|
||||
|
||||
private void ThrowExceptionInUnobservedTask_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Logger.LogDebug("Starting a task that will throw test exception");
|
||||
Task.Run(() =>
|
||||
{
|
||||
Logger.LogDebug("Throwing test exception from a task");
|
||||
throw new InvalidOperationException("Test exception; thrown from a task");
|
||||
});
|
||||
}
|
||||
|
||||
private void ThrowPlainMainThreadExceptionPii_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Logger.LogDebug("Throwing test exception from the UI thread (PII)");
|
||||
throw new InvalidOperationException(SampleData.ExceptionMessageWithPii);
|
||||
}
|
||||
|
||||
private async void OpenLogsCardClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var logFolderPath = Logger.CurrentVersionLogDirectoryPath;
|
||||
if (Directory.Exists(logFolderPath))
|
||||
{
|
||||
await Launcher.LaunchFolderPathAsync(logFolderPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to open directory in Explorer", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OpenCurrentLogCardClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var logPath = Logger.CurrentLogFile;
|
||||
if (File.Exists(logPath))
|
||||
{
|
||||
await Launcher.LaunchUriAsync(new Uri(logPath));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to open log file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OpenConfigFolderCardClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
if (Directory.Exists(directory))
|
||||
{
|
||||
await Launcher.LaunchFolderPathAsync(directory);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to open directory in Explorer", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@
|
||||
x:Uid="Settings_GeneralPage_NavigationViewItem_Extensions"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="Extensions" />
|
||||
<!-- "Internal Tools" page item is added dynamically from code -->
|
||||
</NavigationView.MenuItems>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
|
||||
@@ -30,6 +30,8 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
{
|
||||
private readonly LocalKeyboardListener _localKeyboardListener;
|
||||
|
||||
private readonly NavigationViewItem? _internalNavItem;
|
||||
|
||||
public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
|
||||
|
||||
// Gets or sets optional action invoked after NavigationView is loaded.
|
||||
@@ -54,6 +56,23 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
_localKeyboardListener.Start();
|
||||
Closed += SettingsWindow_Closed;
|
||||
RootElement.AddHandler(UIElement.PointerPressedEvent, new PointerEventHandler(RootElement_OnPointerPressed), true);
|
||||
|
||||
if (!BuildInfo.IsCiBuild)
|
||||
{
|
||||
_internalNavItem = new NavigationViewItem
|
||||
{
|
||||
Content = "Internal Tools",
|
||||
Icon = new FontIcon { Glyph = "\uEC7A" },
|
||||
Tag = "Internal",
|
||||
};
|
||||
NavView.MenuItems.Add(_internalNavItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
_internalNavItem = null;
|
||||
}
|
||||
|
||||
Navigate("General");
|
||||
}
|
||||
|
||||
private void SettingsWindow_Closed(object sender, WindowEventArgs args)
|
||||
@@ -68,9 +87,6 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
// Delay necessary to ensure NavigationView visual state can match navigation
|
||||
Task.Delay(500).ContinueWith(_ => this.NavigationViewLoaded?.Invoke(), TaskScheduler.FromCurrentSynchronizationContext());
|
||||
|
||||
NavView.SelectedItem = NavView.MenuItems[0];
|
||||
Navigate("General");
|
||||
|
||||
if (sender is NavigationView navigationView)
|
||||
{
|
||||
// Register for pane open/close changes to announce to screen readers
|
||||
@@ -96,15 +112,33 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
Navigate((selectedItem.Tag as string)!);
|
||||
}
|
||||
|
||||
private void Navigate(string page)
|
||||
internal void Navigate(string page)
|
||||
{
|
||||
var pageType = page switch
|
||||
Type? pageType;
|
||||
switch (page)
|
||||
{
|
||||
"General" => typeof(GeneralPage),
|
||||
"Appearance" => typeof(AppearancePage),
|
||||
"Extensions" => typeof(ExtensionsPage),
|
||||
_ => null,
|
||||
};
|
||||
case "General":
|
||||
pageType = typeof(GeneralPage);
|
||||
break;
|
||||
case "Appearance":
|
||||
pageType = typeof(AppearancePage);
|
||||
break;
|
||||
case "Extensions":
|
||||
pageType = typeof(ExtensionsPage);
|
||||
break;
|
||||
case "Internal":
|
||||
pageType = typeof(InternalPage);
|
||||
break;
|
||||
case "":
|
||||
// intentional no-op: empty tag means no navigation
|
||||
pageType = null;
|
||||
break;
|
||||
default:
|
||||
// unknown page, no-op and log
|
||||
pageType = null;
|
||||
Logger.LogError($"Unknown settings page tag '{page}'");
|
||||
break;
|
||||
}
|
||||
|
||||
if (pageType is not null)
|
||||
{
|
||||
@@ -268,6 +302,12 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
BreadCrumbs.Add(new(extensionsPageType, extensionsPageType));
|
||||
BreadCrumbs.Add(new(vm.DisplayName, vm));
|
||||
}
|
||||
else if (e.SourcePageType == typeof(InternalPage) && _internalNavItem is not null)
|
||||
{
|
||||
NavView.SelectedItem = _internalNavItem;
|
||||
var pageType = "Internal";
|
||||
BreadCrumbs.Add(new(pageType, pageType));
|
||||
}
|
||||
else
|
||||
{
|
||||
BreadCrumbs.Add(new($"[{e.SourcePageType?.Name}]", string.Empty));
|
||||
|
||||
@@ -8,8 +8,10 @@ using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.UI;
|
||||
using Windows.System;
|
||||
using Windows.UI;
|
||||
@@ -99,6 +101,12 @@ internal sealed partial class DevRibbonViewModel : ObservableObject
|
||||
LatestLogs.Clear();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenInternalTools()
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Internal"));
|
||||
}
|
||||
|
||||
private sealed partial class DevRibbonTraceListener(DevRibbonViewModel viewModel) : TraceListener
|
||||
{
|
||||
private const string TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff";
|
||||
|
||||
Reference in New Issue
Block a user