diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index f2eb897a4d56..83c5cda4c8d7 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -8,7 +8,8 @@ body:
- type: markdown
attributes:
value: Please make sure to [search for existing issues](https://github.com/microsoft/PowerToys/issues) before filing a new one!
-- type: input
+- id: version
+ type: input
attributes:
label: Microsoft PowerToys version
placeholder: 0.70.0
@@ -33,14 +34,6 @@ body:
validations:
required: true
-- type: dropdown
- attributes:
- label: Running as admin
- description: Are you running PowerToys as Admin?
- options:
- - "Yes"
- - "No"
-
- type: dropdown
attributes:
label: Area(s) with issue?
@@ -83,7 +76,8 @@ body:
validations:
required: true
-- type: textarea
+- id: repro
+ type: textarea
attributes:
label: Steps to reproduce
description: We highly suggest including screenshots and a bug report log (System tray > Report bug).
@@ -104,8 +98,22 @@ body:
placeholder: What happened instead?
validations:
required: false
+
+- id: additionalInfo
+ type: textarea
+ attributes:
+ label: Additional Information
+ placeholder: |
+ OS version
+ .Net version
+ System Language
+ User or System Installation
+ Running as admin
+ validations:
+ required: false
-- type: textarea
+- id: otherSoftware
+ type: textarea
attributes:
label: Other Software
description: If you're reporting a bug about our interaction with other software, what software? What versions?
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index e7bcaab1b526..54591055367d 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -379,6 +379,7 @@ drivedetectionwarning
dshow
DSTINVERT
DUMMYUNIONNAME
+dupenv
dutil
DVASPECT
DVASPECTINFO
@@ -645,8 +646,10 @@ IBeam
ICapture
IClass
ICONERROR
+ICONINFORMATION
ICONLOCATION
IData
+IDCANCEL
IDD
IDesktop
IDirect
diff --git a/src/runner/Resources.resx b/src/runner/Resources.resx
index 902e3a08740e..97b674770b48 100644
--- a/src/runner/Resources.resx
+++ b/src/runner/Resources.resx
@@ -134,4 +134,10 @@
Administrator
+
+ Bug Report
+
+
+ Bug report is being generated
+
diff --git a/src/runner/bug_report.cpp b/src/runner/bug_report.cpp
index 9abfe6fa1802..dab1d2a18bd5 100644
--- a/src/runner/bug_report.cpp
+++ b/src/runner/bug_report.cpp
@@ -1,33 +1,366 @@
#include "pch.h"
#include "bug_report.h"
#include "Generated files/resource.h"
+#include
+#include
#include
#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+
+using namespace std;
+using namespace registry::install_scope;
+namespace fs = std::filesystem;
std::atomic_bool isBugReportThreadRunning = false;
-void launch_bug_report() noexcept
+std::string bugReportResult;
+
+bool LaunchBugReport()
{
std::wstring bug_report_path = get_module_folderpath();
bug_report_path += L"\\Tools\\PowerToys.BugReportTool.exe";
+ Logger::info("Starting the bug report tool from {}", WideStringToString(bug_report_path));
+
+ bool success = true;
+
+ try
+ {
+ SHELLEXECUTEINFOW sei{ sizeof(sei) };
+ sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE };
+ sei.lpFile = bug_report_path.c_str();
+ sei.nShow = SW_HIDE;
+
+ if (ShellExecuteExW(&sei))
+ {
+ WaitForSingleObject(sei.hProcess, INFINITE);
+ CloseHandle(sei.hProcess);
+
+ // Find the newest bug report file on the desktop
+ bugReportResult = FindNewestBugReportFile();
+ Logger::info("Bug report generated: {}", bugReportResult);
+ }
+ else
+ {
+ bugReportResult = "Failed to start bug report tool.";
+ auto message = get_last_error_message(GetLastError());
+
+ if (message.has_value())
+ {
+ bugReportResult = "Failed to start bug report tool. Internal error: " + WideStringToString(message.value());
+ }
+ else
+ {
+ bugReportResult = "Failed to start bug report tool with unknown error.";
+ }
+ success = false;
+ }
+ }
+ catch (std::exception& ex)
+ {
+ bugReportResult = std::string{ ex.what() };
+ Logger::error("Exception caught in LaunchBugReport: {}", ex.what());
+ success = false;
+ }
+
+ isBugReportThreadRunning.store(false);
+
+ return success;
+}
+void InitializeReportBugLinkAsync()
+{
bool expected_isBugReportThreadRunning = false;
- if (isBugReportThreadRunning.compare_exchange_strong(expected_isBugReportThreadRunning, true))
- {
- std::thread([bug_report_path]() {
- SHELLEXECUTEINFOW sei{ sizeof(sei) };
- sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE };
- sei.lpFile = bug_report_path.c_str();
- sei.nShow = SW_HIDE;
- if (ShellExecuteExW(&sei))
+
+ try
+ {
+ if (isBugReportThreadRunning.compare_exchange_strong(expected_isBugReportThreadRunning, true))
+ {
+ std::thread([] {
+ std::string gitHubURL;
+ bool launchBugReportResult;
+
+ notifications::show_toast(GET_RESOURCE_STRING(IDS_BUGREPORT_TEXT), GET_RESOURCE_STRING(IDS_BUGREPORT_TITLE));
+
+ // Launch the bug report task
+ auto bugReportTask = std::async(std::launch::async, [&launchBugReportResult] {
+ launchBugReportResult = LaunchBugReport();
+ });
+
+ bugReportTask.wait();
+
+ if (launchBugReportResult && !bugReportResult.empty())
+ {
+ Logger::info("Bug report successfully generated.");
+
+ std::wstring wVersion = get_product_version();
+ std::string version;
+ std::transform(wVersion.begin() + 1, wVersion.end(), std::back_inserter(version), [](wchar_t c) {
+ return static_cast(c);
+ });
+
+ std::string additionalInfo = "OS Build Version: " + GetOSVersion() + "%0a" + ".NET Version: " + GetDotNetVersion() + "%0a%0a";
+ GeneralSettings generalSettings = get_general_settings();
+ std::string isElevatedRun = generalSettings.isElevated ? "Running as admin: Yes" : "Running as admin: No";
+
+ std::string windowsSettings = ReportWindowsSettings();
+
+ const InstallScope current_install_scope = get_current_install_scope();
+
+ std::string installScope = current_install_scope == InstallScope::PerUser ? "Installation : User" : "Installation : System";
+
+ additionalInfo += windowsSettings + "%0a" + installScope + "%0a" + isElevatedRun;
+
+ gitHubURL = "https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=Issue-Bug%2CNeeds-Triage&template=bug_report.yml" +
+ std::string("&version=") + version +
+ std::string("&additionalInfo=") + additionalInfo;
+
+ std::wstring wideBugReportResult = L"Bug report generated on your desktop. Please attach the file to the GitHub issue.\n\n" + stringToWideString(bugReportResult);
+ MessageBox(nullptr, wideBugReportResult.c_str(), L"Bug Report", MB_OK | MB_ICONINFORMATION | MB_SETFOREGROUND);
+ }
+ else
+ {
+ std::wstring message;
+
+ if (bugReportResult.empty())
+ {
+ message = L"Failed to generate bug report. bugReportResult is empty.";
+ }
+ else
+ {
+
+ // Convert std::string to std::wstring
+ std::wstring wideBugReportResult = stringToWideString(bugReportResult);
+
+ // Prepare the message
+ message = L"LaunchBugReport failed: " + wideBugReportResult;
+
+ }
+
+ Logger::error(message);
+
+ MessageBox(nullptr, message.c_str(), L"Bug Report", MB_OK | MB_ICONERROR | MB_SETFOREGROUND);
+ gitHubURL = "https://aka.ms/powerToysReportBug";
+ }
+
+ // Open the URL
+ std::wstring wGitHubURL(gitHubURL.begin(), gitHubURL.end());
+ ShellExecuteW(nullptr, L"open", wGitHubURL.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
+ }).detach();
+ }
+ else
+ {
+ Logger::warn("Bug report thread is already running.");
+ }
+ }
+ catch (std::exception& ex)
+ {
+ Logger::error("Exception in InitializeReportBugLinkAsync: {}", ex.what());
+ }
+}
+
+std::string FindNewestBugReportFile()
+{
+ char* desktopPathC;
+ size_t len;
+
+ Logger::info("Searching for the newest bug report file on the desktop.");
+
+ if (_dupenv_s(&desktopPathC, &len, "USERPROFILE") != 0 || desktopPathC == nullptr)
+ {
+ Logger::error("Failed to get USERPROFILE environment variable.");
+ return "";
+ }
+
+ std::string desktopPath(desktopPathC);
+ free(desktopPathC);
+
+ desktopPath += "\\Desktop";
+ fs::path desktopDir(desktopPath);
+
+ if (!fs::exists(desktopDir) || !fs::is_directory(desktopDir))
+ {
+ Logger::error("Desktop directory not found or is not a directory: {}", desktopPath);
+
+ return "";
+ }
+
+ std::string newestFile;
+ std::time_t newestTime = 0;
+
+ for (const auto& entry : fs::directory_iterator(desktopDir))
+ {
+ if (entry.is_regular_file() && entry.path().filename().wstring().find(L"PowerToysReport_") == 0)
+ {
+ std::time_t fileTime = fs::last_write_time(entry).time_since_epoch().count();
+ if (fileTime > newestTime)
+ {
+ newestTime = fileTime;
+ newestFile = entry.path().string();
+ }
+ }
+ }
+
+ if (newestFile.empty())
+ {
+ Logger::warn("No bug report files found on the desktop.");
+ }
+ else
+ {
+ Logger::info("Newest bug report file found: " + newestFile);
+ }
+
+ return newestFile;
+}
+
+std::wstring ReadRegistryString(HKEY hKeyRoot, const std::wstring& subKey, const std::wstring& valueName)
+{
+ HKEY hKey;
+ if (RegOpenKeyEx(hKeyRoot, subKey.c_str(), 0, KEY_READ, &hKey) != ERROR_SUCCESS)
+ {
+ return L"";
+ }
+
+ wchar_t value[256];
+ DWORD bufferSize = sizeof(value);
+ DWORD type;
+ if (RegQueryValueEx(hKey, valueName.c_str(), 0, &type, (LPBYTE)value, &bufferSize) != ERROR_SUCCESS || type != REG_SZ)
+ {
+ RegCloseKey(hKey);
+ return L"";
+ }
+
+ RegCloseKey(hKey);
+ return std::wstring(value);
+}
+
+// Helper function to convert std::wstring to std::string
+std::string WideStringToString(const std::wstring& wstr)
+{
+ if (wstr.empty())
+ return std::string();
+ int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], static_cast(wstr.size()), NULL, 0, NULL, NULL);
+ std::string str(size_needed, 0);
+ WideCharToMultiByte(CP_UTF8, 0, &wstr[0], static_cast(wstr.size()), &str[0], size_needed, NULL, NULL);
+ return str;
+}
+
+std::wstring stringToWideString(const std::string& str)
+{
+ if (str.empty())
+ return std::wstring();
+ int size_needed = MultiByteToWideChar(CP_UTF8, 0, &str[0], static_cast(str.size()), NULL, 0);
+ std::wstring wstr(size_needed, 0);
+ MultiByteToWideChar(CP_UTF8, 0, &str[0], static_cast(str.size()), &wstr[0], size_needed);
+ return wstr;
+}
+
+// Function to get the .NET version
+std::string GetDotNetVersion()
+{
+ try
+ {
+ std::string dotnetInfo = (exec_and_read_output(L"dotnet --list-runtimes")).value();
+ if (dotnetInfo.empty())
+ {
+ return "Unknown .NET Version";
+ }
+
+ std::regex versionRegex(R"((\d+\.\d+\.\d+))");
+ std::sregex_iterator begin(dotnetInfo.begin(), dotnetInfo.end(), versionRegex), end;
+
+ std::string latestVersion;
+ for (std::sregex_iterator i = begin; i != end; ++i)
+ {
+ std::string version = (*i).str();
+ if (version > latestVersion)
{
- WaitForSingleObject(sei.hProcess, INFINITE);
- CloseHandle(sei.hProcess);
- static const std::wstring bugreport_success = GET_RESOURCE_STRING(IDS_BUGREPORT_SUCCESS);
- MessageBoxW(nullptr, bugreport_success.c_str(), L"PowerToys", MB_OK);
+ latestVersion = version;
}
+ }
- isBugReportThreadRunning.store(false);
- }).detach();
+ return latestVersion.empty() ? "Unknown .NET Version" : ".NET " + latestVersion;
+ }
+ catch (const std::exception& e)
+ {
+ return "Failed to get .NET Version: " + std::string(e.what());
}
}
+
+
+std::string GetOSVersion()
+{
+ OSVERSIONINFOEXW osInfo = { 0 };
+ try
+ {
+ NTSTATUS(WINAPI * RtlGetVersion)
+ (LPOSVERSIONINFOEXW) = nullptr;
+ *reinterpret_cast(&RtlGetVersion) = GetProcAddress(GetModuleHandleA("ntdll"), "RtlGetVersion");
+ if (RtlGetVersion)
+ {
+ osInfo.dwOSVersionInfoSize = sizeof(osInfo);
+ RtlGetVersion(&osInfo);
+ }
+ }
+ catch (...)
+ {
+ return "Unknown Windows Version";
+ }
+
+ try
+ {
+ std::ostringstream osVersion;
+ osVersion << osInfo.dwMajorVersion << "." << osInfo.dwMinorVersion << "." << osInfo.dwBuildNumber;
+ return osVersion.str();
+ }
+ catch (...)
+ {
+ return "Unknown Windows Version";
+ }
+}
+
+
+std::string GetModuleFolderPath()
+{
+ char buffer[MAX_PATH];
+ GetModuleFileNameA(NULL, buffer, MAX_PATH);
+ std::string::size_type pos = std::string(buffer).find_last_of("\\/");
+ return std::string(buffer).substr(0, pos);
+}
+
+std::string ReportWindowsSettings()
+{
+ std::wstring userLanguage;
+ std::wstring userLocale;
+ std::string result;
+
+ try
+ {
+ const auto lang = winrt::Windows::System::UserProfile::GlobalizationPreferences::Languages().GetAt(0);
+ userLanguage = winrt::Windows::Globalization::Language{ lang }.DisplayName().c_str();
+ wchar_t localeName[LOCALE_NAME_MAX_LENGTH]{};
+ if (!LCIDToLocaleName(GetThreadLocale(), localeName, LOCALE_NAME_MAX_LENGTH, 0))
+ {
+ throw -1;
+ }
+ userLocale = localeName;
+ }
+ catch (...)
+ {
+ return "Failed to get windows settings %0a";
+ }
+
+ result = "System Language: " + WideStringToString(userLanguage) + "%0a";
+ result += "User Locale: " + WideStringToString(userLocale) + "%0a";
+
+ return result;
+}
\ No newline at end of file
diff --git a/src/runner/bug_report.h b/src/runner/bug_report.h
index 2d7084ea2134..43f245090b84 100644
--- a/src/runner/bug_report.h
+++ b/src/runner/bug_report.h
@@ -1,3 +1,18 @@
#pragma once
+#include
+#include
+#include
+#include
+#include
-void launch_bug_report() noexcept;
\ No newline at end of file
+void InitializeReportBugLinkAsync();
+bool LaunchBugReport();
+std::string FindNewestBugReportFile();
+std::string GetDotNetVersion();
+std::string GetOSVersion();
+std::string GetModuleFolderPath();
+std::string WideStringToString(const std::wstring& wstr);
+std::wstring stringToWideString(const std::string& str);
+std::string ReportWindowsSettings();
+
+extern std::atomic_bool isBugReportThreadRunning;
diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp
index 7e56622443e0..237013e847c5 100644
--- a/src/runner/settings_window.cpp
+++ b/src/runner/settings_window.cpp
@@ -225,7 +225,7 @@ void dispatch_received_json(const std::wstring& json_to_parse)
}
else if (name == L"bugreport")
{
- launch_bug_report();
+ InitializeReportBugLinkAsync();
}
else if (name == L"killrunner")
{
diff --git a/src/runner/tray_icon.cpp b/src/runner/tray_icon.cpp
index 015fb158b8d3..962a5b98c067 100644
--- a/src/runner/tray_icon.cpp
+++ b/src/runner/tray_icon.cpp
@@ -101,7 +101,7 @@ void handle_tray_command(HWND window, const WPARAM command_id, LPARAM lparam)
break;
case ID_REPORT_BUG_COMMAND:
{
- launch_bug_report();
+ InitializeReportBugLinkAsync();
break;
}
diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
index d5f72b1c432c..2b25b1e815d3 100644
--- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
+++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
@@ -22,6 +22,8 @@
+
+
@@ -49,6 +51,7 @@
+
@@ -116,6 +119,12 @@
Always
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/LoadingMessage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/LoadingMessage.xaml
new file mode 100644
index 000000000000..1b00403251f1
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/LoadingMessage.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/LoadingMessage.xaml.cs
similarity index 58%
rename from src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.cs
rename to src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/LoadingMessage.xaml.cs
index a1cc6b078ce4..757ceb91f418 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/LoadingMessage.xaml.cs
@@ -2,14 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System;
+using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Controls
{
- public class PageLink
+ public sealed partial class LoadingMessage : ContentDialog
{
- public string Text { get; set; }
-
- public Uri Link { get; set; }
+ public LoadingMessage()
+ {
+ InitializeComponent();
+ }
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.xaml
new file mode 100644
index 000000000000..fe63532889f1
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.xaml.cs
new file mode 100644
index 000000000000..6ff63928e54e
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/PageLink.xaml.cs
@@ -0,0 +1,60 @@
+// 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 System;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Windows.System;
+
+namespace Microsoft.PowerToys.Settings.UI.Controls
+{
+ public sealed partial class PageLink : UserControl
+ {
+ public PageLink()
+ {
+ this.InitializeComponent();
+ }
+
+ public string Text { get; set; }
+
+ public Uri Link { get; set; }
+
+ public ICommand Command { get; set; }
+
+ public object CommandParameter { get; set; }
+
+ private async void OnClick(object sender, RoutedEventArgs e)
+ {
+ if (Command != null && Command.CanExecute(CommandParameter))
+ {
+ if (Command is AsyncRelayCommand asyncCommand)
+ {
+ await asyncCommand.ExecuteAsync(CommandParameter);
+ }
+ else
+ {
+ Command.Execute(CommandParameter);
+ }
+
+ // Check if CommandParameter has been updated
+ if (CommandParameter is string uriString && !string.IsNullOrEmpty(uriString))
+ {
+ _ = Launcher.LaunchUriAsync(new Uri(uriString));
+ }
+ else if (Link != null)
+ {
+ _ = Launcher.LaunchUriAsync(Link);
+ }
+ }
+ else if (Link != null)
+ {
+ var uri = CommandParameter as string ?? Link.ToString();
+ _ = Launcher.LaunchUriAsync(new Uri(uri));
+ }
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml
index c9d284bfebc6..7c645bf19275 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml
@@ -437,7 +437,11 @@
-
+
diff --git a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs
index 2bf4e0942a52..975ac7cf305c 100644
--- a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs
@@ -9,13 +9,20 @@
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
+using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
+using System.Threading;
using System.Threading.Tasks;
+using System.Web;
+using System.Windows;
+using CommunityToolkit.Common;
+using CommunityToolkit.Mvvm.Input;
using global::PowerToys.GPOWrapper;
using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Controls;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
@@ -23,11 +30,18 @@
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
using Microsoft.PowerToys.Telemetry;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.Win32;
+using Windows.Globalization;
+using Windows.System.Profile;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public class GeneralViewModel : Observable
{
+ private static readonly object LockObject = new object();
+ private static bool isBugReportThreadRunning;
+
private GeneralSettings GeneralSettingsConfig { get; set; }
private UpdatingSettings UpdatingSettingsConfig { get; set; }
@@ -62,6 +76,8 @@ public class GeneralViewModel : Observable
public string RunningAsAdminDefaultText { get; set; }
+ public AsyncRelayCommand InitializeReportBugLinkCommand { get; }
+
private string _settingsConfigFileFolder = string.Empty;
private IFileSystemWatcher _fileWatcher;
@@ -79,6 +95,7 @@ public GeneralViewModel(ISettingsRepository settingsRepository,
SelectSettingBackupDirEventHandler = new ButtonClickCommand(SelectSettingBackupDir);
RestoreConfigsEventHandler = new ButtonClickCommand(RestoreConfigsClick);
RefreshBackupStatusEventHandler = new ButtonClickCommand(RefreshBackupStatusEventHandlerClick);
+ InitializeReportBugLinkCommand = new AsyncRelayCommand(InitializeReportBugLinkAsync);
HideBackupAndRestoreMessageAreaAction = hideBackupAndRestoreMessageAreaAction;
DoBackupAndRestoreDryRun = doBackupAndRestoreDryRun;
PickSingleFolderDialog = pickSingleFolderDialog;
@@ -238,7 +255,318 @@ public GeneralViewModel(ISettingsRepository settingsRepository,
private int _initLanguagesIndex;
private bool _languageChanged;
+ private string reportBugLink = "https://aka.ms/powerToysReportBug";
+
+ public enum InstallScope
+ {
+ PerMachine = 0,
+ PerUser,
+ }
+
+ private const string InstallScopeRegKey = @"Software\Classes\powertoys\";
+
// Gets or sets a value indicating whether run powertoys on start-up.
+ public string ReportBugLink
+ {
+ get => reportBugLink;
+ set
+ {
+ reportBugLink = value;
+ OnPropertyChanged(nameof(ReportBugLink));
+ }
+ }
+
+ public async Task InitializeReportBugLinkAsync()
+ {
+ string gitHubURL = string.Empty;
+ var version = HttpUtility.UrlEncode(Helper.GetProductVersion().TrimStart('v'));
+
+ var otherSoftwareText = "OS Build Version: " + GetOSVersion() + "\n.NET Version: " + GetDotNetVersion() + "\n\n";
+ var additionalInfo = HttpUtility.UrlEncode(otherSoftwareText);
+ var isElevatedRun = IsElevated ? "Running as admin: Yes" : "Running as admin: No";
+ var windowsSettings = ReportWindowsSettings();
+
+ var current_install_scope = GetCurrentInstallScope();
+
+ var installScope = current_install_scope == InstallScope.PerUser ? "Installation : User" : "Installation : System";
+
+ additionalInfo += windowsSettings + "%0a" + installScope + "%0a" + isElevatedRun;
+
+ var loadingMessage = new LoadingMessage();
+ loadingMessage.XamlRoot = App.GetSettingsWindow().Content.XamlRoot;
+
+ var cts = new CancellationTokenSource();
+
+ try
+ {
+ var showDialogTask = loadingMessage.ShowAsync().AsTask();
+ var bugReportTask = Task.Run(() => LaunchBugReport(cts.Token), cts.Token);
+
+ // Wait for either the dialog to be closed or the bug report to be generated
+ var completedTask = await Task.WhenAny(showDialogTask, bugReportTask);
+
+ if (completedTask == showDialogTask && showDialogTask.GetResultOrDefault() == ContentDialogResult.Primary)
+ {
+ Logger.LogInfo("Bug report generation has been cancelled.");
+
+ // Cancel the bug report task if the dialog was closed with Cancel
+ await cts.CancelAsync();
+ }
+ else if (completedTask == bugReportTask)
+ {
+ loadingMessage.Hide();
+
+ // Bug report task completed
+ string reportResult = await bugReportTask;
+
+ if (string.IsNullOrEmpty(reportResult))
+ {
+ Logger.LogError("Failed to generate bug report. reportResult is empty.");
+ }
+ else
+ {
+ Logger.LogInfo("Bug report successfully generated.");
+ gitHubURL = "https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=Issue-Bug%2CNeeds-Triage&template=bug_report.yml" +
+ "&version=" + version +
+ "&additionalInfo=" + additionalInfo;
+ }
+
+ var dialog = new ContentDialog
+ {
+ Title = string.IsNullOrEmpty(reportResult) ? string.Empty : reportResult,
+ Content = string.IsNullOrEmpty(reportResult) ? "Failed to generate bug report." : "Bug report generated on your desktop. Please attach the file to the GitHub issue.",
+ CloseButtonText = "OK",
+ XamlRoot = loadingMessage.XamlRoot,
+ };
+
+ await dialog.ShowAsync();
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to generate bug report. {ex.Message}");
+ await cts.CancelAsync();
+ loadingMessage.Hide();
+ var errorDialog = new ContentDialog
+ {
+ Title = "Error",
+ Content = $"An error occurred: {ex.Message}",
+ CloseButtonText = "OK",
+ XamlRoot = loadingMessage.XamlRoot,
+ };
+ await errorDialog.ShowAsync();
+ }
+ finally
+ {
+ ReportBugLink = !string.IsNullOrEmpty(gitHubURL) ? gitHubURL : "https://aka.ms/powerToysReportBug";
+ }
+ }
+
+ // Updated LaunchBugReport method to support cancellation
+ public string LaunchBugReport(CancellationToken token)
+ {
+ string bugReportPath = GetModuleFolderPath() + "\\..\\Tools\\PowerToys.BugReportTool.exe";
+ string bugReportFileName = string.Empty;
+
+ lock (LockObject)
+ {
+ if (!isBugReportThreadRunning)
+ {
+ isBugReportThreadRunning = true;
+ Thread bugReportThread = new Thread(() =>
+ {
+ ProcessStartInfo startInfo = new ProcessStartInfo
+ {
+ FileName = bugReportPath,
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ };
+
+ try
+ {
+ using (Process process = Process.Start(startInfo))
+ {
+ while (!process.HasExited)
+ {
+ if (token.IsCancellationRequested)
+ {
+ process.Kill();
+ return;
+ }
+
+ Thread.Sleep(100);
+ }
+ }
+
+ // Find the newest bug report file on the desktop
+ bugReportFileName = FindNewestBugReportFile();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to start bug report tool: {ex.Message}");
+ MessageBox.Show("Failed to start bug report tool: " + ex.Message, "Error");
+ }
+ finally
+ {
+ isBugReportThreadRunning = false;
+ }
+ });
+ bugReportThread.IsBackground = true;
+ bugReportThread.Start();
+ bugReportThread.Join();
+ }
+ }
+
+ return bugReportFileName;
+ }
+
+ private static string GetModuleFolderPath()
+ {
+ // You would implement this method to get the path to your module folder in C#
+ return Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
+ }
+
+ private string FindNewestBugReportFile()
+ {
+ string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
+ DirectoryInfo directoryInfo = new DirectoryInfo(desktopPath);
+
+ // Get all files starting with "PowerToysReport_"
+ FileInfo[] files = directoryInfo.GetFiles("PowerToysReport_*");
+
+ if (files.Length == 0)
+ {
+ return string.Empty;
+ }
+
+ // Find the newest file
+ FileInfo newestFile = files.OrderByDescending(f => f.LastWriteTime).FirstOrDefault();
+ return newestFile?.Name;
+ }
+
+ public static string GetDotNetVersion()
+ {
+ var output = ExecuteCommand("dotnet --list-runtimes");
+ if (string.IsNullOrEmpty(output))
+ {
+ return "Unknown .NET Version";
+ }
+
+ var versions = output
+ .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
+ .Select(line =>
+ {
+ var versionString = line.Split(' ')[1];
+ if (Version.TryParse(versionString, out var version))
+ {
+ return version;
+ }
+
+ return new Version(0, 0, 0);
+ })
+ .Where(version => version > new Version(0, 0, 0))
+ .ToList();
+
+ var latestVersion = versions.Max();
+ return $".NET {latestVersion}";
+ }
+
+ private static string ExecuteCommand(string command)
+ {
+ try
+ {
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = "cmd.exe",
+ Arguments = $"/C {command}",
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ },
+ };
+
+ process.Start();
+ using var reader = process.StandardOutput;
+ return reader.ReadToEnd();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to execute command: {ex.Message}");
+ return $"Failed to execute command: {ex.Message}";
+ }
+ }
+
+ private string GetOSVersion()
+ {
+ var attrNames = new List { "OSVersionFull" };
+ var attrData = AnalyticsInfo.GetSystemPropertiesAsync(attrNames).AsTask().GetAwaiter().GetResult();
+ var osVersion = string.Empty;
+ if (attrData.ContainsKey("OSVersionFull"))
+ {
+ osVersion = attrData["OSVersionFull"];
+ var versionParts = osVersion.Split('.');
+ if (versionParts.Length >= 3)
+ {
+ osVersion = $"{versionParts[0]}.{versionParts[1]}.{versionParts[2]}";
+ }
+ }
+
+ return osVersion.ToString();
+ }
+
+ public static string ReportWindowsSettings()
+ {
+ string userLanguage;
+ string userLocale;
+ string result;
+
+ try
+ {
+ var languages = ApplicationLanguages.Languages;
+ userLanguage = new Language(languages[0]).DisplayName;
+
+ userLocale = CultureInfo.CurrentCulture.Name;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to get windows settings: {ex.Message}");
+ return "Failed to get windows settings\n";
+ }
+
+ result = "System Language: " + userLanguage + "%0a";
+ result += "User Locale: " + userLocale + "%0a";
+
+ return result;
+ }
+
+ public static InstallScope GetCurrentInstallScope()
+ {
+ // Check HKLM first
+ if (Registry.LocalMachine.OpenSubKey(InstallScopeRegKey) != null)
+ {
+ return InstallScope.PerMachine;
+ }
+
+ // If not found, check HKCU
+ var userKey = Registry.CurrentUser.OpenSubKey(InstallScopeRegKey);
+ if (userKey != null)
+ {
+ var installScope = userKey.GetValue("InstallScope") as string;
+ userKey.Close();
+ if (!string.IsNullOrEmpty(installScope) && installScope.Contains("perUser"))
+ {
+ return InstallScope.PerUser;
+ }
+ }
+
+ return InstallScope.PerMachine; // Default if no specific registry key found
+ }
+
public bool Startup
{
get