diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 425ec9bbd0..14d58e86b9 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -789,6 +789,7 @@ iid
Iindex
IIO
iiq
+IJson
Ijwhost
IKs
ILogon
@@ -914,6 +915,7 @@ Lastdevice
Laute
laviusmotileng
LAYOUTRTL
+Lbl
LBUTTON
LBUTTONDBLCLK
LBUTTONDOWN
@@ -1342,6 +1344,7 @@ PARTIALCONFIRMATIONDIALOGTITLE
pasteplain
PATCOPY
pathcch
+PATHEXT
Pathto
PATINVERT
PATPAINT
@@ -1469,6 +1472,7 @@ pscid
PSECURITY
psfgao
psfi
+PSMODULEPATH
Psr
psrm
psrree
@@ -1691,6 +1695,7 @@ setlocal
SETREDRAW
SETTEXT
SETTINGCHANGE
+SETTINGSCHANGED
settingsheader
settingshotkeycontrol
SETWORKAREA
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index c3c91deb6a..d217b2dd1e 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -98,6 +98,10 @@
"WinUI3Apps\\Powertoys.Peek.UI.exe",
"WinUI3Apps\\Powertoys.Peek.dll",
+ "WinUI3Apps\\PowerToys.EnvironmentVariablesModuleInterface.dll",
+ "WinUI3Apps\\PowerToys.EnvironmentVariables.dll",
+ "WinUI3Apps\\PowerToys.EnvironmentVariables.exe",
+
"PowerToys.ImageResizer.exe",
"PowerToys.ImageResizer.dll",
"PowerToys.ImageResizerExt.dll",
diff --git a/Directory.Packages.props b/Directory.Packages.props
index daa51a7736..ab19b0f29e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -8,6 +8,7 @@
+
diff --git a/NOTICE.md b/NOTICE.md
index efe03a0e85..0733fc05dc 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -312,6 +312,7 @@ SOFTWARE.
- CommunityToolkit.Mvvm 8.2.0
- CommunityToolkit.WinUI.Animations 8.0.230907
- CommunityToolkit.WinUI.Controls.Primitives 8.0.230907
+- CommunityToolkit.WinUI.Controls.Segmented 8.0.230907
- CommunityToolkit.WinUI.Controls.SettingsControls 8.0.230907
- CommunityToolkit.WinUI.Controls.Sizers 8.0.230907
- CommunityToolkit.WinUI.Converters 8.0.230907
diff --git a/PowerToys.sln b/PowerToys.sln
index d629d970d4..21c1848742 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -539,6 +539,12 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CropAndLockModuleInterface"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests-FancyZonesEditor", "src\modules\fancyzones\UnitTests-FancyZonesEditor\UnitTests-FancyZonesEditor.csproj", "{FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvironmentVariables", "EnvironmentVariables", "{538ED0BB-B863-4B20-98CC-BCDF7FA0B68A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvironmentVariables", "src\modules\EnvironmentVariables\EnvironmentVariables\EnvironmentVariables.csproj", "{51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EnvironmentVariablesModuleInterface", "src\modules\EnvironmentVariables\EnvironmentVariablesModuleInterface\EnvironmentVariablesModuleInterface.vcxproj", "{B9420661-B0E4-4241-ABD4-4A27A1F64250}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2321,6 +2327,30 @@ Global
{FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Release|x64.Build.0 = Release|x64
{FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Release|x86.ActiveCfg = Release|x64
{FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Release|x86.Build.0 = Release|x64
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|ARM64.Build.0 = Debug|ARM64
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|x64.ActiveCfg = Debug|x64
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|x64.Build.0 = Debug|x64
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|x86.ActiveCfg = Debug|x64
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|x86.Build.0 = Debug|x64
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|ARM64.ActiveCfg = Release|ARM64
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|ARM64.Build.0 = Release|ARM64
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|x64.ActiveCfg = Release|x64
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|x64.Build.0 = Release|x64
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|x86.ActiveCfg = Release|x64
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|x86.Build.0 = Release|x64
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|ARM64.Build.0 = Debug|ARM64
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|x64.ActiveCfg = Debug|x64
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|x64.Build.0 = Debug|x64
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|x86.ActiveCfg = Debug|x64
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|x86.Build.0 = Debug|x64
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|ARM64.ActiveCfg = Release|ARM64
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|ARM64.Build.0 = Release|ARM64
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|x64.ActiveCfg = Release|x64
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|x64.Build.0 = Release|x64
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|x86.ActiveCfg = Release|x64
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2516,6 +2546,9 @@ Global
{F5E1146E-B7B3-4E11-85FD-270A500BD78C} = {3B227528-4BA6-4CAF-B44A-A10C78A64849}
{3157FA75-86CF-4EE2-8F62-C43F776493C6} = {3B227528-4BA6-4CAF-B44A-A10C78A64849}
{FC8EB78F-F061-4BD9-A3F6-507BEA965E2B} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
+ {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
+ {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A}
+ {B9420661-B0E4-4241-ABD4-4A27A1F64250} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/doc/images/icons/Environment Manager.png b/doc/images/icons/Environment Manager.png
new file mode 100644
index 0000000000..1d14dbeaf8
Binary files /dev/null and b/doc/images/icons/Environment Manager.png differ
diff --git a/installer/PowerToysSetup/EnvironmentVariables.wxs b/installer/PowerToysSetup/EnvironmentVariables.wxs
new file mode 100644
index 0000000000..44567055af
--- /dev/null
+++ b/installer/PowerToysSetup/EnvironmentVariables.wxs
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/installer/PowerToysSetup/PowerToysInstaller.wixproj b/installer/PowerToysSetup/PowerToysInstaller.wixproj
index e1cbe42d5f..9992d919ae 100644
--- a/installer/PowerToysSetup/PowerToysInstaller.wixproj
+++ b/installer/PowerToysSetup/PowerToysInstaller.wixproj
@@ -35,6 +35,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
call move /Y ..\..\..\BaseApplications.wxs.bk ..\..\..\BaseApplications.wxs
call move /Y ..\..\..\ColorPicker.wxs.bk ..\..\..\ColorPicker.wxs
call move /Y ..\..\..\Core.wxs.bk ..\..\..\Core.wxs
+ call move /Y ..\..\..\EnvironmentVariables.wxs.bk ..\..\..\EnvironmentVariables.wxs
call move /Y ..\..\..\FileExplorerPreview.wxs.bk ..\..\..\FileExplorerPreview.wxs
call move /Y ..\..\..\FileLocksmith.wxs.bk ..\..\..\FileLocksmith.wxs
call move /Y ..\..\..\Hosts.wxs.bk ..\..\..\Hosts.wxs
@@ -104,6 +105,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
+
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index 853a1ac795..2dea1fab55 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -71,6 +71,7 @@
+
diff --git a/installer/PowerToysSetup/generateAllFileComponents.ps1 b/installer/PowerToysSetup/generateAllFileComponents.ps1
index 6542bc45c3..f233ac5da6 100644
--- a/installer/PowerToysSetup/generateAllFileComponents.ps1
+++ b/installer/PowerToysSetup/generateAllFileComponents.ps1
@@ -32,6 +32,10 @@ Invoke-Expression -Command "$PSScriptRoot\generateFileComponents.ps1 -fileListNa
Invoke-Expression -Command "$PSScriptRoot\generateFileList.ps1 -fileDepsJson """" -fileListName ColorPickerAssetsFiles -wxsFilePath $PSScriptRoot\ColorPicker.wxs -depsPath ""$PSScriptRoot..\..\..\$platform\Release\Assets\ColorPicker"""
Invoke-Expression -Command "$PSScriptRoot\generateFileComponents.ps1 -fileListName ""ColorPickerAssetsFiles"" -wxsFilePath $PSScriptRoot\ColorPicker.wxs -regroot $registryroot"
+#Environment Variables
+Invoke-Expression -Command "$PSScriptRoot\generateFileList.ps1 -fileDepsJson """" -fileListName EnvironmentVariablesAssetsFiles -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -depsPath ""$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\EnvironmentVariables"""
+Invoke-Expression -Command "$PSScriptRoot\generateFileComponents.ps1 -fileListName ""EnvironmentVariablesAssetsFiles"" -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -regroot $registryroot"
+
#FileExplorerAdd-ons
Invoke-Expression -Command "$PSScriptRoot\generateFileList.ps1 -fileDepsJson """" -fileListName MonacoPreviewHandlerMonacoAssetsFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath ""$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco"""
Invoke-Expression -Command "$PSScriptRoot\generateFileList.ps1 -fileDepsJson """" -fileListName MonacoPreviewHandlerCustomLanguagesFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath ""$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco\customLanguages"""
diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp
index 579fe3ba6b..4374d2403d 100644
--- a/installer/PowerToysSetupCustomActions/CustomAction.cpp
+++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp
@@ -1005,7 +1005,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
}
processes.resize(bytes / sizeof(processes[0]));
- std::array processesToTerminate = {
+ std::array processesToTerminate = {
L"PowerToys.PowerLauncher.exe",
L"PowerToys.Settings.exe",
L"PowerToys.Awake.exe",
@@ -1033,6 +1033,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.MouseWithoutBordersHelper.exe",
L"PowerToys.MouseWithoutBordersService.exe",
L"PowerToys.CropAndLock.exe",
+ L"PowerToys.EnvironmentVariables.exe",
L"PowerToys.exe",
};
diff --git a/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj b/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj
index 2768846e22..81235042b4 100644
--- a/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj
+++ b/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj
@@ -52,6 +52,7 @@
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\BaseApplications.wxs"" ""$(ProjectDir)..\PowerToysSetup\BaseApplications.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\ColorPicker.wxs"" ""$(ProjectDir)..\PowerToysSetup\ColorPicker.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\Core.wxs"" ""$(ProjectDir)..\PowerToysSetup\Core.wxs.bk""""
+ call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\EnvironmentVariables.wxs"" ""$(ProjectDir)..\PowerToysSetup\EnvironmentVariables.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\FileExplorerPreview.wxs"" ""$(ProjectDir)..\PowerToysSetup\FileExplorerPreview.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\FileLocksmith.wxs"" ""$(ProjectDir)..\PowerToysSetup\FileLocksmith.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\Hosts.wxs"" ""$(ProjectDir)..\PowerToysSetup\Hosts.wxs.bk""""
diff --git a/src/codeAnalysis/GlobalSuppressions.cs b/src/codeAnalysis/GlobalSuppressions.cs
index f130177ee3..64e1ab9b16 100644
--- a/src/codeAnalysis/GlobalSuppressions.cs
+++ b/src/codeAnalysis/GlobalSuppressions.cs
@@ -33,7 +33,7 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Static methods may improve performance but decrease maintainability")]
[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Renaming everything would be a lot of work. It does not do any harm if an EventHandler delegate ends with the suffix EventHandler. Besides this, the Rule causes some false positives.")]
[assembly: SuppressMessage("Performance", "CA1838:Avoid 'StringBuilder' parameters for P/Invokes", Justification = "We are not concerned about the performance impact of marshaling a StringBuilder")]
-[assembly: SuppressMessage("Performance", "CA1852:Seal internal types", Justification = "The assembly is getting a ComVisible set to false already.", Scope="namespaceanddescendants", Target="MouseWithoutBorders")]
+[assembly: SuppressMessage("Performance", "CA1852:Seal internal types", Justification = "The assembly is getting a ComVisible set to false already.", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")]
// Threading suppressions
[assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.Controls.Notification.OnClose")]
@@ -57,8 +57,8 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("CodeQuality", "IDE0076:Invalid global 'SuppressMessageAttribute'", Justification = "Affect predefined suppressions.")]
// Dotnet port
-[assembly: SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "", Scope="namespaceanddescendants", Target="MouseWithoutBorders")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "", Scope="namespaceanddescendants", Target="MouseWithoutBorders")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "", Scope="namespaceanddescendants", Target="MouseWithoutBorders")]
-[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "", Scope="namespaceanddescendants", Target="MouseWithoutBorders")]
-[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "", Scope="namespaceanddescendants", Target="MouseWithoutBorders")]
+[assembly: SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")]
+[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")]
+[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")]
diff --git a/src/common/Common.UI/SettingsDeepLink.cs b/src/common/Common.UI/SettingsDeepLink.cs
index 1a154f0a82..19cee0b7d8 100644
--- a/src/common/Common.UI/SettingsDeepLink.cs
+++ b/src/common/Common.UI/SettingsDeepLink.cs
@@ -28,6 +28,8 @@ namespace Common.UI
PowerOCR,
RegistryPreview,
CropAndLock,
+ EnvironmentVariables,
+ Dashboard,
}
private static string SettingsWindowNameToString(SettingsWindow value)
@@ -68,6 +70,10 @@ namespace Common.UI
return "RegistryPreview";
case SettingsWindow.CropAndLock:
return "CropAndLock";
+ case SettingsWindow.EnvironmentVariables:
+ return "EnvironmentVariables";
+ case SettingsWindow.Dashboard:
+ return "Dashboard";
default:
{
return string.Empty;
diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp
index 9fd9d7d2af..378e3dcb52 100644
--- a/src/common/GPOWrapper/GPOWrapper.cpp
+++ b/src/common/GPOWrapper/GPOWrapper.cpp
@@ -148,4 +148,8 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast(powertoys_gpo::getRunPluginEnabledValue(winrt::to_string(pluginID)));
}
+ GpoRuleConfigured GPOWrapper::GetConfiguredEnvironmentVariablesEnabledValue()
+ {
+ return static_cast(powertoys_gpo::getConfiguredEnvironmentVariablesEnabledValue());
+ }
}
diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h
index 34d3f0f5c2..c60f5e78ef 100644
--- a/src/common/GPOWrapper/GPOWrapper.h
+++ b/src/common/GPOWrapper/GPOWrapper.h
@@ -43,6 +43,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetDisableAutomaticUpdateDownloadValue();
static GpoRuleConfigured GetAllowExperimentationValue();
static GpoRuleConfigured GetRunPluginEnabledValue(winrt::hstring const& pluginID);
+ static GpoRuleConfigured GetConfiguredEnvironmentVariablesEnabledValue();
};
}
diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl
index 5472ad1e87..748f63db91 100644
--- a/src/common/GPOWrapper/GPOWrapper.idl
+++ b/src/common/GPOWrapper/GPOWrapper.idl
@@ -47,6 +47,7 @@ namespace PowerToys
static GpoRuleConfigured GetDisableAutomaticUpdateDownloadValue();
static GpoRuleConfigured GetAllowExperimentationValue();
static GpoRuleConfigured GetRunPluginEnabledValue(String pluginID);
+ static GpoRuleConfigured GetConfiguredEnvironmentVariablesEnabledValue();
}
}
}
diff --git a/src/common/interop/interop.cpp b/src/common/interop/interop.cpp
index 46ce729219..ebf6c8be22 100644
--- a/src/common/interop/interop.cpp
+++ b/src/common/interop/interop.cpp
@@ -266,5 +266,13 @@ public
static String ^ CropAndLockReparentEvent() {
return gcnew String(CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT);
}
+
+ static String ^ ShowEnvironmentVariablesSharedEvent() {
+ return gcnew String(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT);
+ }
+
+ static String ^ ShowEnvironmentVariablesAdminSharedEvent() {
+ return gcnew String(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT);
+ }
};
}
diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h
index 4761d3c22c..3d71b5fdba 100644
--- a/src/common/interop/shared_constants.h
+++ b/src/common/interop/shared_constants.h
@@ -82,6 +82,10 @@ namespace CommonSharedConstants
const wchar_t CROP_AND_LOCK_THUMBNAIL_EVENT[] = L"Local\\PowerToysCropAndLockThumbnailEvent-1637be50-da72-46b2-9220-b32b206b2434";
const wchar_t CROP_AND_LOCK_EXIT_EVENT[] = L"Local\\PowerToysCropAndLockExitEvent-d995d409-7b70-482b-bad6-e7c8666f375a";
+ // Path to the events used by EnvironmentVariables
+ const wchar_t SHOW_ENVIRONMENT_VARIABLES_EVENT[] = L"Local\\PowerToysEnvironmentVariables-ShowEnvironmentVariablesEvent-1021f616-e951-4d64-b231-a8f972159978";
+ const wchar_t SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT[] = L"Local\\PowerToysEnvironmentVariables-EnvironmentVariablesAdminEvent-8c95d2ad-047c-49a2-9e8b-b4656326cfb2";
+
// Max DWORD for key code to disable keys.
const DWORD VK_DISABLED = 0x100;
}
diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h
index 8498f9bf4b..038a9a8e20 100644
--- a/src/common/logger/logger_settings.h
+++ b/src/common/logger/logger_settings.h
@@ -62,6 +62,7 @@ struct LogSettings
inline const static std::string registryPreviewLoggerName = "registrypreview";
inline const static std::string cropAndLockLoggerName = "crop-and-lock";
inline const static std::wstring registryPreviewLogPath = L"Logs\\registryPreview-log.txt";
+ inline const static std::string environmentVariablesLoggerName = "environment-variables";
inline const static int retention = 30;
std::wstring logLevel;
LogSettings();
diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h
index bbb6545f85..840fa266f0 100644
--- a/src/common/utils/gpo.h
+++ b/src/common/utils/gpo.h
@@ -55,6 +55,7 @@ namespace powertoys_gpo {
const std::wstring POLICY_CONFIGURE_ENABLED_REGISTRY_PREVIEW = L"ConfigureEnabledUtilityRegistryPreview";
const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_WITHOUT_BORDERS = L"ConfigureEnabledUtilityMouseWithoutBorders";
const std::wstring POLICY_CONFIGURE_ENABLED_PEEK = L"ConfigureEnabledUtilityPeek";
+ const std::wstring POLICY_CONFIGURE_ENABLED_ENVIRONMENT_VARIABLES = L"ConfigureEnabledUtilityEnvironmentVariables";
// The registry value names for PowerToys installer and update policies.
const std::wstring POLICY_DISABLE_PER_USER_INSTALLATION = L"PerUserInstallationDisabled";
@@ -376,6 +377,11 @@ namespace powertoys_gpo {
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_REGISTRY_PREVIEW);
}
+ inline gpo_rule_configured_t getConfiguredEnvironmentVariablesEnabledValue()
+ {
+ return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_ENVIRONMENT_VARIABLES);
+ }
+
inline gpo_rule_configured_t getDisablePerUserInstallationValue()
{
return getConfiguredValue(POLICY_DISABLE_PER_USER_INSTALLATION);
@@ -440,5 +446,4 @@ namespace powertoys_gpo {
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS);
}
}
-
}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/EnvironmentVariables.ico b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/EnvironmentVariables.ico
new file mode 100644
index 0000000000..dc07264100
Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/EnvironmentVariables.ico differ
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/LockScreenLogo.scale-200.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/LockScreenLogo.scale-200.png
new file mode 100644
index 0000000000..7440f0d4bf
Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/LockScreenLogo.scale-200.png differ
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/ProfileIcon.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/ProfileIcon.png
new file mode 100644
index 0000000000..7440f0d4bf
Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/ProfileIcon.png differ
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/SplashScreen.scale-200.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/SplashScreen.scale-200.png
new file mode 100644
index 0000000000..32f486a867
Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/SplashScreen.scale-200.png differ
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square150x150Logo.scale-200.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square150x150Logo.scale-200.png
new file mode 100644
index 0000000000..53ee3777ea
Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square150x150Logo.scale-200.png differ
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square44x44Logo.scale-200.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square44x44Logo.scale-200.png
new file mode 100644
index 0000000000..f713bba67f
Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square44x44Logo.scale-200.png differ
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square44x44Logo.targetsize-24_altform-unplated.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square44x44Logo.targetsize-24_altform-unplated.png
new file mode 100644
index 0000000000..dc9f5bea0c
Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square44x44Logo.targetsize-24_altform-unplated.png differ
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/StoreLogo.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/StoreLogo.png
new file mode 100644
index 0000000000..a4586f26bd
Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/StoreLogo.png differ
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/SystemIcon.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/SystemIcon.png
new file mode 100644
index 0000000000..af9436bd14
Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/SystemIcon.png differ
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/UserIcon.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/UserIcon.png
new file mode 100644
index 0000000000..7440f0d4bf
Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/UserIcon.png differ
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Wide310x150Logo.scale-200.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Wide310x150Logo.scale-200.png
new file mode 100644
index 0000000000..8b4a5d0dd5
Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Wide310x150Logo.scale-200.png differ
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariables.csproj b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariables.csproj
new file mode 100644
index 0000000000..8d10c65e2c
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariables.csproj
@@ -0,0 +1,114 @@
+
+
+
+
+ WinExe
+ net7.0-windows10.0.20348.0
+ 10.0.19041.0
+ 10.0.19041.0
+ EnvironmentVariables
+ app.manifest
+ win10-x64;win10-arm64
+ true
+ true
+ true
+ None
+ false
+ false
+ true
+ ..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps
+ PowerToys.EnvironmentVariables
+ DISABLE_XAML_GENERATED_MAIN,TRACE
+ Assets/EnvironmentVariables/EnvironmentVariables.ico
+ true
+
+ PowerToys.EnvironmentVariables.pri
+
+
+
+
+
+
+
+
+
+
+
+ win10-x64
+
+
+ win10-arm64
+
+
+
+
+ PowerToys.GPOWrapper
+ $(OutDir)
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ https://pkgs.dev.azure.com/dotnet/CommunityToolkit/_packaging/CommunityToolkit-Labs/nuget/v3/index.json
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+
+
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml
new file mode 100644
index 0000000000..ae8ef32742
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml.cs
new file mode 100644
index 0000000000..ca2fdb72b8
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml.cs
@@ -0,0 +1,96 @@
+// 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.IO.Abstractions;
+using EnvironmentVariables.Helpers;
+using EnvironmentVariables.ViewModels;
+using ManagedCommon;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.UI.Dispatching;
+using Microsoft.UI.Xaml;
+
+namespace EnvironmentVariables
+{
+ ///
+ /// Provides application-specific behavior to supplement the default Application class.
+ ///
+ public partial class App : Application
+ {
+ public IHost Host { get; }
+
+ public static T GetService()
+ where T : class
+ {
+ if ((App.Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
+ {
+ throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
+ }
+
+ return service;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ this.InitializeComponent();
+
+ Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder().UseContentRoot(AppContext.BaseDirectory).ConfigureServices((context, services) =>
+ {
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddSingleton();
+ }).Build();
+
+ UnhandledException += App_UnhandledException;
+ }
+
+ private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
+ {
+ Logger.LogError("Unhandled exception", e.Exception);
+ }
+
+ ///
+ /// Invoked when the application is launched.
+ ///
+ /// Details about the launch request and process.
+ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
+ {
+ var cmdArgs = Environment.GetCommandLineArgs();
+ if (cmdArgs?.Length > 1)
+ {
+ if (int.TryParse(cmdArgs[cmdArgs.Length - 1], out int powerToysRunnerPid))
+ {
+ Logger.LogInfo($"EnvironmentVariables started from the PowerToys Runner. Runner pid={powerToysRunnerPid}.");
+
+ var dispatcher = DispatcherQueue.GetForCurrentThread();
+ RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () =>
+ {
+ Logger.LogInfo("PowerToys Runner exited. Exiting EnvironmentVariables");
+ dispatcher.TryEnqueue(App.Current.Exit);
+ });
+ }
+ }
+ else
+ {
+ Logger.LogInfo($"EnvironmentVariables started detached from PowerToys Runner.");
+ }
+
+ PowerToysTelemetry.Log.WriteEvent(new EnvironmentVariables.Telemetry.EnvironmentVariablesOpenedEvent());
+
+ window = new MainWindow();
+ window.Activate();
+ }
+
+ private Window window;
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToBoolConverter.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToBoolConverter.cs
new file mode 100644
index 0000000000..81e1ef1efd
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToBoolConverter.cs
@@ -0,0 +1,27 @@
+// 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 EnvironmentVariables.Models;
+using Microsoft.UI.Xaml.Data;
+
+namespace EnvironmentVariables.Converters;
+
+public class EnvironmentStateToBoolConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ var type = (EnvironmentState)value;
+ return type switch
+ {
+ EnvironmentState.Unchanged => false,
+ _ => true,
+ };
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToMessageConverter.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToMessageConverter.cs
new file mode 100644
index 0000000000..ffe473ef28
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToMessageConverter.cs
@@ -0,0 +1,32 @@
+// 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 EnvironmentVariables.Helpers;
+using EnvironmentVariables.Models;
+using Microsoft.UI.Xaml.Data;
+
+namespace EnvironmentVariables.Converters;
+
+public class EnvironmentStateToMessageConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ var resourceLoader = ResourceLoaderInstance.ResourceLoader;
+ var type = (EnvironmentState)value;
+ return type switch
+ {
+ EnvironmentState.Unchanged => string.Empty,
+ EnvironmentState.ChangedOnStartup => resourceLoader.GetString("StateNotUpToDateOnStartupMsg"),
+ EnvironmentState.EnvironmentMessageReceived => resourceLoader.GetString("StateNotUpToDateEnvironmentMessageReceivedMsg"),
+ EnvironmentState.ProfileNotApplicable => resourceLoader.GetString("StateProfileNotApplicableMsg"),
+ _ => throw new NotImplementedException(),
+ };
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToTitleConverter.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToTitleConverter.cs
new file mode 100644
index 0000000000..344c9b1072
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToTitleConverter.cs
@@ -0,0 +1,29 @@
+// 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 EnvironmentVariables.Helpers;
+using EnvironmentVariables.Models;
+using Microsoft.UI.Xaml.Data;
+
+namespace EnvironmentVariables.Converters;
+
+public class EnvironmentStateToTitleConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ var resourceLoader = ResourceLoaderInstance.ResourceLoader;
+ var type = (EnvironmentState)value;
+ return type switch
+ {
+ EnvironmentState.ProfileNotApplicable => resourceLoader.GetString("ProfileNotApplicableTitle"),
+ _ => resourceLoader.GetString("StateNotUpToDateTitle"),
+ };
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToVisibilityConverter.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToVisibilityConverter.cs
new file mode 100644
index 0000000000..682a93cc13
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToVisibilityConverter.cs
@@ -0,0 +1,28 @@
+// 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 EnvironmentVariables.Models;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Data;
+
+namespace EnvironmentVariables.Converters;
+
+public class EnvironmentStateToVisibilityConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ var type = (EnvironmentState)value;
+ return type switch
+ {
+ EnvironmentState.Unchanged => Visibility.Collapsed,
+ _ => Visibility.Visible,
+ };
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/VariableTypeToGlyphConverter.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/VariableTypeToGlyphConverter.cs
new file mode 100644
index 0000000000..fc6ec01f30
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/VariableTypeToGlyphConverter.cs
@@ -0,0 +1,31 @@
+// 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 EnvironmentVariables.Models;
+using Microsoft.UI.Xaml.Data;
+
+namespace EnvironmentVariables.Converters;
+
+public class VariableTypeToGlyphConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ var type = (VariablesSetType)value;
+ return type switch
+ {
+ VariablesSetType.User => "\uE77B",
+ VariablesSetType.System => "\uE977",
+ VariablesSetType.Profile => "\uEDE3",
+ VariablesSetType.Path => "\uE8AC",
+ VariablesSetType.Duplicate => "\uE8C8",
+ _ => throw new NotImplementedException(),
+ };
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml
new file mode 100644
index 0000000000..b5d7dafdb1
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml.cs
new file mode 100644
index 0000000000..ec24920054
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml.cs
@@ -0,0 +1,76 @@
+// 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.Runtime.InteropServices;
+using EnvironmentVariables.Helpers;
+using EnvironmentVariables.Helpers.Win32;
+using EnvironmentVariables.ViewModels;
+using Microsoft.UI.Dispatching;
+using WinUIEx;
+
+namespace EnvironmentVariables
+{
+ ///
+ /// An empty window that can be used on its own or navigated to within a Frame.
+ ///
+ public sealed partial class MainWindow : WindowEx
+ {
+ public MainWindow()
+ {
+ this.InitializeComponent();
+
+ ExtendsContentIntoTitleBar = true;
+ SetTitleBar(titleBar);
+
+ AppWindow.SetIcon("Assets/EnvironmentVariables/EnvironmentVariables.ico");
+ var loader = ResourceLoaderInstance.ResourceLoader;
+ var title = App.GetService().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
+ Title = title;
+ AppTitleTextBlock.Text = title;
+
+ RegisterWindow();
+ }
+
+ private static readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
+ private static NativeMethods.WinProc newWndProc;
+ private static IntPtr oldWndProc = IntPtr.Zero;
+
+ private void RegisterWindow()
+ {
+ newWndProc = new NativeMethods.WinProc(WndProc);
+
+ var handle = this.GetWindowHandle();
+
+ oldWndProc = NativeMethods.SetWindowLongPtr(handle, NativeMethods.WindowLongIndexFlags.GWL_WNDPROC, newWndProc);
+ }
+
+ private static IntPtr WndProc(IntPtr hWnd, NativeMethods.WindowMessage msg, IntPtr wParam, IntPtr lParam)
+ {
+ switch (msg)
+ {
+ case NativeMethods.WindowMessage.WM_SETTINGSCHANGED:
+ {
+ var lParamStr = Marshal.PtrToStringUTF8(lParam);
+ if (lParamStr == "Environment")
+ {
+ // Do not react on self - not nice, re-check this
+ if (wParam != (IntPtr)0x12345)
+ {
+ var viewModel = App.GetService();
+ viewModel.EnvironmentState = Models.EnvironmentState.EnvironmentMessageReceived;
+ }
+ }
+
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return NativeMethods.CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam);
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Styles/TextBlock.xaml b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Styles/TextBlock.xaml
new file mode 100644
index 0000000000..bf6d01383b
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Styles/TextBlock.xaml
@@ -0,0 +1,8 @@
+
+
+ 12
+
+
\ No newline at end of file
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Views/MainPage.xaml b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Views/MainPage.xaml
new file mode 100644
index 0000000000..08400e5478
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Views/MainPage.xaml
@@ -0,0 +1,744 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Views/MainPage.xaml.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Views/MainPage.xaml.cs
new file mode 100644
index 0000000000..7ca3c00456
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Views/MainPage.xaml.cs
@@ -0,0 +1,551 @@
+// 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.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using CommunityToolkit.Mvvm.Input;
+using EnvironmentVariables.Models;
+using EnvironmentVariables.ViewModels;
+using Microsoft.UI.Xaml.Controls;
+using Windows.Foundation.Collections;
+
+namespace EnvironmentVariables.Views
+{
+ public sealed partial class MainPage : Page
+ {
+ private sealed class RelayCommandParameter
+ {
+ public RelayCommandParameter(Variable variable, VariablesSet set)
+ {
+ Variable = variable;
+ this.Set = set;
+ }
+
+ public Variable Variable { get; set; }
+
+ public VariablesSet Set { get; set; }
+ }
+
+ public MainViewModel ViewModel { get; private set; }
+
+ public ICommand EditCommand => new RelayCommand(EditVariable);
+
+ public ICommand NewProfileCommand => new AsyncRelayCommand(AddProfileAsync);
+
+ public ICommand AddProfileCommand => new RelayCommand(AddProfile);
+
+ public ICommand UpdateProfileCommand => new RelayCommand(UpdateProfile);
+
+ public ICommand AddVariableCommand => new RelayCommand(AddVariable);
+
+ public ICommand CancelAddVariableCommand => new RelayCommand(CancelAddVariable);
+
+ public ICommand AddDefaultVariableCommand => new RelayCommand(AddDefaultVariable);
+
+ public MainPage()
+ {
+ this.InitializeComponent();
+ ViewModel = App.GetService();
+ DataContext = ViewModel;
+ }
+
+ private async Task ShowEditDialogAsync(Variable variable, VariablesSet parentSet)
+ {
+ var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
+
+ EditVariableDialog.Title = resourceLoader.GetString("EditVariableDialog_Title");
+ EditVariableDialog.PrimaryButtonText = resourceLoader.GetString("SaveBtn");
+ EditVariableDialog.SecondaryButtonText = resourceLoader.GetString("CancelBtn");
+ EditVariableDialog.PrimaryButtonCommand = EditCommand;
+ EditVariableDialog.PrimaryButtonCommandParameter = new RelayCommandParameter(variable, parentSet);
+
+ var clone = variable.Clone();
+ EditVariableDialog.DataContext = clone;
+
+ await EditVariableDialog.ShowAsync();
+ }
+
+ private async void EditVariable_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ var btn = sender as MenuFlyoutItem;
+ var variablesSet = btn.DataContext as VariablesSet;
+ var variable = btn.CommandParameter as Variable;
+
+ if (variable != null)
+ {
+ await ShowEditDialogAsync(variable, variablesSet);
+ }
+ }
+
+ private void EditVariable(RelayCommandParameter param)
+ {
+ var variableSet = param.Set as ProfileVariablesSet;
+ var original = param.Variable;
+ var edited = EditVariableDialog.DataContext as Variable;
+ ViewModel.EditVariable(original, edited, variableSet);
+ }
+
+ private async Task AddProfileAsync()
+ {
+ SwitchViewsSegmentedView.SelectedIndex = 0;
+
+ var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
+ AddProfileDialog.Title = resourceLoader.GetString("AddNewProfileDialog_Title");
+ AddProfileDialog.PrimaryButtonText = resourceLoader.GetString("AddBtn");
+ AddProfileDialog.SecondaryButtonText = resourceLoader.GetString("CancelBtn");
+ AddProfileDialog.PrimaryButtonCommand = AddProfileCommand;
+ AddProfileDialog.DataContext = new ProfileVariablesSet(Guid.NewGuid(), string.Empty);
+
+ await AddProfileDialog.ShowAsync();
+ }
+
+ private void AddProfile()
+ {
+ var profile = AddProfileDialog.DataContext as ProfileVariablesSet;
+ ViewModel.AddProfile(profile);
+ }
+
+ private void UpdateProfile()
+ {
+ var updatedProfile = AddProfileDialog.DataContext as ProfileVariablesSet;
+ ViewModel.UpdateProfile(updatedProfile);
+ }
+
+ private async void RemoveProfileBtn_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ var button = sender as MenuFlyoutItem;
+ var profile = button.CommandParameter as ProfileVariablesSet;
+
+ if (profile != null)
+ {
+ var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
+ ContentDialog dialog = new ContentDialog();
+ dialog.XamlRoot = RootPage.XamlRoot;
+ dialog.Title = profile.Name;
+ dialog.PrimaryButtonText = resourceLoader.GetString("Yes");
+ dialog.CloseButtonText = resourceLoader.GetString("No");
+ dialog.DefaultButton = ContentDialogButton.Primary;
+ dialog.Content = new TextBlock() { Text = resourceLoader.GetString("Delete_Dialog_Description"), TextWrapping = Microsoft.UI.Xaml.TextWrapping.WrapWholeWords };
+ dialog.PrimaryButtonClick += (s, args) =>
+ {
+ ViewModel.RemoveProfile(profile);
+ };
+
+ var result = await dialog.ShowAsync();
+ }
+ }
+
+ private void AddVariable()
+ {
+ var profile = AddProfileDialog.DataContext as ProfileVariablesSet;
+ if (profile != null)
+ {
+ if (AddVariableSwitchPresenter.Value as string == "NewVariable")
+ {
+ profile.Variables.Add(new Variable(AddNewVariableName.Text, AddNewVariableValue.Text, VariablesSetType.Profile));
+ }
+ else
+ {
+ foreach (Variable variable in ExistingVariablesListView.SelectedItems)
+ {
+ if (!profile.Variables.Where(x => x.Name == variable.Name).Any())
+ {
+ var clone = variable.Clone(true);
+ profile.Variables.Add(clone);
+ }
+ }
+ }
+ }
+
+ AddNewVariableName.Text = string.Empty;
+ AddNewVariableValue.Text = string.Empty;
+ ExistingVariablesListView.SelectionChanged -= ExistingVariablesListView_SelectionChanged;
+ ExistingVariablesListView.SelectedItems.Clear();
+ ExistingVariablesListView.SelectionChanged += ExistingVariablesListView_SelectionChanged;
+ AddVariableFlyout.Hide();
+ }
+
+ private void CancelAddVariable()
+ {
+ AddNewVariableName.Text = string.Empty;
+ AddNewVariableValue.Text = string.Empty;
+
+ ExistingVariablesListView.SelectionChanged -= ExistingVariablesListView_SelectionChanged;
+ ExistingVariablesListView.SelectedItems.Clear();
+ ExistingVariablesListView.SelectionChanged += ExistingVariablesListView_SelectionChanged;
+
+ AddVariableFlyout.Hide();
+ }
+
+ private void AddDefaultVariable(DefaultVariablesSet set)
+ {
+ var variable = AddDefaultVariableDialog.DataContext as Variable;
+ var type = set.Type;
+
+ ViewModel.AddDefaultVariable(variable, type);
+ }
+
+ private async void Delete_Variable_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ MenuFlyoutItem selectedItem = sender as MenuFlyoutItem;
+ var variableSet = selectedItem.DataContext as ProfileVariablesSet;
+ var variable = selectedItem.CommandParameter as Variable;
+
+ if (variable != null)
+ {
+ var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
+ ContentDialog dialog = new ContentDialog();
+ dialog.XamlRoot = RootPage.XamlRoot;
+ dialog.Title = variable.Name;
+ dialog.PrimaryButtonText = resourceLoader.GetString("Yes");
+ dialog.CloseButtonText = resourceLoader.GetString("No");
+ dialog.DefaultButton = ContentDialogButton.Primary;
+ dialog.Content = new TextBlock() { Text = resourceLoader.GetString("Delete_Variable_Description"), TextWrapping = Microsoft.UI.Xaml.TextWrapping.WrapWholeWords };
+ dialog.PrimaryButtonClick += (s, args) =>
+ {
+ ViewModel.DeleteVariable(variable, variableSet);
+ };
+ var result = await dialog.ShowAsync();
+ }
+ }
+
+ private void AddNewVariableName_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ TextBox nameTxtBox = sender as TextBox;
+ var profile = AddProfileDialog.DataContext as ProfileVariablesSet;
+
+ if (nameTxtBox != null)
+ {
+ if (nameTxtBox.Text.Length == 0 || nameTxtBox.Text.Length >= 255 || profile.Variables.Where(x => x.Name.Equals(nameTxtBox.Text, StringComparison.OrdinalIgnoreCase)).Any())
+ {
+ ConfirmAddVariableBtn.IsEnabled = false;
+ }
+ else
+ {
+ ConfirmAddVariableBtn.IsEnabled = true;
+ }
+ }
+ }
+
+ private void ReloadButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ ViewModel.LoadEnvironmentVariables();
+ ViewModel.EnvironmentState = EnvironmentState.Unchanged;
+ }
+
+ private void ExistingVariablesListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ var profile = AddProfileDialog.DataContext as ProfileVariablesSet;
+
+ int toRemove = -1;
+
+ if (e.AddedItems.Count > 0)
+ {
+ var list = sender as ListView;
+ var duplicates = list.SelectedItems.GroupBy(x => ((Variable)x).Name.ToLowerInvariant()).Where(g => g.Count() > 1).ToList();
+
+ foreach (var dup in duplicates)
+ {
+ ExistingVariablesListView.SelectionChanged -= ExistingVariablesListView_SelectionChanged;
+ list.SelectedItems.Remove(dup.ElementAt(1));
+ ExistingVariablesListView.SelectionChanged += ExistingVariablesListView_SelectionChanged;
+ }
+ }
+
+ if (e.RemovedItems.Count > 0)
+ {
+ Variable removedVariable = e.RemovedItems[0] as Variable;
+ for (int i = 0; i < profile.Variables.Count; i++)
+ {
+ if (profile.Variables[i].Name == removedVariable.Name && profile.Variables[i].Values == removedVariable.Values)
+ {
+ toRemove = i;
+ break;
+ }
+ }
+
+ if (toRemove != -1)
+ {
+ profile.Variables.RemoveAt(toRemove);
+ }
+ }
+
+ ConfirmAddVariableBtn.IsEnabled = false;
+ foreach (Variable variable in ExistingVariablesListView.SelectedItems)
+ {
+ if (variable != null)
+ {
+ if (!profile.Variables.Where(x => x.Name.Equals(variable.Name, StringComparison.Ordinal) && x.Values.Equals(variable.Values, StringComparison.Ordinal)).Any())
+ {
+ ConfirmAddVariableBtn.IsEnabled = true;
+ break;
+ }
+ }
+ }
+ }
+
+ private async void EditProfileBtn_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ SwitchViewsSegmentedView.SelectedIndex = 0;
+
+ var button = sender as MenuFlyoutItem;
+ var profile = button.CommandParameter as ProfileVariablesSet;
+
+ if (profile != null)
+ {
+ var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
+ AddProfileDialog.Title = resourceLoader.GetString("EditProfileDialog_Title");
+ AddProfileDialog.PrimaryButtonText = resourceLoader.GetString("SaveBtn");
+ AddProfileDialog.SecondaryButtonText = resourceLoader.GetString("CancelBtn");
+ AddProfileDialog.PrimaryButtonCommand = UpdateProfileCommand;
+ AddProfileDialog.DataContext = profile.Clone();
+ await AddProfileDialog.ShowAsync();
+ }
+ }
+
+ private void ExistingVariablesListView_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ var profile = AddProfileDialog.DataContext as ProfileVariablesSet;
+
+ foreach (Variable item in ExistingVariablesListView.Items)
+ {
+ if (item != null)
+ {
+ foreach (var profileItem in profile.Variables)
+ {
+ if (profileItem.Name == item.Name && profileItem.Values == item.Values)
+ {
+ if (ExistingVariablesListView.SelectedItems.Where(x => ((Variable)x).Name.Equals(profileItem.Name, StringComparison.OrdinalIgnoreCase)).Any())
+ {
+ continue;
+ }
+
+ ExistingVariablesListView.SelectionChanged -= ExistingVariablesListView_SelectionChanged;
+ ExistingVariablesListView.SelectedItems.Add(item);
+ ExistingVariablesListView.SelectionChanged += ExistingVariablesListView_SelectionChanged;
+ }
+ }
+ }
+ }
+ }
+
+ private async Task ShowAddDefaultVariableDialogAsync(DefaultVariablesSet set)
+ {
+ var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
+
+ AddDefaultVariableDialog.Title = resourceLoader.GetString("AddVariable_Title");
+ AddDefaultVariableDialog.PrimaryButtonText = resourceLoader.GetString("SaveBtn");
+ AddDefaultVariableDialog.SecondaryButtonText = resourceLoader.GetString("CancelBtn");
+ AddDefaultVariableDialog.PrimaryButtonCommand = AddDefaultVariableCommand;
+ AddDefaultVariableDialog.PrimaryButtonCommandParameter = set;
+
+ var variableType = set.Id == VariablesSet.SystemGuid ? VariablesSetType.System : VariablesSetType.User;
+ AddDefaultVariableDialog.DataContext = new Variable(string.Empty, string.Empty, variableType);
+
+ await AddDefaultVariableDialog.ShowAsync();
+ }
+
+ private async void AddDefaultVariableBtn_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ var button = sender as Button;
+ var defaultVariableSet = button.CommandParameter as DefaultVariablesSet;
+
+ if (defaultVariableSet != null)
+ {
+ await ShowAddDefaultVariableDialogAsync(defaultVariableSet);
+ }
+ }
+
+ private void EditVariableDialogNameTxtBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ var variable = EditVariableDialog.DataContext as Variable;
+ var param = EditVariableDialog.PrimaryButtonCommandParameter as RelayCommandParameter;
+ var variableSet = param.Set;
+
+ if (variableSet == null)
+ {
+ // default set
+ variableSet = variable.ParentType == VariablesSetType.User ? ViewModel.UserDefaultSet : ViewModel.SystemDefaultSet;
+ }
+
+ if (variableSet != null)
+ {
+ if (variableSet.Variables.Where(x => x.Name.Equals(EditVariableDialogNameTxtBox.Text, StringComparison.OrdinalIgnoreCase)).Any() || !variable.Valid)
+ {
+ EditVariableDialog.IsPrimaryButtonEnabled = false;
+ }
+ else
+ {
+ EditVariableDialog.IsPrimaryButtonEnabled = true;
+ }
+ }
+
+ if (!variable.Validate())
+ {
+ EditVariableDialog.IsPrimaryButtonEnabled = false;
+ }
+ }
+
+ private void AddDefaultVariableNameTxtBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ TextBox nameTxtBox = sender as TextBox;
+ var variable = AddDefaultVariableDialog.DataContext as Variable;
+ var defaultSet = variable.ParentType == VariablesSetType.User ? ViewModel.UserDefaultSet : ViewModel.SystemDefaultSet;
+
+ if (nameTxtBox != null)
+ {
+ if (nameTxtBox.Text.Length == 0 || defaultSet.Variables.Where(x => x.Name.Equals(nameTxtBox.Text, StringComparison.OrdinalIgnoreCase)).Any())
+ {
+ AddDefaultVariableDialog.IsPrimaryButtonEnabled = false;
+ }
+ else
+ {
+ AddDefaultVariableDialog.IsPrimaryButtonEnabled = true;
+ }
+ }
+
+ if (!variable.Validate())
+ {
+ AddDefaultVariableDialog.IsPrimaryButtonEnabled = false;
+ }
+ }
+
+ private void EditVariableDialogValueTxtBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ var txtBox = sender as TextBox;
+ var variable = EditVariableDialog.DataContext as Variable;
+ EditVariableDialog.IsPrimaryButtonEnabled = true;
+
+ variable.ValuesList = Variable.ValuesStringToValuesListItemCollection(txtBox.Text);
+ }
+
+ private void ReorderButtonUp_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ var listItem = ((MenuFlyoutItem)sender).DataContext as Variable.ValuesListItem;
+ if (listItem == null)
+ {
+ return;
+ }
+
+ var variable = EditVariableDialog.DataContext as Variable;
+
+ var index = variable.ValuesList.IndexOf(listItem);
+ if (index > 0)
+ {
+ variable.ValuesList.Move(index, index - 1);
+ }
+
+ var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
+ EditVariableDialogValueTxtBox.Text = newValues;
+ }
+
+ private void ReorderButtonDown_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ var listItem = ((MenuFlyoutItem)sender).DataContext as Variable.ValuesListItem;
+ if (listItem == null)
+ {
+ return;
+ }
+
+ var variable = EditVariableDialog.DataContext as Variable;
+ var btn = EditVariableDialog.PrimaryButtonCommandParameter as Button;
+
+ var index = variable.ValuesList.IndexOf(listItem);
+ if (index < variable.ValuesList.Count - 1)
+ {
+ variable.ValuesList.Move(index, index + 1);
+ }
+
+ var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
+ EditVariableDialogValueTxtBox.Text = newValues;
+ }
+
+ private void RemoveListVariableButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ var listItem = ((MenuFlyoutItem)sender).DataContext as Variable.ValuesListItem;
+ if (listItem == null)
+ {
+ return;
+ }
+
+ var variable = EditVariableDialog.DataContext as Variable;
+ variable.ValuesList.Remove(listItem);
+
+ var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
+ EditVariableDialogValueTxtBox.Text = newValues;
+ }
+
+ private void InsertListEntryBeforeButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ var listItem = (sender as MenuFlyoutItem)?.DataContext as Variable.ValuesListItem;
+ if (listItem == null)
+ {
+ return;
+ }
+
+ var variable = EditVariableDialog.DataContext as Variable;
+ var index = variable.ValuesList.IndexOf(listItem);
+ variable.ValuesList.Insert(index, new Variable.ValuesListItem { Text = string.Empty });
+
+ var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
+ EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
+ EditVariableDialogValueTxtBox.Text = newValues;
+ EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
+ }
+
+ private void InsertListEntryAfterButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ var listItem = (sender as MenuFlyoutItem)?.DataContext as Variable.ValuesListItem;
+ if (listItem == null)
+ {
+ return;
+ }
+
+ var variable = EditVariableDialog.DataContext as Variable;
+ var index = variable.ValuesList.IndexOf(listItem);
+ variable.ValuesList.Insert(index + 1, new Variable.ValuesListItem { Text = string.Empty });
+
+ var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
+ EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
+ EditVariableDialogValueTxtBox.Text = newValues;
+ EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
+ }
+
+ private void EditVariableValuesListTextBox_LostFocus(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ var listItem = (sender as TextBox)?.DataContext as Variable.ValuesListItem;
+ if (listItem == null)
+ {
+ return;
+ }
+
+ if (listItem.Text == (sender as TextBox)?.Text)
+ {
+ return;
+ }
+
+ listItem.Text = (sender as TextBox)?.Text;
+ var variable = EditVariableDialog.DataContext as Variable;
+
+ var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
+ EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
+ EditVariableDialogValueTxtBox.Text = newValues;
+ EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
+ }
+
+ private void InvalidStateInfoBar_CloseButtonClick(InfoBar sender, object args)
+ {
+ ViewModel.EnvironmentState = EnvironmentState.Unchanged;
+ }
+
+ private void AddVariableFlyout_Closed(object sender, object e)
+ {
+ CancelAddVariable();
+ ConfirmAddVariableBtn.IsEnabled = false;
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/ElevationHelper.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/ElevationHelper.cs
new file mode 100644
index 0000000000..9b3bc16e2f
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/ElevationHelper.cs
@@ -0,0 +1,20 @@
+// 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.Security.Principal;
+
+namespace EnvironmentVariables.Helpers
+{
+ public class ElevationHelper : IElevationHelper
+ {
+ private readonly bool _isElevated;
+
+ public bool IsElevated => _isElevated;
+
+ public ElevationHelper()
+ {
+ _isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/EnvironmentVariablesHelper.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/EnvironmentVariablesHelper.cs
new file mode 100644
index 0000000000..4933050e82
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/EnvironmentVariablesHelper.cs
@@ -0,0 +1,189 @@
+// 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.Collections;
+using System.Collections.Generic;
+using EnvironmentVariables.Helpers.Win32;
+using EnvironmentVariables.Models;
+using ManagedCommon;
+using Microsoft.Win32;
+
+namespace EnvironmentVariables.Helpers
+{
+ internal sealed class EnvironmentVariablesHelper
+ {
+ internal static string GetBackupVariableName(Variable variable, string profileName)
+ {
+ return variable.Name + "_PowerToys_" + profileName;
+ }
+
+ internal static Variable GetExisting(string variableName)
+ {
+ var userVariables = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User);
+
+ foreach (DictionaryEntry variable in userVariables)
+ {
+ var key = variable.Key as string;
+ if (key.Equals(variableName, StringComparison.OrdinalIgnoreCase))
+ {
+ return new Variable(key, userVariables[key] as string, VariablesSetType.User);
+ }
+ }
+
+ var systemVariables = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Machine);
+
+ foreach (DictionaryEntry variable in systemVariables)
+ {
+ var key = variable.Key as string;
+ if (key.Equals(variableName, StringComparison.OrdinalIgnoreCase))
+ {
+ return new Variable(key, systemVariables[key] as string, VariablesSetType.System);
+ }
+ }
+
+ return null;
+ }
+
+ private static RegistryKey OpenEnvironmentKeyIfExists(bool fromMachine, bool writable)
+ {
+ RegistryKey baseKey;
+ string keyName;
+
+ if (fromMachine)
+ {
+ baseKey = Registry.LocalMachine;
+ keyName = @"System\CurrentControlSet\Control\Session Manager\Environment";
+ }
+ else
+ {
+ baseKey = Registry.CurrentUser;
+ keyName = "Environment";
+ }
+
+ return baseKey.OpenSubKey(keyName, writable: writable);
+ }
+
+ private static void SetEnvironmentVariableFromRegistryWithoutNotify(string variable, string value, bool fromMachine)
+ {
+ const int MaxUserEnvVariableLength = 255; // User-wide env vars stored in the registry have names limited to 255 chars
+ if (!fromMachine && variable.Length >= MaxUserEnvVariableLength)
+ {
+ Logger.LogError("Can't apply variable - name too long.");
+ return;
+ }
+
+ using (RegistryKey environmentKey = OpenEnvironmentKeyIfExists(fromMachine, writable: true))
+ {
+ if (environmentKey != null)
+ {
+ if (value == null)
+ {
+ environmentKey.DeleteValue(variable, throwOnMissingValue: false);
+ }
+ else
+ {
+ environmentKey.SetValue(variable, value);
+ }
+ }
+ }
+ }
+
+ internal static void NotifyEnvironmentChange()
+ {
+ unsafe
+ {
+ // send a WM_SETTINGCHANGE message to all windows
+ fixed (char* lParam = "Environment")
+ {
+ _ = NativeMethods.SendNotifyMessage(new IntPtr(NativeMethods.HWND_BROADCAST), NativeMethods.WindowMessage.WM_SETTINGSCHANGED, (IntPtr)0x12345, (IntPtr)lParam);
+ }
+ }
+ }
+
+ internal static void GetVariables(EnvironmentVariableTarget target, VariablesSet set)
+ {
+ var variables = Environment.GetEnvironmentVariables(target);
+ var sortedList = new SortedList();
+
+ foreach (DictionaryEntry variable in variables)
+ {
+ string key = variable.Key as string;
+ string value = variable.Value as string;
+
+ if (string.IsNullOrEmpty(key))
+ {
+ continue;
+ }
+
+ Variable entry = new Variable(key, value, set.Type);
+ sortedList.Add(key, entry);
+ }
+
+ set.Variables = new System.Collections.ObjectModel.ObservableCollection(sortedList.Values);
+ }
+
+ internal static bool SetVariableWithoutNotify(Variable variable)
+ {
+ bool fromMachine = variable.ParentType switch
+ {
+ VariablesSetType.Profile => false,
+ VariablesSetType.User => false,
+ VariablesSetType.System => true,
+ _ => throw new NotImplementedException(),
+ };
+
+ SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, variable.Values, fromMachine);
+
+ return true;
+ }
+
+ internal static bool SetVariable(Variable variable)
+ {
+ bool fromMachine = variable.ParentType switch
+ {
+ VariablesSetType.Profile => false,
+ VariablesSetType.User => false,
+ VariablesSetType.System => true,
+ _ => throw new NotImplementedException(),
+ };
+
+ SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, variable.Values, fromMachine);
+ NotifyEnvironmentChange();
+
+ return true;
+ }
+
+ internal static bool UnsetVariableWithoutNotify(Variable variable)
+ {
+ bool fromMachine = variable.ParentType switch
+ {
+ VariablesSetType.Profile => false,
+ VariablesSetType.User => false,
+ VariablesSetType.System => true,
+ _ => throw new NotImplementedException(),
+ };
+
+ SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, null, fromMachine);
+
+ return true;
+ }
+
+ internal static bool UnsetVariable(Variable variable)
+ {
+ bool fromMachine = variable.ParentType switch
+ {
+ VariablesSetType.Profile => false,
+ VariablesSetType.User => false,
+ VariablesSetType.System => true,
+ _ => throw new NotImplementedException(),
+ };
+
+ SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, null, fromMachine);
+ NotifyEnvironmentChange();
+
+ return true;
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/EnvironmentVariablesService.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/EnvironmentVariablesService.cs
new file mode 100644
index 0000000000..6fde9872fc
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/EnvironmentVariablesService.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.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Text.Json;
+using System.Threading.Tasks;
+using EnvironmentVariables.Models;
+
+namespace EnvironmentVariables.Helpers
+{
+ internal sealed class EnvironmentVariablesService : IEnvironmentVariablesService
+ {
+ private const string ProfilesJsonFileSubPath = "Microsoft\\PowerToys\\EnvironmentVariables\\";
+
+ private readonly string _profilesJsonFilePath;
+
+ private readonly IFileSystem _fileSystem;
+
+ private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ };
+
+ public string ProfilesJsonFilePath => _profilesJsonFilePath;
+
+ public EnvironmentVariablesService(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+
+ _profilesJsonFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ProfilesJsonFileSubPath, "profiles.json");
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public List ReadProfiles()
+ {
+ if (!_fileSystem.File.Exists(ProfilesJsonFilePath))
+ {
+ return new List();
+ }
+
+ var fileContent = _fileSystem.File.ReadAllText(ProfilesJsonFilePath);
+ var profiles = JsonSerializer.Deserialize>(fileContent);
+
+ return profiles;
+ }
+
+ public async Task WriteAsync(IEnumerable profiles)
+ {
+ string jsonData = JsonSerializer.Serialize(profiles, _serializerOptions);
+ await _fileSystem.File.WriteAllTextAsync(ProfilesJsonFilePath, jsonData);
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/IElevationHelper.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/IElevationHelper.cs
new file mode 100644
index 0000000000..0b27b0a544
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/IElevationHelper.cs
@@ -0,0 +1,11 @@
+// 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 EnvironmentVariables.Helpers
+{
+ public interface IElevationHelper
+ {
+ bool IsElevated { get; }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/IEnvironmentVariablesService.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/IEnvironmentVariablesService.cs
new file mode 100644
index 0000000000..5a1604ab1b
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/IEnvironmentVariablesService.cs
@@ -0,0 +1,20 @@
+// 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.Collections.Generic;
+using System.Threading.Tasks;
+using EnvironmentVariables.Models;
+
+namespace EnvironmentVariables.Helpers
+{
+ public interface IEnvironmentVariablesService : IDisposable
+ {
+ string ProfilesJsonFilePath { get; }
+
+ List ReadProfiles();
+
+ Task WriteAsync(IEnumerable profiles);
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/NativeMethods.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/NativeMethods.cs
new file mode 100644
index 0000000000..1ca3e2428c
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/NativeMethods.cs
@@ -0,0 +1,54 @@
+// 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.Runtime.InteropServices;
+
+namespace EnvironmentVariables.Helpers.Win32
+{
+ public static class NativeMethods
+ {
+ internal const int HWND_BROADCAST = 0xffff;
+
+ internal delegate IntPtr WinProc(IntPtr hWnd, WindowMessage msg, IntPtr wParam, IntPtr lParam);
+
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
+ internal static extern int SendNotifyMessage(IntPtr hWnd, WindowMessage msg, IntPtr wParam, IntPtr lParam);
+
+ [DllImport("User32.dll")]
+ internal static extern int GetDpiForWindow(IntPtr hwnd);
+
+ [DllImport("user32.dll", EntryPoint = "SetWindowLong")]
+ internal static extern int SetWindowLong32(IntPtr hWnd, WindowLongIndexFlags nIndex, WinProc newProc);
+
+ [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
+ internal static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, WindowLongIndexFlags nIndex, WinProc newProc);
+
+ [DllImport("user32.dll")]
+ internal static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, WindowMessage msg, IntPtr wParam, IntPtr lParam);
+
+ [Flags]
+ internal enum WindowLongIndexFlags : int
+ {
+ GWL_WNDPROC = -4,
+ }
+
+ internal enum WindowMessage : int
+ {
+ WM_SETTINGSCHANGED = 0x001A,
+ }
+
+ internal static IntPtr SetWindowLongPtr(IntPtr hWnd, WindowLongIndexFlags nIndex, WinProc newProc)
+ {
+ if (IntPtr.Size == 8)
+ {
+ return SetWindowLongPtr64(hWnd, nIndex, newProc);
+ }
+ else
+ {
+ return new IntPtr(SetWindowLong32(hWnd, nIndex, newProc));
+ }
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/ResourceLoaderInstance.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/ResourceLoaderInstance.cs
new file mode 100644
index 0000000000..b0a11730b9
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/ResourceLoaderInstance.cs
@@ -0,0 +1,17 @@
+// 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 Microsoft.Windows.ApplicationModel.Resources;
+
+namespace EnvironmentVariables.Helpers
+{
+ internal static class ResourceLoaderInstance
+ {
+ internal static ResourceLoader ResourceLoader { get; private set; }
+
+ static ResourceLoaderInstance()
+ {
+ ResourceLoader = new ResourceLoader("PowerToys.EnvironmentVariables.pri");
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Models/DefaultVariablesSet.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/DefaultVariablesSet.cs
new file mode 100644
index 0000000000..9c07c1df3b
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/DefaultVariablesSet.cs
@@ -0,0 +1,16 @@
+// 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;
+
+namespace EnvironmentVariables.Models
+{
+ public class DefaultVariablesSet : VariablesSet
+ {
+ public DefaultVariablesSet(Guid id, string name, VariablesSetType type)
+ : base(id, name, type)
+ {
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Models/EnvironmentState.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/EnvironmentState.cs
new file mode 100644
index 0000000000..12837055cd
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/EnvironmentState.cs
@@ -0,0 +1,14 @@
+// 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 EnvironmentVariables.Models
+{
+ public enum EnvironmentState
+ {
+ Unchanged = 0,
+ ChangedOnStartup,
+ EnvironmentMessageReceived,
+ ProfileNotApplicable,
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Models/ProfileVariablesSet.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/ProfileVariablesSet.cs
new file mode 100644
index 0000000000..db9306efa7
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/ProfileVariablesSet.cs
@@ -0,0 +1,164 @@
+// 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.Collections.ObjectModel;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using EnvironmentVariables.Helpers;
+using ManagedCommon;
+
+namespace EnvironmentVariables.Models
+{
+ public partial class ProfileVariablesSet : VariablesSet
+ {
+ [ObservableProperty]
+ private bool _isEnabled;
+
+ public ProfileVariablesSet()
+ : base()
+ {
+ Type = VariablesSetType.Profile;
+ IconPath = ProfileIconPath;
+ }
+
+ public ProfileVariablesSet(Guid id, string name)
+ : base(id, name, VariablesSetType.Profile)
+ {
+ IconPath = ProfileIconPath;
+ }
+
+ public Task Apply()
+ {
+ return Task.Run(() =>
+ {
+ foreach (var variable in Variables)
+ {
+ var applyToSystem = variable.ApplyToSystem;
+
+ // Get existing variable with the same name if it exist
+ var variableToOverride = EnvironmentVariablesHelper.GetExisting(variable.Name);
+
+ // It exists. Rename it to preserve it.
+ if (variableToOverride != null && variableToOverride.ParentType == VariablesSetType.User)
+ {
+ variableToOverride.Name = EnvironmentVariablesHelper.GetBackupVariableName(variableToOverride, this.Name);
+
+ // Backup the variable
+ if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToOverride))
+ {
+ Logger.LogError("Failed to set backup variable.");
+ }
+ }
+
+ if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variable))
+ {
+ Logger.LogError("Failed to set profile variable.");
+ }
+ }
+
+ EnvironmentVariablesHelper.NotifyEnvironmentChange();
+ });
+ }
+
+ public Task UnApply()
+ {
+ return Task.Run(() =>
+ {
+ foreach (var variable in Variables)
+ {
+ UnapplyVariable(variable);
+ }
+
+ EnvironmentVariablesHelper.NotifyEnvironmentChange();
+ });
+ }
+
+ public void UnapplyVariable(Variable variable)
+ {
+ // Unset the variable
+ if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(variable))
+ {
+ Logger.LogError("Failed to unset variable.");
+ }
+
+ var originalName = variable.Name;
+ var backupName = EnvironmentVariablesHelper.GetBackupVariableName(variable, this.Name);
+
+ // Get backup variable if it exist
+ var backupVariable = EnvironmentVariablesHelper.GetExisting(backupName);
+
+ if (backupVariable != null)
+ {
+ var variableToRestore = new Variable(originalName, backupVariable.Values, backupVariable.ParentType);
+
+ if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(backupVariable))
+ {
+ Logger.LogError("Failed to unset backup variable.");
+ }
+
+ if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToRestore))
+ {
+ Logger.LogError("Failed to restore backup variable.");
+ }
+ }
+ }
+
+ public bool IsCorrectlyApplied()
+ {
+ if (!IsEnabled)
+ {
+ return false;
+ }
+
+ foreach (var variable in Variables)
+ {
+ var applied = EnvironmentVariablesHelper.GetExisting(variable.Name);
+ if (applied != null && applied.Values == variable.Values && applied.ParentType == VariablesSetType.User)
+ {
+ continue;
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public bool IsApplicable()
+ {
+ foreach (var variable in Variables)
+ {
+ if (!variable.Validate())
+ {
+ return false;
+ }
+
+ // Get existing variable with the same name if it exist
+ var variableToOverride = EnvironmentVariablesHelper.GetExisting(variable.Name);
+
+ // It exists. Backup is needed.
+ if (variableToOverride != null && variableToOverride.ParentType == VariablesSetType.User)
+ {
+ variableToOverride.Name = EnvironmentVariablesHelper.GetBackupVariableName(variableToOverride, this.Name);
+ if (!variableToOverride.Validate())
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public ProfileVariablesSet Clone()
+ {
+ var clone = new ProfileVariablesSet(this.Id, this.Name);
+ clone.Variables = new ObservableCollection(this.Variables);
+ clone.IsEnabled = this.IsEnabled;
+
+ return clone;
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Models/Variable.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/Variable.cs
new file mode 100644
index 0000000000..bb93189124
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/Variable.cs
@@ -0,0 +1,202 @@
+// 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.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using EnvironmentVariables.Helpers;
+using ManagedCommon;
+
+namespace EnvironmentVariables.Models
+{
+ public partial class Variable : ObservableObject, IJsonOnDeserialized
+ {
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(Valid))]
+ [NotifyPropertyChangedFor(nameof(ShowAsList))]
+ private string _name;
+
+ [ObservableProperty]
+ private string _values;
+
+ [ObservableProperty]
+ private bool _applyToSystem;
+
+ [JsonIgnore]
+ public bool IsEditable
+ {
+ get
+ {
+ return ParentType != VariablesSetType.System || App.GetService().IsElevated;
+ }
+ }
+
+ [JsonIgnore]
+ public VariablesSetType ParentType { get; set; }
+
+ // To store the strings in the Values List with actual objects that can be referenced and identity compared
+ public class ValuesListItem
+ {
+ public string Text { get; set; }
+ }
+
+ [ObservableProperty]
+ [property: JsonIgnore]
+ [JsonIgnore]
+ private ObservableCollection _valuesList;
+
+ [JsonIgnore]
+ public bool Valid => Validate();
+
+ [JsonIgnore]
+ public bool ShowAsList => IsList();
+
+ private bool IsList()
+ {
+ List listVariables = new() { "PATH", "PATHEXT", "PSMODULEPATH" };
+
+ foreach (var name in listVariables)
+ {
+ if (Name.Equals(name, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void OnDeserialized()
+ {
+ // No need to save ValuesList to the Json, so we are generating it after deserializing
+ ValuesList = ValuesStringToValuesListItemCollection(Values);
+ }
+
+ public Variable()
+ {
+ }
+
+ public Variable(string name, string values, VariablesSetType parentType)
+ {
+ Name = name;
+ Values = values;
+ ParentType = parentType;
+
+ ValuesList = ValuesStringToValuesListItemCollection(Values);
+ }
+
+ internal static ObservableCollection ValuesStringToValuesListItemCollection(string values)
+ {
+ return new ObservableCollection(values.Split(';').Select(x => new ValuesListItem { Text = x }));
+ }
+
+ internal Task Update(Variable edited, bool propagateChange, ProfileVariablesSet parentProfile)
+ {
+ bool nameChanged = Name != edited.Name;
+
+ var clone = this.Clone();
+
+ // Update state
+ Name = edited.Name;
+ Values = edited.Values;
+
+ ValuesList = ValuesStringToValuesListItemCollection(Values);
+
+ return Task.Run(() =>
+ {
+ // Apply changes
+ if (propagateChange)
+ {
+ if (nameChanged)
+ {
+ if (!EnvironmentVariablesHelper.UnsetVariable(clone))
+ {
+ Logger.LogError("Failed to unset original variable.");
+ }
+
+ if (parentProfile != null)
+ {
+ var backupName = EnvironmentVariablesHelper.GetBackupVariableName(clone, parentProfile.Name);
+
+ // Get backup variable if it exist
+ var backupVariable = EnvironmentVariablesHelper.GetExisting(backupName);
+ if (backupVariable != null)
+ {
+ var variableToRestore = new Variable(clone.Name, backupVariable.Values, backupVariable.ParentType);
+
+ if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(backupVariable))
+ {
+ Logger.LogError("Failed to unset backup variable.");
+ }
+
+ if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToRestore))
+ {
+ Logger.LogError("Failed to restore backup variable.");
+ }
+ }
+ }
+ }
+
+ // Get existing variable with the same name if it exist
+ var variableToOverride = EnvironmentVariablesHelper.GetExisting(Name);
+
+ // It exists. Rename it to preserve it.
+ if (variableToOverride != null && variableToOverride.ParentType == VariablesSetType.User && parentProfile != null)
+ {
+ // Gets which name the backup variable should have.
+ variableToOverride.Name = EnvironmentVariablesHelper.GetBackupVariableName(variableToOverride, parentProfile.Name);
+
+ // Only create a backup variable if there's not one already, to avoid overriding. (solves Path nuking errors, for example, after editing path on an enabled profile)
+ if (EnvironmentVariablesHelper.GetExisting(variableToOverride.Name) == null)
+ {
+ // Backup the variable
+ if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToOverride))
+ {
+ Logger.LogError("Failed to set backup variable.");
+ }
+ }
+ }
+
+ if (!EnvironmentVariablesHelper.SetVariable(this))
+ {
+ Logger.LogError("Failed to set new variable.");
+ }
+ }
+ });
+ }
+
+ internal Variable Clone(bool profile = false)
+ {
+ return new Variable
+ {
+ Name = Name,
+ Values = Values,
+ ParentType = profile ? VariablesSetType.Profile : ParentType,
+ ValuesList = ValuesStringToValuesListItemCollection(Values),
+ };
+ }
+
+ public bool Validate()
+ {
+ if (string.IsNullOrWhiteSpace(Name))
+ {
+ return false;
+ }
+
+ const int MaxUserEnvVariableLength = 255; // User-wide env vars stored in the registry have names limited to 255 chars
+ if (ParentType != VariablesSetType.System && Name.Length >= MaxUserEnvVariableLength)
+ {
+ Logger.LogError("Variable name too long.");
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Models/VariablesSet.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/VariablesSet.cs
new file mode 100644
index 0000000000..a1e352d4e9
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/VariablesSet.cs
@@ -0,0 +1,70 @@
+// 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.Collections.ObjectModel;
+using System.Linq;
+using System.Text.Json.Serialization;
+using CommunityToolkit.Mvvm.ComponentModel;
+using EnvironmentVariables.ViewModels;
+
+namespace EnvironmentVariables.Models
+{
+ public partial class VariablesSet : ObservableObject
+ {
+ public static readonly Guid UserGuid = new Guid("92F7AA9A-AE31-49CD-83C8-80A71E432AA5");
+ public static readonly Guid SystemGuid = new Guid("F679C74D-DB00-4795-92E1-B1F6A4833279");
+
+ private static readonly string UserIconPath = "/Assets/EnvironmentVariables/UserIcon.png";
+ private static readonly string SystemIconPath = "/Assets/EnvironmentVariables/SystemIcon.png";
+ protected static readonly string ProfileIconPath = "/Assets/EnvironmentVariables/ProfileIcon.png";
+
+ public Guid Id { get; set; }
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(Valid))]
+ private string _name;
+
+ [JsonIgnore]
+ public VariablesSetType Type { get; set; }
+
+ [JsonIgnore]
+ public string IconPath { get; set; }
+
+ [ObservableProperty]
+ private ObservableCollection _variables;
+
+ public bool Valid => Validate();
+
+ public VariablesSet()
+ {
+ }
+
+ public VariablesSet(Guid id, string name, VariablesSetType type)
+ {
+ Id = id;
+ Name = name;
+ Type = type;
+ Variables = new ObservableCollection();
+
+ IconPath = Type switch
+ {
+ VariablesSetType.User => UserIconPath,
+ VariablesSetType.System => SystemIconPath,
+ VariablesSetType.Profile => ProfileIconPath,
+ _ => throw new NotImplementedException(),
+ };
+ }
+
+ private bool Validate()
+ {
+ if (string.IsNullOrWhiteSpace(Name))
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Models/VariablesSetType.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/VariablesSetType.cs
new file mode 100644
index 0000000000..6b59b05b68
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/VariablesSetType.cs
@@ -0,0 +1,15 @@
+// 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 EnvironmentVariables.Models
+{
+ public enum VariablesSetType
+ {
+ Path = 0,
+ Duplicate,
+ Profile,
+ User,
+ System,
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Program.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Program.cs
new file mode 100644
index 0000000000..cc8349fe7c
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Program.cs
@@ -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.
+
+using System;
+using System.Threading;
+using ManagedCommon;
+using Microsoft.UI.Dispatching;
+using Microsoft.Windows.AppLifecycle;
+
+namespace EnvironmentVariables
+{
+ public static class Program
+ {
+ [STAThread]
+ public static void Main(string[] args)
+ {
+ Logger.InitializeLogger("\\EnvironmentVariables\\Logs");
+
+ WinRT.ComWrappersSupport.InitializeComWrappers();
+
+ if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredEnvironmentVariablesEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
+ {
+ Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
+ return;
+ }
+
+ var instanceKey = AppInstance.FindOrRegisterForKey("PowerToys_EnvironmentVariables_Instance");
+
+ if (instanceKey.IsCurrent)
+ {
+ Microsoft.UI.Xaml.Application.Start((p) =>
+ {
+ var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
+ SynchronizationContext.SetSynchronizationContext(context);
+ _ = new App();
+ });
+ }
+ else
+ {
+ Logger.LogWarning("Another instance of Environment Variables is running. Exiting.");
+ }
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Strings/en-us/Resources.resw b/src/modules/EnvironmentVariables/EnvironmentVariables/Strings/en-us/Resources.resw
new file mode 100644
index 0000000000..f7310f306c
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Strings/en-us/Resources.resw
@@ -0,0 +1,281 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Default variables
+
+
+ Default variables
+
+
+ New profile
+
+
+ You can create profiles to quickly apply a set of preconfigured variables
+
+
+ Profiles
+
+
+ System
+
+
+ User
+
+
+ Environment Variables
+ Title of the window when running as user
+
+
+ Cancel
+
+
+ Edit variable
+
+
+ Name
+
+
+ Save
+
+
+ Value
+
+
+ Save
+
+
+ New profile
+
+
+ Enabled
+
+
+ Add variable
+
+
+ Variable name
+
+
+ Variable value
+
+
+ Existing
+
+
+ New
+
+
+ Administrator: Environment Variables
+ Title of the window when running as administrator
+
+
+ Cancel
+
+
+ Add
+
+
+ List of applied variables
+
+
+ Applied variables
+
+
+ Variables
+
+
+ Delete
+
+
+ Are you sure you want to delete this profile? Deleting applied profile will remove all profile variables.
+
+
+ Administrator permissions are required to edit System variables
+
+
+ No
+
+
+ Yes
+
+
+ Changes were made outside of this app.
+
+
+ Variables included in applied profile have been modified. Review the latest changes before applying the profile again.
+
+
+ Cancel
+
+
+ Variables have been modified. Reload to get the latest changes.
+
+
+ Add variable
+
+
+ Are you sure you want to delete this variable?
+
+
+ Edit profile
+
+
+ Add variable
+
+
+ Add, remove or edit USER and SYSTEM variables
+
+
+ Edit
+
+
+ More options
+
+
+ Move down
+
+
+ Move up
+
+
+ Insert Before
+
+
+ Insert After
+
+
+ Apply to SYSTEM?
+
+
+ Delete
+
+
+ Remove
+
+
+ Add variable
+
+
+ Profile can not be applied.
+
+
+ Variables or backup variables are invalid.
+
+
\ No newline at end of file
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesOpenedEvent.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesOpenedEvent.cs
new file mode 100644
index 0000000000..113d88faed
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesOpenedEvent.cs
@@ -0,0 +1,16 @@
+// 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.Diagnostics.Tracing;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace EnvironmentVariables.Telemetry
+{
+ [EventData]
+ public class EnvironmentVariablesOpenedEvent : EventBase, IEvent
+ {
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesProfileEnabledEvent.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesProfileEnabledEvent.cs
new file mode 100644
index 0000000000..2d32b8e8c5
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesProfileEnabledEvent.cs
@@ -0,0 +1,18 @@
+// 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.Diagnostics.Tracing;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace EnvironmentVariables.Telemetry
+{
+ [EventData]
+ public class EnvironmentVariablesProfileEnabledEvent : EventBase, IEvent
+ {
+ public bool Enabled { get; set; }
+
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesVariableChangedEvent.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesVariableChangedEvent.cs
new file mode 100644
index 0000000000..1583adb9fa
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesVariableChangedEvent.cs
@@ -0,0 +1,24 @@
+// 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.Diagnostics.Tracing;
+using EnvironmentVariables.Models;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace EnvironmentVariables.Telemetry
+{
+ [EventData]
+ public class EnvironmentVariablesVariableChangedEvent : EventBase, IEvent
+ {
+ public VariablesSetType VariablesType { get; set; }
+
+ public EnvironmentVariablesVariableChangedEvent(VariablesSetType type)
+ {
+ this.VariablesType = type;
+ }
+
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/ViewModels/MainViewModel.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000000..164325b4bb
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/ViewModels/MainViewModel.cs
@@ -0,0 +1,417 @@
+// 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.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using EnvironmentVariables.Helpers;
+using EnvironmentVariables.Models;
+using ManagedCommon;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.UI.Dispatching;
+
+namespace EnvironmentVariables.ViewModels
+{
+ public partial class MainViewModel : ObservableObject
+ {
+ private readonly IEnvironmentVariablesService _environmentVariablesService;
+
+ private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
+
+ public DefaultVariablesSet UserDefaultSet { get; private set; } = new DefaultVariablesSet(VariablesSet.UserGuid, ResourceLoaderInstance.ResourceLoader.GetString("User"), VariablesSetType.User);
+
+ public DefaultVariablesSet SystemDefaultSet { get; private set; } = new DefaultVariablesSet(VariablesSet.SystemGuid, ResourceLoaderInstance.ResourceLoader.GetString("System"), VariablesSetType.System);
+
+ public VariablesSet DefaultVariables { get; private set; } = new DefaultVariablesSet(Guid.NewGuid(), "DefaultVariables", VariablesSetType.User);
+
+ [ObservableProperty]
+ private ObservableCollection _profiles;
+
+ [ObservableProperty]
+ private ObservableCollection _appliedVariables = new ObservableCollection();
+
+ [ObservableProperty]
+ private bool _isElevated;
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(IsInfoBarButtonVisible))]
+ private EnvironmentState _environmentState;
+
+ public bool IsInfoBarButtonVisible => EnvironmentState == EnvironmentState.EnvironmentMessageReceived;
+
+ public ProfileVariablesSet AppliedProfile { get; set; }
+
+ public MainViewModel(IEnvironmentVariablesService environmentVariablesService)
+ {
+ _environmentVariablesService = environmentVariablesService;
+ var isElevated = App.GetService().IsElevated;
+ IsElevated = isElevated;
+ }
+
+ private void LoadDefaultVariables()
+ {
+ UserDefaultSet.Variables.Clear();
+ SystemDefaultSet.Variables.Clear();
+ DefaultVariables.Variables.Clear();
+
+ EnvironmentVariablesHelper.GetVariables(EnvironmentVariableTarget.Machine, SystemDefaultSet);
+ EnvironmentVariablesHelper.GetVariables(EnvironmentVariableTarget.User, UserDefaultSet);
+
+ foreach (var variable in UserDefaultSet.Variables)
+ {
+ DefaultVariables.Variables.Add(variable);
+ }
+
+ foreach (var variable in SystemDefaultSet.Variables)
+ {
+ DefaultVariables.Variables.Add(variable);
+ }
+ }
+
+ [RelayCommand]
+ public void LoadEnvironmentVariables()
+ {
+ LoadDefaultVariables();
+ LoadProfiles();
+ PopulateAppliedVariables();
+ }
+
+ private void LoadProfiles()
+ {
+ try
+ {
+ var profiles = _environmentVariablesService.ReadProfiles();
+ foreach (var profile in profiles)
+ {
+ profile.PropertyChanged += Profile_PropertyChanged;
+
+ foreach (var variable in profile.Variables)
+ {
+ variable.ParentType = VariablesSetType.Profile;
+ }
+ }
+
+ var appliedProfiles = profiles.Where(x => x.IsEnabled).ToList();
+ if (appliedProfiles.Count > 0)
+ {
+ var appliedProfile = appliedProfiles.First();
+ if (appliedProfile.IsCorrectlyApplied())
+ {
+ AppliedProfile = appliedProfile;
+ EnvironmentState = EnvironmentState.Unchanged;
+ }
+ else
+ {
+ EnvironmentState = EnvironmentState.ChangedOnStartup;
+ appliedProfile.IsEnabled = false;
+ }
+ }
+
+ Profiles = new ObservableCollection(profiles);
+ }
+ catch (Exception ex)
+ {
+ // Show some error
+ Logger.LogError("Failed to load profiles.json file", ex);
+
+ Profiles = new ObservableCollection();
+ }
+ }
+
+ private void PopulateAppliedVariables()
+ {
+ LoadDefaultVariables();
+
+ var variables = new List();
+ if (AppliedProfile != null)
+ {
+ variables = variables.Concat(AppliedProfile.Variables.OrderBy(x => x.Name)).ToList();
+ }
+
+ variables = variables.Concat(UserDefaultSet.Variables.OrderBy(x => x.Name)).Concat(SystemDefaultSet.Variables.OrderBy(x => x.Name)).ToList();
+
+ // Handle PATH variable - add USER value to the end of the SYSTEM value
+ var profilePath = variables.Where(x => x.Name.Equals("PATH", StringComparison.OrdinalIgnoreCase) && x.ParentType == VariablesSetType.Profile).FirstOrDefault();
+ var userPath = variables.Where(x => x.Name.Equals("PATH", StringComparison.OrdinalIgnoreCase) && x.ParentType == VariablesSetType.User).FirstOrDefault();
+ var systemPath = variables.Where(x => x.Name.Equals("PATH", StringComparison.OrdinalIgnoreCase) && x.ParentType == VariablesSetType.System).FirstOrDefault();
+
+ if (systemPath != null)
+ {
+ var clone = systemPath.Clone();
+ clone.ParentType = VariablesSetType.Path;
+
+ if (userPath != null)
+ {
+ clone.Values += ";" + userPath.Values;
+ variables.Remove(userPath);
+ }
+
+ if (profilePath != null)
+ {
+ variables.Remove(profilePath);
+ }
+
+ variables.Insert(variables.IndexOf(systemPath), clone);
+ variables.Remove(systemPath);
+ }
+
+ variables = variables.GroupBy(x => x.Name).Select(y => y.First()).ToList();
+
+ // Find duplicates
+ var duplicates = variables.Where(x => !x.Name.Equals("PATH", StringComparison.OrdinalIgnoreCase)).GroupBy(x => x.Name.ToLower(CultureInfo.InvariantCulture)).Where(g => g.Count() > 1);
+ foreach (var duplicate in duplicates)
+ {
+ var userVar = duplicate.ElementAt(0);
+ var systemVar = duplicate.ElementAt(1);
+
+ var clone = userVar.Clone();
+ clone.ParentType = VariablesSetType.Duplicate;
+ clone.Name = systemVar.Name;
+ variables.Insert(variables.IndexOf(userVar), clone);
+ variables.Remove(userVar);
+ variables.Remove(systemVar);
+ }
+
+ variables = variables.OrderBy(x => x.ParentType).ToList();
+ AppliedVariables = new ObservableCollection(variables);
+ }
+
+ internal void AddDefaultVariable(Variable variable, VariablesSetType type)
+ {
+ if (type == VariablesSetType.User)
+ {
+ UserDefaultSet.Variables.Add(variable);
+ UserDefaultSet.Variables = new ObservableCollection(UserDefaultSet.Variables.OrderBy(x => x.Name).ToList());
+ }
+ else if (type == VariablesSetType.System)
+ {
+ SystemDefaultSet.Variables.Add(variable);
+ SystemDefaultSet.Variables = new ObservableCollection(SystemDefaultSet.Variables.OrderBy(x => x.Name).ToList());
+ }
+
+ EnvironmentVariablesHelper.SetVariable(variable);
+ PopulateAppliedVariables();
+ }
+
+ internal void EditVariable(Variable original, Variable edited, ProfileVariablesSet variablesSet)
+ {
+ bool propagateChange = variablesSet == null /* not a profile */ || variablesSet.Id.Equals(AppliedProfile?.Id);
+ bool changed = original.Name != edited.Name || original.Values != edited.Values;
+ if (changed)
+ {
+ var task = original.Update(edited, propagateChange, variablesSet);
+ task.ContinueWith(x =>
+ {
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ PopulateAppliedVariables();
+ });
+ });
+
+ PowerToysTelemetry.Log.WriteEvent(new Telemetry.EnvironmentVariablesVariableChangedEvent(original.ParentType));
+
+ _ = Task.Run(SaveAsync);
+ }
+ }
+
+ internal void AddProfile(ProfileVariablesSet profile)
+ {
+ profile.PropertyChanged += Profile_PropertyChanged;
+ if (profile.IsEnabled)
+ {
+ UnsetAppliedProfile();
+ SetAppliedProfile(profile);
+ }
+
+ Profiles.Add(profile);
+
+ _ = Task.Run(SaveAsync);
+ }
+
+ internal void UpdateProfile(ProfileVariablesSet updatedProfile)
+ {
+ var existingProfile = Profiles.Where(x => x.Id == updatedProfile.Id).FirstOrDefault();
+ if (existingProfile != null)
+ {
+ if (updatedProfile.IsEnabled)
+ {
+ // Let's unset the profile before applying the update. Even if this one is the one that's currently set.
+ UnsetAppliedProfile();
+ }
+
+ existingProfile.Name = updatedProfile.Name;
+ existingProfile.IsEnabled = updatedProfile.IsEnabled;
+ existingProfile.Variables = updatedProfile.Variables;
+ }
+
+ _ = Task.Run(SaveAsync);
+ }
+
+ private async Task SaveAsync()
+ {
+ try
+ {
+ await _environmentVariablesService.WriteAsync(Profiles);
+ }
+ catch (Exception ex)
+ {
+ // Show some error
+ Logger.LogError("Failed to save to profiles.json file", ex);
+ }
+ }
+
+ private void Profile_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ var profile = sender as ProfileVariablesSet;
+
+ if (profile != null)
+ {
+ if (e.PropertyName == nameof(ProfileVariablesSet.IsEnabled))
+ {
+ if (profile.IsEnabled)
+ {
+ UnsetAppliedProfile();
+ SetAppliedProfile(profile);
+
+ var telemetryEnabled = new Telemetry.EnvironmentVariablesProfileEnabledEvent()
+ {
+ Enabled = true,
+ };
+
+ PowerToysTelemetry.Log.WriteEvent(telemetryEnabled);
+ }
+ else
+ {
+ UnsetAppliedProfile();
+
+ var telemetryEnabled = new Telemetry.EnvironmentVariablesProfileEnabledEvent()
+ {
+ Enabled = false,
+ };
+
+ PowerToysTelemetry.Log.WriteEvent(telemetryEnabled);
+ }
+ }
+ }
+
+ _ = Task.Run(SaveAsync);
+ }
+
+ private void SetAppliedProfile(ProfileVariablesSet profile)
+ {
+ if (profile != null)
+ {
+ if (!profile.IsApplicable())
+ {
+ profile.PropertyChanged -= Profile_PropertyChanged;
+ profile.IsEnabled = false;
+ profile.PropertyChanged += Profile_PropertyChanged;
+
+ EnvironmentState = EnvironmentState.ProfileNotApplicable;
+
+ return;
+ }
+ }
+
+ var task = profile.Apply();
+ task.ContinueWith((a) =>
+ {
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ PopulateAppliedVariables();
+ });
+ });
+ AppliedProfile = profile;
+ }
+
+ private void UnsetAppliedProfile()
+ {
+ if (AppliedProfile != null)
+ {
+ var appliedProfile = AppliedProfile;
+ appliedProfile.PropertyChanged -= Profile_PropertyChanged;
+ var task = AppliedProfile.UnApply();
+ task.ContinueWith((a) =>
+ {
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ PopulateAppliedVariables();
+ });
+ });
+ AppliedProfile.IsEnabled = false;
+ AppliedProfile = null;
+ appliedProfile.PropertyChanged += Profile_PropertyChanged;
+ }
+ }
+
+ internal void RemoveProfile(ProfileVariablesSet profile)
+ {
+ if (profile.IsEnabled)
+ {
+ UnsetAppliedProfile();
+ }
+
+ Profiles.Remove(profile);
+
+ _ = Task.Run(SaveAsync);
+ }
+
+ internal void DeleteVariable(Variable variable, ProfileVariablesSet profile)
+ {
+ bool propagateChange = true;
+
+ if (profile != null)
+ {
+ // Profile variable
+ profile.Variables.Remove(variable);
+
+ if (!profile.IsEnabled)
+ {
+ propagateChange = false;
+ }
+
+ _ = Task.Run(SaveAsync);
+ }
+ else
+ {
+ if (variable.ParentType == VariablesSetType.User)
+ {
+ UserDefaultSet.Variables.Remove(variable);
+ }
+ else if (variable.ParentType == VariablesSetType.System)
+ {
+ SystemDefaultSet.Variables.Remove(variable);
+ }
+ }
+
+ if (propagateChange)
+ {
+ var task = Task.Run(() =>
+ {
+ if (profile == null)
+ {
+ EnvironmentVariablesHelper.UnsetVariable(variable);
+ }
+ else
+ {
+ profile.UnapplyVariable(variable);
+ }
+ });
+ task.ContinueWith((a) =>
+ {
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ PopulateAppliedVariables();
+ });
+ });
+ }
+ }
+ }
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/app.manifest b/src/modules/EnvironmentVariables/EnvironmentVariables/app.manifest
new file mode 100644
index 0000000000..cbe8c5420c
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/app.manifest
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PerMonitorV2
+
+
+
\ No newline at end of file
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.base.rc b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.base.rc
new file mode 100644
index 0000000000..5fa3c8b90d
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.base.rc
@@ -0,0 +1,40 @@
+#include
+#include "resource.h"
+#include "../../../common/version/version.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+#include "winres.h"
+#undef APSTUDIO_READONLY_SYMBOLS
+
+1 VERSIONINFO
+FILEVERSION FILE_VERSION
+PRODUCTVERSION PRODUCT_VERSION
+FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+FILEFLAGS VS_FF_DEBUG
+#else
+FILEFLAGS 0x0L
+#endif
+FILEOS VOS_NT_WINDOWS32
+FILETYPE VFT_DLL
+FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
+ BEGIN
+ VALUE "CompanyName", COMPANY_NAME
+ VALUE "FileDescription", FILE_DESCRIPTION
+ VALUE "FileVersion", FILE_VERSION_STRING
+ VALUE "InternalName", INTERNAL_NAME
+ VALUE "LegalCopyright", COPYRIGHT_NOTE
+ VALUE "OriginalFilename", ORIGINAL_FILENAME
+ VALUE "ProductName", PRODUCT_NAME
+ VALUE "ProductVersion", PRODUCT_VERSION_STRING
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
+ END
+END
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj
new file mode 100644
index 0000000000..6bf3bb3b20
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+ 17.0
+ Win32Proj
+ {b9420661-b0e4-4241-abd4-4a27a1f64250}
+ EnvironmentVariablesModuleInterface
+ EnvironmentVariablesModuleInterface
+
+
+
+ DynamicLibrary
+ true
+ v143
+ Unicode
+
+
+ DynamicLibrary
+ false
+ v143
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\
+ PowerToys.EnvironmentVariablesModuleInterface
+
+
+
+ Level3
+ true
+ WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ Use
+
+
+ Windows
+ true
+ false
+
+
+
+
+ Level3
+ true
+ true
+ true
+ WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ Use
+
+
+ Windows
+ true
+ true
+ true
+ false
+
+
+
+
+ $(SolutionDir)src;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories)
+
+
+
+
+
+
+
+
+
+
+
+ Create
+
+
+
+
+
+
+
+
+
+
+
+ {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}
+
+
+ {6955446d-23f7-4023-9bb3-8657f904af99}
+
+
+ {cc6e41ac-8174-4e8a-8d22-85dd7f4851df}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj.filters b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj.filters
new file mode 100644
index 0000000000..14b6af8966
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj.filters
@@ -0,0 +1,52 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Resource Files
+
+
+ Resource Files
+
+
+
+
+ Resource Files
+
+
+
\ No newline at end of file
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/Resource.resx b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/Resource.resx
new file mode 100644
index 0000000000..d9331c4ad0
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/Resource.resx
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Environment Variables
+
+
\ No newline at end of file
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp
new file mode 100644
index 0000000000..9adbb3cd10
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp
@@ -0,0 +1,280 @@
+// dllmain.cpp : Defines the entry point for the DLL application.
+#include "pch.h"
+
+#include "Generated Files/resource.h"
+#include "trace.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+extern "C" IMAGE_DOS_HEADER __ImageBase;
+
+BOOL APIENTRY DllMain( HMODULE hModule,
+ DWORD ul_reason_for_call,
+ LPVOID lpReserved
+ )
+{
+ switch (ul_reason_for_call)
+ {
+ case DLL_PROCESS_ATTACH:
+ Trace::RegisterProvider();
+ break;
+ case DLL_THREAD_ATTACH:
+ case DLL_THREAD_DETACH:
+ break;
+ case DLL_PROCESS_DETACH:
+ Trace::UnregisterProvider();
+ break;
+ }
+ return TRUE;
+}
+
+namespace
+{
+ // Name of the powertoy module.
+ inline const std::wstring ModuleKey = L"EnvironmentVariables";
+}
+
+class EnvironmentVariablesModuleInterface : public PowertoyModuleIface
+{
+private:
+ bool m_enabled = false;
+
+ std::wstring app_name;
+
+ //contains the non localized key of the powertoy
+ std::wstring app_key;
+
+ HANDLE m_hProcess;
+
+ HANDLE m_hShowEvent;
+
+ EventWaiter m_showEventWaiter;
+
+ HANDLE m_hShowAdminEvent;
+
+ EventWaiter m_showAdminEventWaiter;
+
+ bool is_process_running()
+ {
+ return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
+ }
+
+ void bring_process_to_front()
+ {
+ auto enum_windows = [](HWND hwnd, LPARAM param) -> BOOL {
+ HANDLE process_handle = reinterpret_cast(param);
+ DWORD window_process_id = 0;
+
+ GetWindowThreadProcessId(hwnd, &window_process_id);
+ if (GetProcessId(process_handle) == window_process_id)
+ {
+ SetForegroundWindow(hwnd);
+ return FALSE;
+ }
+ return TRUE;
+ };
+
+ EnumWindows(enum_windows, (LPARAM)m_hProcess);
+ }
+
+ void launch_process(bool runas)
+ {
+ Logger::trace("EnvironmentVariablesModuleInterface::launch_process()");
+ unsigned long powertoys_pid = GetCurrentProcessId();
+
+ std::wstring executable_args = L"";
+ executable_args.append(std::to_wstring(powertoys_pid));
+
+ SHELLEXECUTEINFOW sei{ sizeof(sei) };
+ sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
+ sei.lpFile = L"WinUI3Apps\\PowerToys.EnvironmentVariables.exe";
+ sei.nShow = SW_SHOWNORMAL;
+ sei.lpParameters = executable_args.data();
+
+ if (runas)
+ {
+ sei.lpVerb = L"runas";
+ }
+
+ if (ShellExecuteExW(&sei))
+ {
+ Logger::trace("Successfully started the Environment Variables process");
+ }
+ else
+ {
+ Logger::error(L"Environment Variables failed to start. {}", get_last_error_or_default(GetLastError()));
+ }
+
+ m_hProcess = sei.hProcess;
+ }
+
+public:
+ EnvironmentVariablesModuleInterface()
+ {
+ app_name = GET_RESOURCE_STRING(IDS_ENVIRONMENT_VARIABLES_NAME);
+ app_key = ModuleKey;
+ LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::environmentVariablesLoggerName);
+
+ m_hShowEvent = CreateDefaultEvent(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT);
+ if (!m_hShowEvent)
+ {
+ Logger::error(L"Failed to create show Environment Variables event");
+ auto message = get_last_error_message(GetLastError());
+ if (message.has_value())
+ {
+ Logger::error(message.value());
+ }
+ }
+
+ m_hShowAdminEvent = CreateDefaultEvent(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT);
+ if (!m_hShowAdminEvent)
+ {
+ Logger::error(L"Failed to create show Environment Variables admin event");
+ auto message = get_last_error_message(GetLastError());
+ if (message.has_value())
+ {
+ Logger::error(message.value());
+ }
+ }
+
+ m_showEventWaiter = EventWaiter(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT, [&](int err) {
+ if (m_enabled && err == ERROR_SUCCESS)
+ {
+ Logger::trace(L"{} event was signaled", CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT);
+
+ if (is_process_running())
+ {
+ bring_process_to_front();
+ }
+ else
+ {
+ launch_process(false);
+ }
+
+ Trace::ActivateEnvironmentVariables();
+ }
+ });
+
+ m_showAdminEventWaiter = EventWaiter(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT, [&](int err) {
+ if (m_enabled && err == ERROR_SUCCESS)
+ {
+ Logger::trace(L"{} event was signaled", CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT);
+
+ if (is_process_running())
+ {
+ bring_process_to_front();
+ }
+ else
+ {
+ launch_process(true);
+ }
+
+ Trace::ActivateEnvironmentVariables();
+ }
+ });
+ }
+
+ ~EnvironmentVariablesModuleInterface()
+ {
+ m_enabled = false;
+ }
+
+ // Destroy the powertoy and free memory
+ virtual void destroy() override
+ {
+ Logger::trace("EnvironmentVariablesModuleInterface::destroy()");
+
+ if (m_hShowEvent)
+ {
+ CloseHandle(m_hShowEvent);
+ m_hShowEvent = nullptr;
+ }
+
+ if (m_hShowAdminEvent)
+ {
+ CloseHandle(m_hShowAdminEvent);
+ m_hShowAdminEvent = nullptr;
+ }
+
+ delete this;
+ }
+
+ // Return the localized display name of the powertoy
+ virtual const wchar_t* get_name() override
+ {
+ return app_name.c_str();
+ }
+
+ // Return the non localized key of the powertoy, this will be cached by the runner
+ virtual const wchar_t* get_key() override
+ {
+ return app_key.c_str();
+ }
+
+ // Return the configured status for the gpo policy for the module
+ virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
+ {
+ return powertoys_gpo::getConfiguredEnvironmentVariablesEnabledValue();
+ }
+
+ virtual bool get_config(wchar_t* /*buffer*/, int* /*buffer_size*/) override
+ {
+ return false;
+ }
+
+ virtual void call_custom_action(const wchar_t* /*action*/) override
+ {
+ }
+
+ virtual void set_config(const wchar_t* /*config*/) override
+ {
+ }
+
+ virtual bool is_enabled() override
+ {
+ return m_enabled;
+ }
+
+ virtual void enable()
+ {
+ Logger::trace("EnvironmentVariablesModuleInterface::enable()");
+ m_enabled = true;
+ Trace::EnableEnvironmentVariables(true);
+ }
+
+ virtual void disable()
+ {
+ Logger::trace("EnvironmentVariablesModuleInterface::disable()");
+ if (m_enabled)
+ {
+ if (m_hShowEvent)
+ {
+ ResetEvent(m_hShowEvent);
+ }
+
+ if (m_hShowAdminEvent)
+ {
+ ResetEvent(m_hShowAdminEvent);
+ }
+
+ TerminateProcess(m_hProcess, 1);
+ }
+
+ m_enabled = false;
+ Trace::EnableEnvironmentVariables(false);
+ }
+};
+
+extern "C" __declspec(dllexport) PowertoyModuleIface * __cdecl powertoy_create()
+{
+ return new EnvironmentVariablesModuleInterface();
+}
\ No newline at end of file
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/pch.cpp b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/pch.cpp
new file mode 100644
index 0000000000..64b7eef6d6
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/pch.cpp
@@ -0,0 +1,5 @@
+// pch.cpp: source file corresponding to the pre-compiled header
+
+#include "pch.h"
+
+// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/pch.h b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/pch.h
new file mode 100644
index 0000000000..6f70098567
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/pch.h
@@ -0,0 +1,18 @@
+// pch.h: This is a precompiled header file.
+// Files listed below are compiled only once, improving build performance for future builds.
+// This also affects IntelliSense performance, including code completion and many code browsing features.
+// However, files listed here are ALL re-compiled if any one of them is updated between builds.
+// Do not add files here that you will be updating frequently as this negates the performance advantage.
+
+#ifndef PCH_H
+#define PCH_H
+
+#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
+// Windows Header Files
+#include
+#include
+
+#include
+#include
+
+#endif //PCH_H
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/resource.base.h b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/resource.base.h
new file mode 100644
index 0000000000..da4dc076a8
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/resource.base.h
@@ -0,0 +1,13 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by AlwaysOnTopModuleInterface.rc
+
+//////////////////////////////
+// Non-localizable
+
+#define FILE_DESCRIPTION "PowerToys Environment Variables Module"
+#define INTERNAL_NAME "PowerToys.EnvironmentVariablesModuleInterface"
+#define ORIGINAL_FILENAME "PowerToys.EnvironmentVariablesModuleInterface.dll"
+
+// Non-localizable
+//////////////////////////////
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/trace.cpp b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/trace.cpp
new file mode 100644
index 0000000000..bb458c1b6d
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/trace.cpp
@@ -0,0 +1,40 @@
+#include "pch.h"
+#include "trace.h"
+
+TRACELOGGING_DEFINE_PROVIDER(
+ g_hProvider,
+ "Microsoft.PowerToys",
+ // {38e8889b-9731-53f5-e901-e8a7c1753074}
+ (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
+ TraceLoggingOptionProjectTelemetry());
+
+void Trace::RegisterProvider() noexcept
+{
+ TraceLoggingRegister(g_hProvider);
+}
+
+void Trace::UnregisterProvider() noexcept
+{
+ TraceLoggingUnregister(g_hProvider);
+}
+
+// Log if the user has Environment Variables enabled or disabled
+void Trace::EnableEnvironmentVariables(const bool enabled) noexcept
+{
+ TraceLoggingWrite(
+ g_hProvider,
+ "EnvironmentVariables_EnableEnvironmentVariables",
+ ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
+ TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
+ TraceLoggingBoolean(enabled, "Enabled"));
+}
+
+// Log that the user tried to activate the editor
+void Trace::ActivateEnvironmentVariables() noexcept
+{
+ TraceLoggingWrite(
+ g_hProvider,
+ "EnvironmentVariables_Activate",
+ ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
+ TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
+}
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/trace.h b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/trace.h
new file mode 100644
index 0000000000..0898da4602
--- /dev/null
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/trace.h
@@ -0,0 +1,14 @@
+#pragma once
+
+class Trace
+{
+public:
+ static void RegisterProvider() noexcept;
+ static void UnregisterProvider() noexcept;
+
+ // Log if the user has EnvironmentVariables enabled or disabled
+ static void EnableEnvironmentVariables(const bool enabled) noexcept;
+
+ // Log that the user tried to activate the editor
+ static void ActivateEnvironmentVariables() noexcept;
+};
diff --git a/src/modules/keyboardmanager/dll/dllmain.cpp b/src/modules/keyboardmanager/dll/dllmain.cpp
index 0f0ea94bee..e76fd5f716 100644
--- a/src/modules/keyboardmanager/dll/dllmain.cpp
+++ b/src/modules/keyboardmanager/dll/dllmain.cpp
@@ -168,6 +168,13 @@ public:
{
return m_enabled;
}
+
+ // Returns whether the PowerToys should be enabled by default
+ virtual bool is_enabled_by_default() const override
+ {
+ return false;
+ }
+
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs
index 8fd8529fef..876a1dabf2 100644
--- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs
+++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs
@@ -61,11 +61,25 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys.Components
AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
Action = _ =>
{
- using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowHostsAdminSharedEvent()))
- {
- eventHandle.Set();
- }
-
+ using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowHostsAdminSharedEvent());
+ eventHandle.Set();
+ return true;
+ },
+ });
+ }
+ else if (Key == UtilityKey.EnvironmentVariables)
+ {
+ results.Add(new ContextMenuResult
+ {
+ Title = Resources.Action_Run_As_Administrator,
+ Glyph = "\xE7EF",
+ FontFamily = "Segoe MDL2 Assets",
+ AcceleratorKey = System.Windows.Input.Key.Enter,
+ AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
+ Action = _ =>
+ {
+ using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowEnvironmentVariablesAdminSharedEvent());
+ eventHandle.Set();
return true;
},
});
diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityHelper.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityHelper.cs
index 3d495479a9..b49eb4ecd0 100644
--- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityHelper.cs
+++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityHelper.cs
@@ -20,6 +20,7 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys.Components
UtilityKey.ShortcutGuide => "Images/ShortcutGuide.png",
UtilityKey.RegistryPreview => "Images/RegistryPreview.png",
UtilityKey.CropAndLock => "Images/CropAndLock.png",
+ UtilityKey.EnvironmentVariables => "Images/EnvironmentVariables.png",
_ => null,
};
}
@@ -36,6 +37,7 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys.Components
UtilityKey.ShortcutGuide => SettingsDeepLink.SettingsWindow.ShortcutGuide,
UtilityKey.RegistryPreview => SettingsDeepLink.SettingsWindow.RegistryPreview,
UtilityKey.CropAndLock => SettingsDeepLink.SettingsWindow.CropAndLock,
+ UtilityKey.EnvironmentVariables => SettingsDeepLink.SettingsWindow.EnvironmentVariables,
_ => null,
};
}
diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityKey.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityKey.cs
index 33cb97b4a6..bc4db24615 100644
--- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityKey.cs
+++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityKey.cs
@@ -14,5 +14,6 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys.Components
ShortcutGuide = 5,
RegistryPreview = 6,
CropAndLock = 7,
+ EnvironmentVariables = 8,
}
}
diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityProvider.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityProvider.cs
index 59fc0c82f5..6497da89db 100644
--- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityProvider.cs
+++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityProvider.cs
@@ -170,6 +170,20 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys
}));
}
+ if (GPOWrapper.GetConfiguredEnvironmentVariablesEnabledValue() != GpoRuleConfigured.Disabled)
+ {
+ _utilities.Add(new Utility(
+ UtilityKey.EnvironmentVariables,
+ Resources.Environment_Variables,
+ generalSettings.Enabled.EnvironmentVariables,
+ (_) =>
+ {
+ using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowEnvironmentVariablesSharedEvent());
+ eventHandle.Set();
+ return true;
+ }));
+ }
+
_watcher = new FileSystemWatcher
{
Path = Path.GetDirectoryName(settingsUtils.GetSettingsFilePath()),
@@ -214,6 +228,7 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys
case UtilityKey.ShortcutGuide: u.Enable(generalSettings.Enabled.ShortcutGuide); break;
case UtilityKey.RegistryPreview: u.Enable(generalSettings.Enabled.RegistryPreview); break;
case UtilityKey.CropAndLock: u.Enable(generalSettings.Enabled.CropAndLock); break;
+ case UtilityKey.EnvironmentVariables: u.Enable(generalSettings.Enabled.EnvironmentVariables); break;
}
}
diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Microsoft.PowerToys.Run.Plugin.PowerToys.csproj b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Microsoft.PowerToys.Run.Plugin.PowerToys.csproj
index 08ddf577dc..f56985e9ff 100644
--- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Microsoft.PowerToys.Run.Plugin.PowerToys.csproj
+++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Microsoft.PowerToys.Run.Plugin.PowerToys.csproj
@@ -105,6 +105,10 @@
Images\ShortcutGuide.png
PreserveNewest
+
+ Images\EnvironmentVariables.png
+ PreserveNewest
+
diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.Designer.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.Designer.cs
index 05d0f47ec5..4f01b8edb3 100644
--- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.Designer.cs
+++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.Designer.cs
@@ -105,6 +105,15 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys.Properties {
}
}
+ ///
+ /// Looks up a localized string similar to Environment Variables.
+ ///
+ internal static string Environment_Variables {
+ get {
+ return ResourceManager.GetString("Environment_Variables", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to FancyZones Editor.
///
diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.resx b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.resx
index cbc299caa9..325697d375 100644
--- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.resx
+++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.resx
@@ -135,6 +135,10 @@
Crop And Lock (Thumbnail)
"Crop And Lock" is the name of the utility, "Thumbnail" is the activation mode
+
+ Environment Variables
+ "Environment Variables" is the name of the utility
+
FancyZones Editor
"FancyZones" is the name of the utility
diff --git a/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs
index 0941943639..450c3f5e78 100644
--- a/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs
+++ b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs
@@ -774,7 +774,19 @@ namespace RegistryPreview
// before moving onto the next node, tag the previous node and update the path
previousNode = newNode;
- fullPath = fullPath.Replace(string.Format(CultureInfo.InvariantCulture, @"\{0}", individualKeys[i]), string.Empty);
+
+ // this used to use Replace but that would replace all instances of the same key name, which causes bugs.
+ try
+ {
+ int removeAt = fullPath.LastIndexOf(string.Format(CultureInfo.InvariantCulture, @"\{0}", individualKeys[i]), StringComparison.InvariantCulture);
+ if (removeAt > -1)
+ {
+ fullPath = fullPath.Substring(0, removeAt);
+ }
+ }
+ catch
+ {
+ }
// One last check: if we get here, the parent of this node is not yet in the tree, so we need to add it as a RootNode
if (i == 0)
diff --git a/src/runner/main.cpp b/src/runner/main.cpp
index eb1fb48503..7749fc9f17 100644
--- a/src/runner/main.cpp
+++ b/src/runner/main.cpp
@@ -81,7 +81,7 @@ inline wil::unique_mutex_nothrow create_msi_mutex()
void open_menu_from_another_instance(std::optional settings_window)
{
const HWND hwnd_main = FindWindowW(L"PToyTrayIconWindow", nullptr);
- LPARAM msg = static_cast(ESettingsWindowNames::Overview);
+ LPARAM msg = static_cast(ESettingsWindowNames::Dashboard);
if (settings_window.has_value() && settings_window.value() != "")
{
msg = static_cast(ESettingsWindowNames_from_string(settings_window.value()));
@@ -155,6 +155,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
L"WinUI3Apps/PowerToys.MeasureToolModuleInterface.dll",
L"WinUI3Apps/PowerToys.HostsModuleInterface.dll",
L"WinUI3Apps/PowerToys.Peek.dll",
+ L"WinUI3Apps/PowerToys.EnvironmentVariablesModuleInterface.dll",
L"PowerToys.MouseWithoutBordersModuleInterface.dll",
L"PowerToys.CropAndLockModuleInterface.dll",
};
diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp
index bcba0915b1..86a6f25c13 100644
--- a/src/runner/settings_window.cpp
+++ b/src/runner/settings_window.cpp
@@ -596,7 +596,7 @@ void open_settings_window(std::optional settings_window, bool show
}
else
{
- current_settings_ipc->send(L"{\"ShowYourself\":\"Overview\"}");
+ current_settings_ipc->send(L"{\"ShowYourself\":\"Dashboard\"}");
}
}
}
@@ -676,6 +676,10 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value)
return "RegistryPreview";
case ESettingsWindowNames::CropAndLock:
return "CropAndLock";
+ case ESettingsWindowNames::EnvironmentVariables:
+ return "EnvironmentVariables";
+ case ESettingsWindowNames::Dashboard:
+ return "Dashboard";
default:
{
Logger::error(L"Can't convert ESettingsWindowNames value={} to string", static_cast(value));
@@ -755,11 +759,19 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
{
return ESettingsWindowNames::CropAndLock;
}
+ else if (value == "EnvironmentVariables")
+ {
+ return ESettingsWindowNames::EnvironmentVariables;
+ }
+ else if (value == "Dashboard")
+ {
+ return ESettingsWindowNames::Dashboard;
+ }
else
{
Logger::error(L"Can't convert string value={} to ESettingsWindowNames", winrt::to_hstring(value));
assert(false);
}
- return ESettingsWindowNames::Overview;
+ return ESettingsWindowNames::Dashboard;
}
diff --git a/src/runner/settings_window.h b/src/runner/settings_window.h
index 2a14ec46e0..0f213b28bc 100644
--- a/src/runner/settings_window.h
+++ b/src/runner/settings_window.h
@@ -4,7 +4,8 @@
enum class ESettingsWindowNames
{
- Overview = 0,
+ Dashboard = 0,
+ Overview,
Awake,
ColorPicker,
FancyZones,
@@ -21,6 +22,7 @@ enum class ESettingsWindowNames
PowerOCR,
RegistryPreview,
CropAndLock,
+ EnvironmentVariables,
};
std::string ESettingsWindowNames_to_string(ESettingsWindowNames value);
diff --git a/src/settings-ui/Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Settings.UI.Library/EnabledModules.cs
index b93ad376b3..27af49c4eb 100644
--- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs
+++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs
@@ -427,6 +427,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
+ private bool environmentVariables = true;
+
+ [JsonPropertyName("EnvironmentVariables")]
+ public bool EnvironmentVariables
+ {
+ get => environmentVariables;
+ set
+ {
+ if (environmentVariables != value)
+ {
+ LogTelemetryEvent(value);
+ environmentVariables = value;
+ }
+ }
+ }
+
private void NotifyChange()
{
notifyEnabledChangedAction?.Invoke();
diff --git a/src/settings-ui/Settings.UI.Library/EnvironmentVariablesProperties.cs b/src/settings-ui/Settings.UI.Library/EnvironmentVariablesProperties.cs
new file mode 100644
index 0000000000..2d2b93d95f
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/EnvironmentVariablesProperties.cs
@@ -0,0 +1,20 @@
+// 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.Text.Json.Serialization;
+using Settings.UI.Library.Enumerations;
+
+namespace Microsoft.PowerToys.Settings.UI.Library
+{
+ public class EnvironmentVariablesProperties
+ {
+ [JsonConverter(typeof(BoolPropertyJsonConverter))]
+ public bool LaunchAdministrator { get; set; }
+
+ public EnvironmentVariablesProperties()
+ {
+ LaunchAdministrator = true;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/EnvironmentVariablesSettings.cs b/src/settings-ui/Settings.UI.Library/EnvironmentVariablesSettings.cs
new file mode 100644
index 0000000000..0263d88bfc
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/EnvironmentVariablesSettings.cs
@@ -0,0 +1,46 @@
+// 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.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+
+namespace Microsoft.PowerToys.Settings.UI.Library
+{
+ public class EnvironmentVariablesSettings : BasePTModuleSettings, ISettingsConfig
+ {
+ public const string ModuleName = "EnvironmentVariables";
+
+ [JsonPropertyName("properties")]
+ public EnvironmentVariablesProperties Properties { get; set; }
+
+ public EnvironmentVariablesSettings()
+ {
+ Properties = new EnvironmentVariablesProperties();
+ Version = "1.0";
+ Name = ModuleName;
+ }
+
+ public virtual void Save(ISettingsUtils settingsUtils)
+ {
+ // Save settings to file
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ };
+
+ if (settingsUtils == null)
+ {
+ throw new ArgumentNullException(nameof(settingsUtils));
+ }
+
+ settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName);
+ }
+
+ public string GetModuleName() => Name;
+
+ public bool UpgradeSettingsConfiguration() => false;
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsEnvironmentVariables.png b/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsEnvironmentVariables.png
new file mode 100644
index 0000000000..159f7c0da1
Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsEnvironmentVariables.png differ
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/EnvironmentVariables.png b/src/settings-ui/Settings.UI/Assets/Settings/Modules/EnvironmentVariables.png
new file mode 100644
index 0000000000..97e2ab67e6
Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/EnvironmentVariables.png differ
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/EnvironmentVariables.png b/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/EnvironmentVariables.png
new file mode 100644
index 0000000000..dcfb68d067
Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/EnvironmentVariables.png differ
diff --git a/src/settings-ui/Settings.UI/Converters/ModuleItemTemplateSelector.cs b/src/settings-ui/Settings.UI/Converters/ModuleItemTemplateSelector.cs
new file mode 100644
index 0000000000..b34331a0e7
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Converters/ModuleItemTemplateSelector.cs
@@ -0,0 +1,33 @@
+// 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 Microsoft.PowerToys.Settings.UI.ViewModels;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Microsoft.PowerToys.Settings.UI.Converters
+{
+ public class ModuleItemTemplateSelector : DataTemplateSelector
+ {
+ public DataTemplate TextTemplate { get; set; }
+
+ public DataTemplate ButtonTemplate { get; set; }
+
+ public DataTemplate ShortcutTemplate { get; set; }
+
+ public DataTemplate KBMTemplate { get; set; }
+
+ protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
+ {
+ switch (item)
+ {
+ case DashboardModuleButtonItem: return ButtonTemplate;
+ case DashboardModuleShortcutItem: return ShortcutTemplate;
+ case DashboardModuleTextItem: return TextTemplate;
+ case DashboardModuleKBMItem: return KBMTemplate;
+ default: return TextTemplate;
+ }
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Converters/NegativeBoolToVisibilityConverter.cs b/src/settings-ui/Settings.UI/Converters/NegativeBoolToVisibilityConverter.cs
new file mode 100644
index 0000000000..df9f02eca7
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Converters/NegativeBoolToVisibilityConverter.cs
@@ -0,0 +1,37 @@
+// 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.Globalization;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Data;
+
+namespace Microsoft.PowerToys.Settings.UI.Converters
+{
+ public class NegativeBoolToVisibilityConverter : IValueConverter
+ {
+ object IValueConverter.Convert(object value, Type targetType, object parameter, string language)
+ {
+ if ((bool)value)
+ {
+ return Visibility.Collapsed;
+ }
+
+ return Visibility.Visible;
+ }
+
+ object IValueConverter.ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ if (value is Visibility)
+ {
+ if ((Visibility)value == Visibility.Visible)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs
index af54f7c679..1a5091f5f4 100644
--- a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs
+++ b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs
@@ -11,6 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
Awake,
ColorPicker,
CropAndLock,
+ EnvironmentVariables,
FancyZones,
FileLocksmith,
FileExplorer,
diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
index bffa7cc353..fb3ee12164 100644
--- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
+++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
@@ -21,6 +21,9 @@
PowerToys.Settings.pri
+
+
+
@@ -124,5 +127,11 @@
Always
+
+
+
+ $(DefaultXamlRuntime)
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs
index ba9988dd2d..202cadc306 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs
@@ -60,7 +60,7 @@ namespace Microsoft.PowerToys.Settings.UI
public bool ShowScoobe { get; set; }
- public Type StartupPage { get; set; } = typeof(Views.GeneralPage);
+ public Type StartupPage { get; set; } = typeof(Views.DashboardPage);
public static Action IPCMessageReceivedCallback { get; set; }
@@ -218,8 +218,8 @@ namespace Microsoft.PowerToys.Settings.UI
settingsWindow.NavigateToSection(StartupPage);
ShowMessageDialog("The application is running in Debug mode.", "DEBUG");
#else
- /* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the General page. */
- SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Overview, true);
+ /* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */
+ SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Dashboard, true);
Exit();
#endif
}
@@ -380,6 +380,7 @@ namespace Microsoft.PowerToys.Settings.UI
{
switch (settingWindow)
{
+ case "Dashboard": return typeof(DashboardPage);
case "Overview": return typeof(GeneralPage);
case "AlwaysOnTop": return typeof(AlwaysOnTopPage);
case "Awake": return typeof(AwakePage);
@@ -403,10 +404,11 @@ namespace Microsoft.PowerToys.Settings.UI
case "PastePlain": return typeof(PastePlainPage);
case "Peek": return typeof(PeekPage);
case "CropAndLock": return typeof(CropAndLockPage);
+ case "EnvironmentVariables": return typeof(EnvironmentVariablesPage);
default:
- // Fallback to general
+ // Fallback to Dashboard
Debug.Assert(false, "Unexpected SettingsWindow argument value");
- return typeof(GeneralPage);
+ return typeof(DashboardPage);
}
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.cs
index 5e5d3ee846..8b36831984 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.cs
@@ -116,7 +116,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
case 91: // The left Windows key
case 92: // The right Windows key
- PathIcon winIcon = XamlReader.Load(@"") as PathIcon;
+ PathIcon winIcon = XamlReader.Load(@"") as PathIcon;
Viewbox winIconContainer = new Viewbox();
winIconContainer.Child = winIcon;
winIconContainer.HorizontalAlignment = HorizontalAlignment.Center;
@@ -143,6 +143,10 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
return (Style)App.Current.Resources["SmallOutline" + styleName];
}
+ else if (VisualType == VisualType.TextOnly)
+ {
+ return (Style)App.Current.Resources["Only" + styleName];
+ }
else
{
return (Style)App.Current.Resources["Default" + styleName];
@@ -181,6 +185,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
Small,
SmallOutline,
+ TextOnly,
Large,
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml
index 2d469563a2..68590a040a 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml
@@ -5,9 +5,7 @@
16
12
-
+
+
+
+
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml
index 1a51b17874..1f1fc9e071 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml
@@ -23,9 +23,7 @@
ItemsSource="{x:Bind Keys}">
-
+
@@ -41,7 +39,6 @@
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs
index ebb1928937..f28ccd25ce 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs
@@ -42,6 +42,21 @@ namespace Microsoft.PowerToys.Settings.UI.Flyout
}
break;
+ case "EnvironmentVariables": // Launch Environment Variables
+ {
+ bool launchAdmin = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.LaunchAdministrator;
+ string eventName = !App.IsElevated && launchAdmin
+ ? Constants.ShowEnvironmentVariablesAdminSharedEvent()
+ : Constants.ShowEnvironmentVariablesSharedEvent();
+
+ using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName))
+ {
+ eventHandle.Set();
+ }
+ }
+
+ break;
+
case "FancyZones": // Launch FancyZones Editor
using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.FZEToggleEvent()))
{
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs
index 142ba0b6b4..874e3b6130 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs
@@ -100,6 +100,9 @@ namespace Microsoft.PowerToys.Settings.UI
case "CropAndLock":
needToUpdate = generalSettingsConfig.Enabled.CropAndLock != isEnabled;
generalSettingsConfig.Enabled.CropAndLock = isEnabled; break;
+ case "EnvironmentVariables":
+ needToUpdate = generalSettingsConfig.Enabled.EnvironmentVariables != isEnabled;
+ generalSettingsConfig.Enabled.EnvironmentVariables = isEnabled; break;
case "FancyZones":
needToUpdate = generalSettingsConfig.Enabled.FancyZones != isEnabled;
generalSettingsConfig.Enabled.FancyZones = isEnabled; break;
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml
new file mode 100644
index 0000000000..4a8ee0e2c2
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml.cs
new file mode 100644
index 0000000000..308aabb105
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.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.Threading;
+using interop;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
+using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
+using Microsoft.PowerToys.Settings.UI.Views;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Navigation;
+
+namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
+{
+ public sealed partial class OobeEnvironmentVariables : Page
+ {
+ public OobePowerToysModule ViewModel { get; }
+
+ public OobeEnvironmentVariables()
+ {
+ InitializeComponent();
+ ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.EnvironmentVariables]);
+ DataContext = ViewModel;
+ }
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ ViewModel.LogOpeningModuleEvent();
+ }
+
+ protected override void OnNavigatedFrom(NavigationEventArgs e)
+ {
+ ViewModel.LogClosingModuleEvent();
+ }
+
+ private void Launch_EnvironmentVariables_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ bool launchAdmin = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.LaunchAdministrator;
+ string eventName = !App.IsElevated && launchAdmin
+ ? Constants.ShowEnvironmentVariablesAdminSharedEvent()
+ : Constants.ShowEnvironmentVariablesSharedEvent();
+
+ using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName))
+ {
+ eventHandle.Set();
+ }
+ }
+
+ private void Launch_Settings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ if (OobeShellPage.OpenMainWindowCallback != null)
+ {
+ OobeShellPage.OpenMainWindowCallback(typeof(EnvironmentVariablesPage));
+ }
+
+ ViewModel.LogOpeningSettingsEvent();
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs
index b86a17aa38..d43bda83bf 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs
@@ -25,7 +25,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
if (OobeShellPage.OpenMainWindowCallback != null)
{
- OobeShellPage.OpenMainWindowCallback(typeof(GeneralPage));
+ OobeShellPage.OpenMainWindowCallback(typeof(DashboardPage));
}
ViewModel.LogOpeningSettingsEvent();
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverviewAlternate.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverviewAlternate.xaml.cs
index c5171f4159..d0ae488347 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverviewAlternate.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverviewAlternate.xaml.cs
@@ -31,7 +31,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
if (OobeShellPage.OpenMainWindowCallback != null)
{
- OobeShellPage.OpenMainWindowCallback(typeof(GeneralPage));
+ OobeShellPage.OpenMainWindowCallback(typeof(DashboardPage));
}
ViewModel.LogOpeningSettingsEvent();
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverviewPlaceholder.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverviewPlaceholder.xaml.cs
index 40dcb56aa2..18a18189ab 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverviewPlaceholder.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverviewPlaceholder.xaml.cs
@@ -54,7 +54,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
if (OobeShellPage.OpenMainWindowCallback != null)
{
- OobeShellPage.OpenMainWindowCallback(typeof(GeneralPage));
+ OobeShellPage.OpenMainWindowCallback(typeof(DashboardPage));
}
ViewModel.LogOpeningSettingsEvent();
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml
index 26da2ac853..8b38d7466f 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml
@@ -1,4 +1,4 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml.cs
index 52d74f4e35..449e86cfb5 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml.cs
@@ -89,6 +89,11 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
ModuleName = "CropAndLock",
IsNew = true,
});
+ Modules.Insert((int)PowerToysModules.EnvironmentVariables, new OobePowerToysModule()
+ {
+ ModuleName = "EnvironmentVariables",
+ IsNew = true,
+ });
Modules.Insert((int)PowerToysModules.FancyZones, new OobePowerToysModule()
{
ModuleName = "FancyZones",
@@ -247,6 +252,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
case "Awake": NavigationFrame.Navigate(typeof(OobeAwake)); break;
case "ColorPicker": NavigationFrame.Navigate(typeof(OobeColorPicker)); break;
case "CropAndLock": NavigationFrame.Navigate(typeof(OobeCropAndLock)); break;
+ case "EnvironmentVariables": NavigationFrame.Navigate(typeof(OobeEnvironmentVariables)); break;
case "FancyZones": NavigationFrame.Navigate(typeof(OobeFancyZones)); break;
case "FileLocksmith": NavigationFrame.Navigate(typeof(OobeFileLocksmith)); break;
case "Run": NavigationFrame.Navigate(typeof(OobeRun)); break;
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml
new file mode 100644
index 0000000000..cac07b5bfd
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml
@@ -0,0 +1,446 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs
new file mode 100644
index 0000000000..26f4727441
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs
@@ -0,0 +1,58 @@
+// 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;
+using System.Threading.Tasks;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.OOBE.Views;
+using Microsoft.PowerToys.Settings.UI.ViewModels;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Windows.Storage.Pickers;
+
+namespace Microsoft.PowerToys.Settings.UI.Views
+{
+ ///
+ /// Dashboard Settings Page.
+ ///
+ public sealed partial class DashboardPage : Page, IRefreshablePage
+ {
+ ///
+ /// Gets or sets view model.
+ ///
+ public DashboardViewModel ViewModel { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Dashboard Settings page constructor.
+ ///
+ public DashboardPage()
+ {
+ InitializeComponent();
+ var settingsUtils = new SettingsUtils();
+
+ ViewModel = new DashboardViewModel(
+ SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
+ DataContext = ViewModel;
+ }
+
+ public void RefreshEnabledState()
+ {
+ ViewModel.ModuleEnabledChangedOnSettingsPage();
+ }
+
+ private void SWVersionButtonClicked(object sender, RoutedEventArgs e)
+ {
+ ViewModel.SWVersionButtonClicked();
+ }
+
+ private void SettingsButtonClicked(object sender, RoutedEventArgs e)
+ {
+ ViewModel.SettingsButtonClicked(sender);
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/EnvironmentVariablesPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/EnvironmentVariablesPage.xaml
new file mode 100644
index 0000000000..b8030fa9f5
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/EnvironmentVariablesPage.xaml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/EnvironmentVariablesPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/EnvironmentVariablesPage.xaml.cs
new file mode 100644
index 0000000000..66c80810c3
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/EnvironmentVariablesPage.xaml.cs
@@ -0,0 +1,28 @@
+// 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 Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.ViewModels;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Microsoft.PowerToys.Settings.UI.Views
+{
+ public sealed partial class EnvironmentVariablesPage : Page, IRefreshablePage
+ {
+ private EnvironmentVariablesViewModel ViewModel { get; }
+
+ public EnvironmentVariablesPage()
+ {
+ InitializeComponent();
+ var settingsUtils = new SettingsUtils();
+ ViewModel = new EnvironmentVariablesViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, App.IsElevated);
+ }
+
+ public void RefreshEnabledState()
+ {
+ ViewModel.RefreshEnabledState();
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml.cs
index eabe0ff2db..9d84968206 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml.cs
@@ -73,7 +73,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
}
- private int FilterRemapKeysList(List remapKeysList)
+ public static int FilterRemapKeysList(List remapKeysList)
{
if (remapKeysList != null)
{
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml
index c0b12c8914..b7c3fe7368 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml
@@ -85,10 +85,16 @@
+
+
+ Icon="{ui:FontIcon Glyph=}" />
+
+
+
This overrides the Windows Snap shortcut (Win + arrow) to move windows between zones
- Hold Shift key to activate zones while dragging
+ Hold Shift key to activate zones while dragging a window
+
+
+ Drag windows to activate zones
Show zones on all monitors while dragging a window
@@ -2716,6 +2719,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
Learn more
+
+ Learn more
+
You need to run as administrator to modify these settings.
@@ -2991,6 +2997,78 @@ Activate by holding the key for the character you want to add an accent to, then
Attribution
+
+ Pin a window
+
+
+ Keep your PC awake
+
+
+ Pick a color
+
+
+ Thumbnail
+
+
+ Reparent
+
+
+ Open editor
+
+
+ Right-click on files or directories to show running processes
+
+
+ Find the mouse
+
+
+ Select Image Resizer in the right-click context menu
+
+
+ Highlight clicks
+
+
+ Quickly move the mouse pointer
+
+
+ Draw crosshairs centered on the mouse pointer
+
+
+ Move your cursor across multiple devices
+
+
+ Paste clipboard content without formatting
+
+
+ Quick and easy previewer
+
+
+ Select Power Rename in right-click context menu
+
+
+ A quick launcher
+
+
+ An alternative way to type accented characters
+
+
+ Visualize and edit Windows Registry files
+
+
+ Measure pixels on your screen
+
+
+ Show a help overlay with Windows shortcuts
+
+
+ A convenient way to copy text from anywhere on screen
+
+
+ Activation
+
+
+ Show remappings
+
Quick Accent is an easy way to write letters with accents, like on a smartphone.
@@ -3383,6 +3461,48 @@ Activate by holding the key for the character you want to add an accent to, then
Launch as administrator
+
+ A quick utility for managing environment variables.
+
+
+ Environment Variables
+
+
+ Environment Variables
+
+
+ Enable Environment Variables
+
+
+ Activation
+
+
+ Manage your environment variables
+
+
+ Launch Environment Variables
+
+
+ Launch Environment Variables
+
+
+ Launch Environment Variables
+
+
+ Learn more about Environment Variables
+
+
+ Environment Variables is a quick utility for managing environment variables.
+
+
+ Environment Variables
+
+
+ Needs to be launched as administrator in order to make changes to the system environment variables
+
+
+ Launch as administrator
+
Press duration before showing taskbar icon shortcuts (ms)
ms = milliseconds
@@ -3415,7 +3535,7 @@ Activate by holding the key for the character you want to add an accent to, then
This refers to directly integrating in with Windows
- The system administrator is forcing this setting.
+ This setting is enforced by your System Administrator.
Additional content includes the file header and lines that can't parse
@@ -3703,4 +3823,19 @@ Activate by holding the key for the character you want to add an accent to, then
Administrator: PowerToys Settings
Title of the settings window when running as administrator
+
+ Dashboard
+
+
+ Dashboard
+
+
+ This setting is enforced by your System Administrator.
+
+
+ Disabled modules
+
+
+ Enabled modules
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/ViewModels/DashboardListItem.cs b/src/settings-ui/Settings.UI/ViewModels/DashboardListItem.cs
new file mode 100644
index 0000000000..1efe8257d0
--- /dev/null
+++ b/src/settings-ui/Settings.UI/ViewModels/DashboardListItem.cs
@@ -0,0 +1,69 @@
+// 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.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using Microsoft.UI;
+using Windows.UI;
+
+namespace Microsoft.PowerToys.Settings.UI.ViewModels
+{
+ public class DashboardListItem : INotifyPropertyChanged
+ {
+ private bool _visible;
+ private bool _isEnabled;
+
+ public string Label { get; set; }
+
+ public string Icon { get; set; }
+
+ public string ToolTip { get; set; }
+
+ public string Tag { get; set; }
+
+ public Color AccentColor { get; set; } = Colors.Transparent;
+
+ public bool IsLocked { get; set; }
+
+ public bool IsEnabled
+ {
+ get => _isEnabled;
+ set
+ {
+ if (_isEnabled != value)
+ {
+ _isEnabled = value;
+ OnPropertyChanged();
+ EnabledChangedCallback?.Invoke(this);
+ }
+ }
+ }
+
+ public Action EnabledChangedCallback { get; set; }
+
+ public bool Visible
+ {
+ get => _visible;
+ set
+ {
+ if (_visible != value)
+ {
+ _visible = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ private void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ public ObservableCollection DashboardModuleItems { get; set; } = new ObservableCollection();
+ }
+}
diff --git a/src/settings-ui/Settings.UI/ViewModels/DashboardModuleItem.cs b/src/settings-ui/Settings.UI/ViewModels/DashboardModuleItem.cs
new file mode 100644
index 0000000000..5032abe999
--- /dev/null
+++ b/src/settings-ui/Settings.UI/ViewModels/DashboardModuleItem.cs
@@ -0,0 +1,75 @@
+// 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.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.UI.Xaml;
+using Windows.UI;
+
+namespace Microsoft.PowerToys.Settings.UI.ViewModels
+{
+#pragma warning disable SA1402 // File may only contain a single type
+#pragma warning disable SA1649 // File name should match first type name
+ public class DashboardModuleTextItem : DashboardModuleItem
+ {
+ }
+
+ public class DashboardModuleButtonItem : DashboardModuleItem
+ {
+ public string ButtonTitle { get; set; }
+
+ public bool IsButtonDescriptionVisible { get; set; }
+
+ public string ButtonDescription { get; set; }
+
+ public string ButtonGlyph { get; set; }
+
+ public RoutedEventHandler ButtonClickHandler { get; set; }
+ }
+
+ public class DashboardModuleShortcutItem : DashboardModuleItem
+ {
+ public List