Merge branch 'main' of https://github.com/microsoft/PowerToys into leilzh/v3

This commit is contained in:
Leilei Zhang
2025-11-06 10:19:19 +08:00
116 changed files with 3499 additions and 619 deletions

View File

@@ -7,6 +7,13 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: Please make sure to [search for existing issues](https://github.com/microsoft/PowerToys/issues) before filing a new one! value: Please make sure to [search for existing issues](https://github.com/microsoft/PowerToys/issues) before filing a new one!
- type: markdown
attributes:
value: |
We are aware of the following high-volume issues and are actively working on them. Please check if your issue is one of these before filing a new bug report:
* **PowerToys Run crash related to "Desktop composition is disabled"**: This may appear as `COMException: 0x80263001`. For more details, see issue [#31226](https://github.com/microsoft/PowerToys/issues/31226).
* **PowerToys Run crash with `COMException (0xD0000701)`**: For more details, see issue [#30769](https://github.com/microsoft/PowerToys/issues/30769).
* **PowerToys Run crash with a "Cyclic reference" error**: This `System.InvalidOperationException` is detailed in issue [#36451](https://github.com/microsoft/PowerToys/issues/36451).
- id: version - id: version
type: input type: input
attributes: attributes:

View File

@@ -22,6 +22,7 @@ ADate
ADDSTRING ADDSTRING
ADDUNDORECORD ADDUNDORECORD
ADifferent ADifferent
adjacents
ADMINS ADMINS
adml adml
admx admx
@@ -257,6 +258,7 @@ cominterop
commandnotfound commandnotfound
commandpalette commandpalette
compmgmt compmgmt
COMPOSITIONDISABLED
COMPOSITIONFULL COMPOSITIONFULL
CONFIGW CONFIGW
CONFLICTINGMODIFIERKEY CONFLICTINGMODIFIERKEY
@@ -313,6 +315,8 @@ CURRENTDIR
CURSORINFO CURSORINFO
cursorpos cursorpos
CURSORSHOWING CURSORSHOWING
CURSORWRAP
CursorWrap
customaction customaction
CUSTOMACTIONTEST CUSTOMACTIONTEST
CUSTOMFORMATPLACEHOLDER CUSTOMFORMATPLACEHOLDER

View File

@@ -181,6 +181,7 @@
"PowerToys.MousePointerCrosshairs.dll", "PowerToys.MousePointerCrosshairs.dll",
"PowerToys.MouseJumpUI.dll", "PowerToys.MouseJumpUI.dll",
"PowerToys.MouseJumpUI.exe", "PowerToys.MouseJumpUI.exe",
"PowerToys.CursorWrap.dll",
"PowerToys.MouseWithoutBorders.dll", "PowerToys.MouseWithoutBorders.dll",
"PowerToys.MouseWithoutBorders.exe", "PowerToys.MouseWithoutBorders.exe",

View File

@@ -822,6 +822,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}"
EndProject EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CursorWrap", "src\modules\MouseUtils\CursorWrap\CursorWrap.vcxproj", "{48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3DCCD936-D085-4869-A1DE-CA6A64152C94}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3DCCD936-D085-4869-A1DE-CA6A64152C94}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\modules\LightSwitch\Tests\LightSwitch.UITests\LightSwitch.UITests.csproj", "{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\modules\LightSwitch\Tests\LightSwitch.UITests\LightSwitch.UITests.csproj", "{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}"
@@ -2990,6 +2992,14 @@ Global
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64 {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64 {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64 {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64
{48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Debug|ARM64.ActiveCfg = Debug|ARM64
{48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Debug|ARM64.Build.0 = Debug|ARM64
{48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Debug|x64.ActiveCfg = Debug|x64
{48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Debug|x64.Build.0 = Debug|x64
{48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Release|ARM64.ActiveCfg = Release|ARM64
{48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Release|ARM64.Build.0 = Release|ARM64
{48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Release|x64.ActiveCfg = Release|x64
{48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Release|x64.Build.0 = Release|x64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.ActiveCfg = Debug|ARM64 {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.ActiveCfg = Debug|ARM64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.Build.0 = Debug|ARM64 {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.Build.0 = Debug|ARM64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.Deploy.0 = Debug|ARM64 {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.Deploy.0 = Debug|ARM64
@@ -3351,6 +3361,7 @@ Global
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5} = {322566EF-20DC-43A6-B9F8-616AF942579A}
{3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477} {3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477}
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94} {F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94}
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}

View File

@@ -117,11 +117,6 @@
<ComponentRef Id="powertoys_exe" /> <ComponentRef Id="powertoys_exe" />
<ComponentRef Id="PowerToysStartMenuShortcut" /> <ComponentRef Id="PowerToysStartMenuShortcut" />
<ComponentRef Id="powertoys_per_machine_comp" /> <ComponentRef Id="powertoys_per_machine_comp" />
<?if $(var.PerUser) = "true" ?>
<ComponentRef Id="powertoys_env_path_user" />
<?else?>
<ComponentRef Id="powertoys_env_path_machine" />
<?endif?>
<ComponentRef Id="powertoys_toast_clsid" /> <ComponentRef Id="powertoys_toast_clsid" />
<ComponentRef Id="License_rtf" /> <ComponentRef Id="License_rtf" />
<ComponentRef Id="Notice_md" /> <ComponentRef Id="Notice_md" />

View File

@@ -112,6 +112,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{ {
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredMousePointerCrosshairsEnabledValue()); return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredMousePointerCrosshairsEnabledValue());
} }
GpoRuleConfigured GPOWrapper::GetConfiguredCursorWrapEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredCursorWrapEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredPowerRenameEnabledValue() GpoRuleConfigured GPOWrapper::GetConfiguredPowerRenameEnabledValue()
{ {
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredPowerRenameEnabledValue()); return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredPowerRenameEnabledValue());

View File

@@ -35,6 +35,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredMouseHighlighterEnabledValue(); static GpoRuleConfigured GetConfiguredMouseHighlighterEnabledValue();
static GpoRuleConfigured GetConfiguredMouseJumpEnabledValue(); static GpoRuleConfigured GetConfiguredMouseJumpEnabledValue();
static GpoRuleConfigured GetConfiguredMousePointerCrosshairsEnabledValue(); static GpoRuleConfigured GetConfiguredMousePointerCrosshairsEnabledValue();
static GpoRuleConfigured GetConfiguredCursorWrapEnabledValue();
static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue(); static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue();
static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue(); static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue();
static GpoRuleConfigured GetConfiguredQuickAccentEnabledValue(); static GpoRuleConfigured GetConfiguredQuickAccentEnabledValue();

View File

@@ -38,6 +38,7 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredMouseHighlighterEnabledValue(); static GpoRuleConfigured GetConfiguredMouseHighlighterEnabledValue();
static GpoRuleConfigured GetConfiguredMouseJumpEnabledValue(); static GpoRuleConfigured GetConfiguredMouseJumpEnabledValue();
static GpoRuleConfigured GetConfiguredMousePointerCrosshairsEnabledValue(); static GpoRuleConfigured GetConfiguredMousePointerCrosshairsEnabledValue();
static GpoRuleConfigured GetConfiguredCursorWrapEnabledValue();
static GpoRuleConfigured GetConfiguredMouseWithoutBordersEnabledValue(); static GpoRuleConfigured GetConfiguredMouseWithoutBordersEnabledValue();
static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue(); static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue();
static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue(); static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue();

View File

@@ -12,6 +12,7 @@ namespace ManagedCommon
ColorPicker, ColorPicker,
CmdPal, CmdPal,
CropAndLock, CropAndLock,
CursorWrap,
EnvironmentVariables, EnvironmentVariables,
FancyZones, FancyZones,
FileLocksmith, FileLocksmith,

View File

@@ -59,6 +59,7 @@ struct LogSettings
inline const static std::string mouseHighlighterLoggerName = "mouse-highlighter"; inline const static std::string mouseHighlighterLoggerName = "mouse-highlighter";
inline const static std::string mouseJumpLoggerName = "mouse-jump"; inline const static std::string mouseJumpLoggerName = "mouse-jump";
inline const static std::string mousePointerCrosshairsLoggerName = "mouse-pointer-crosshairs"; inline const static std::string mousePointerCrosshairsLoggerName = "mouse-pointer-crosshairs";
inline const static std::string cursorWrapLoggerName = "cursor-wrap";
inline const static std::string imageResizerLoggerName = "imageresizer"; inline const static std::string imageResizerLoggerName = "imageresizer";
inline const static std::string powerRenameLoggerName = "powerrename"; inline const static std::string powerRenameLoggerName = "powerrename";
inline const static std::string alwaysOnTopLoggerName = "always-on-top"; inline const static std::string alwaysOnTopLoggerName = "always-on-top";

View File

@@ -3,6 +3,7 @@
#include <Windows.h> #include <Windows.h>
#include <optional> #include <optional>
#include <vector> #include <vector>
#include <string>
namespace powertoys_gpo namespace powertoys_gpo
{ {
@@ -51,6 +52,7 @@ namespace powertoys_gpo
const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_HIGHLIGHTER = L"ConfigureEnabledUtilityMouseHighlighter"; const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_HIGHLIGHTER = L"ConfigureEnabledUtilityMouseHighlighter";
const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_JUMP = L"ConfigureEnabledUtilityMouseJump"; const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_JUMP = L"ConfigureEnabledUtilityMouseJump";
const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_POINTER_CROSSHAIRS = L"ConfigureEnabledUtilityMousePointerCrosshairs"; const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_POINTER_CROSSHAIRS = L"ConfigureEnabledUtilityMousePointerCrosshairs";
const std::wstring POLICY_CONFIGURE_ENABLED_CURSOR_WRAP = L"ConfigureEnabledUtilityCursorWrap";
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_RENAME = L"ConfigureEnabledUtilityPowerRename"; const std::wstring POLICY_CONFIGURE_ENABLED_POWER_RENAME = L"ConfigureEnabledUtilityPowerRename";
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER = L"ConfigureEnabledUtilityPowerLauncher"; const std::wstring POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER = L"ConfigureEnabledUtilityPowerLauncher";
const std::wstring POLICY_CONFIGURE_ENABLED_QUICK_ACCENT = L"ConfigureEnabledUtilityQuickAccent"; const std::wstring POLICY_CONFIGURE_ENABLED_QUICK_ACCENT = L"ConfigureEnabledUtilityQuickAccent";
@@ -409,6 +411,11 @@ namespace powertoys_gpo
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_MOUSE_POINTER_CROSSHAIRS); return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_MOUSE_POINTER_CROSSHAIRS);
} }
inline gpo_rule_configured_t getConfiguredCursorWrapEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_CURSOR_WRAP);
}
inline gpo_rule_configured_t getConfiguredPowerRenameEnabledValue() inline gpo_rule_configured_t getConfiguredPowerRenameEnabledValue()
{ {
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_POWER_RENAME); return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_POWER_RENAME);

View File

@@ -14,28 +14,13 @@
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default"> <LinearGradientBrush x:Name="IntelligentUnderlineGradient" StartPoint="0,0.5" EndPoint="1,0.5">
<Color x:Key="AccentGradientColor">#65C8F2</Color> <GradientStop Offset="0.0" Color="#FF0078D4" />
<LinearGradientBrush x:Key="AccentGradientBrush" StartPoint="0,0" EndPoint="1,1"> <GradientStop Offset="0.42" Color="#FF464FEB" />
<GradientStop Offset="0.0" Color="#98EFFE" /> <GradientStop Offset="0.87" Color="#FFD660FF" />
<GradientStop Offset="0.25" Color="#48B1E9" /> <GradientStop Offset="1.0" Color="#FFFEA874" />
<GradientStop Offset="1.0" Color="{StaticResource AccentGradientColor}" /> </LinearGradientBrush>
</LinearGradientBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<Color x:Key="AccentGradientColor">#005FB8</Color>
<LinearGradientBrush x:Key="AccentGradientBrush" StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0.0" Color="#4992C7" />
<GradientStop Offset="0.25" Color="#1353A0" />
<GradientStop Offset="1.0" Color="{StaticResource AccentGradientColor}" />
</LinearGradientBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<Color x:Key="AccentGradientColor">#48B1E9</Color>
<SolidColorBrush x:Key="AccentGradientBrush" Color="{StaticResource AccentGradientColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<x:Double x:Key="ModelSelectorButtonWidth">44</x:Double> <x:Double x:Key="ModelSelectorButtonWidth">44</x:Double>
<Style x:Key="CustomTextBoxStyle" TargetType="TextBox"> <Style x:Key="CustomTextBoxStyle" TargetType="TextBox">
<Setter Property="Foreground" Value="{ThemeResource TextControlForeground}" /> <Setter Property="Foreground" Value="{ThemeResource TextControlForeground}" />
@@ -171,6 +156,19 @@
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Control.IsTemplateFocusTarget="True" Control.IsTemplateFocusTarget="True"
CornerRadius="{TemplateBinding CornerRadius}" /> CornerRadius="{TemplateBinding CornerRadius}" />
<Rectangle
x:Name="FocusHighlighter"
Grid.Row="1"
Grid.RowSpan="1"
Grid.ColumnSpan="4"
Height="2"
Margin="12,0,12,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{StaticResource IntelligentUnderlineGradient}"
RadiusX="1"
RadiusY="1"
Visibility="Collapsed" />
<Grid Grid.Row="1" Width="{StaticResource ModelSelectorButtonWidth}"> <Grid Grid.Row="1" Width="{StaticResource ModelSelectorButtonWidth}">
<ProgressRing <ProgressRing
Width="20" Width="20"
@@ -282,7 +280,10 @@
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="Background"> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlBackgroundFocused}" /> <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlBackgroundFocused}" />
</ObjectAnimationUsingKeyFrames> </ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="BorderBrush"> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusHighlighter" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
</ObjectAnimationUsingKeyFrames>
<!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value> <DiscreteObjectKeyFrame.Value>
<LinearGradientBrush MappingMode="Absolute" StartPoint="0,0" EndPoint="0,2"> <LinearGradientBrush MappingMode="Absolute" StartPoint="0,0" EndPoint="0,2">
@@ -299,7 +300,7 @@
</ObjectAnimationUsingKeyFrames> </ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="BorderThickness"> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="BorderThickness">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlBorderThemeThicknessFocused}" /> <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlBorderThemeThicknessFocused}" />
</ObjectAnimationUsingKeyFrames> </ObjectAnimationUsingKeyFrames>-->
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentElement" Storyboard.TargetProperty="Foreground"> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentElement" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlForegroundFocused}" /> <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlForegroundFocused}" />
</ObjectAnimationUsingKeyFrames> </ObjectAnimationUsingKeyFrames>
@@ -740,8 +741,8 @@
x:Name="LoadingText" x:Name="LoadingText"
x:Uid="LoadingText" x:Uid="LoadingText"
Grid.Row="1" Grid.Row="1"
FontWeight="SemiBold" Margin="4,4,0,0"
Foreground="{ThemeResource AccentGradientBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}" Style="{StaticResource CaptionTextBlockStyle}"
Visibility="Collapsed"> Visibility="Collapsed">
<animations:Implicit.ShowAnimations> <animations:Implicit.ShowAnimations>

View File

@@ -1,11 +1,7 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.IO.Abstractions.TestingHelpers;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Hosts.Tests.Mocks; using Hosts.Tests.Mocks;
using HostsUILib.Helpers; using HostsUILib.Helpers;
@@ -19,6 +15,7 @@ namespace Hosts.FuzzTests
{ {
private static Mock<IUserSettings> _userSettings; private static Mock<IUserSettings> _userSettings;
private static Mock<IElevationHelper> _elevationHelper; private static Mock<IElevationHelper> _elevationHelper;
private static Mock<IBackupManager> _backupManager;
// Case1 Fuzzing method for ValidIPv4 // Case1 Fuzzing method for ValidIPv4
public static void FuzzValidIPv4(ReadOnlySpan<byte> input) public static void FuzzValidIPv4(ReadOnlySpan<byte> input)
@@ -73,9 +70,10 @@ namespace Hosts.FuzzTests
_userSettings = new Mock<IUserSettings>(); _userSettings = new Mock<IUserSettings>();
_elevationHelper = new Mock<IElevationHelper>(); _elevationHelper = new Mock<IElevationHelper>();
_elevationHelper.Setup(m => m.IsElevated).Returns(true); _elevationHelper.Setup(m => m.IsElevated).Returns(true);
_backupManager = new Mock<IBackupManager>();
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
string input = System.Text.Encoding.UTF8.GetString(data); string input = System.Text.Encoding.UTF8.GetString(data);

View File

@@ -30,8 +30,11 @@
<Compile Include="..\HostsUILib\Models\Entry.cs" Link="Entry.cs" /> <Compile Include="..\HostsUILib\Models\Entry.cs" Link="Entry.cs" />
<Compile Include="..\HostsUILib\Models\HostsData.cs" Link="HostsData.cs" /> <Compile Include="..\HostsUILib\Models\HostsData.cs" Link="HostsData.cs" />
<Compile Include="..\HostsUILib\Settings\HostsAdditionalLinesPosition.cs" Link="HostsAdditionalLinesPosition.cs" /> <Compile Include="..\HostsUILib\Settings\HostsAdditionalLinesPosition.cs" Link="HostsAdditionalLinesPosition.cs" />
<Compile Include="..\HostsUILib\Settings\HostsDeleteBackupMode.cs" Link="HostsDeleteBackupMode.cs" />
<Compile Include="..\HostsUILib\Settings\HostsEncoding.cs" Link="HostsEncoding.cs" /> <Compile Include="..\HostsUILib\Settings\HostsEncoding.cs" Link="HostsEncoding.cs" />
<Compile Include="..\HostsUILib\Settings\IUserSettings.cs" Link="IUserSettings.cs" /> <Compile Include="..\HostsUILib\Settings\IUserSettings.cs" Link="IUserSettings.cs" />
<Compile Include="..\HostsUILib\Helpers\IBackupManager.cs" Link="IBackupManager.cs" />
<Compile Include="..\HostsUILib\Helpers\BackupManager.cs" Link="BackupManager.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,156 @@
// 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.TestingHelpers;
using HostsUILib.Helpers;
using HostsUILib.Settings;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace Hosts.Tests
{
[TestClass]
public class BackupManagerTest
{
private const string HostsPath = @"C:\Windows\System32\Drivers\etc\hosts";
private const string BackupPath = @"C:\Backup\hosts";
private const string BackupSearchPattern = $"*_PowerToysBackup_*";
[TestMethod]
public void Hosts_Backup_Not_Executed()
{
var fileSystem = new MockFileSystem();
SetupFiles(fileSystem, true);
var userSettings = new Mock<IUserSettings>();
userSettings.Setup(m => m.BackupHosts).Returns(false);
userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
var backupManager = new BackupManager(fileSystem, userSettings.Object);
backupManager.Create(HostsPath);
Assert.AreEqual(0, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length);
}
[TestMethod]
public void Hosts_Backup_Executed_Once()
{
var fileSystem = new MockFileSystem();
SetupFiles(fileSystem, true);
var userSettings = new Mock<IUserSettings>();
userSettings.Setup(m => m.BackupHosts).Returns(true);
userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
var backupManager = new BackupManager(fileSystem, userSettings.Object);
backupManager.Create(HostsPath);
backupManager.Create(HostsPath);
Assert.AreEqual(1, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length);
var hostsContent = fileSystem.File.ReadAllText(HostsPath);
var backupContent = fileSystem.File.ReadAllText(fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern)[0]);
Assert.AreEqual(hostsContent, backupContent);
}
[DataTestMethod]
[DataRow(-10, -10)]
[DataRow(-10, 0)]
[DataRow(-10, 10)]
[DataRow(0, -10)]
[DataRow(0, 0)]
[DataRow(0, 10)]
[DataRow(10, -10)]
[DataRow(10, 0)]
[DataRow(10, 10)]
public void Hosts_Backups_Delete_Never(int count, int days)
{
var fileSystem = new MockFileSystem();
SetupFiles(fileSystem, false);
var userSettings = new Mock<IUserSettings>();
userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
userSettings.Setup(m => m.DeleteBackupsMode).Returns(HostsDeleteBackupMode.Never);
var backupManager = new BackupManager(fileSystem, userSettings.Object);
backupManager.Delete();
Assert.AreEqual(30, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length);
Assert.AreEqual(31, fileSystem.Directory.GetFiles(BackupPath).Length);
}
[DataTestMethod]
[DataRow(-10, 30)]
[DataRow(0, 30)]
[DataRow(10, 10)]
public void Hosts_Backups_Delete_ByCount(int count, int expectedBackups)
{
var fileSystem = new MockFileSystem();
SetupFiles(fileSystem, false);
var userSettings = new Mock<IUserSettings>();
userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
userSettings.Setup(m => m.DeleteBackupsMode).Returns(HostsDeleteBackupMode.Count);
userSettings.Setup(m => m.DeleteBackupsCount).Returns(count);
var backupManager = new BackupManager(fileSystem, userSettings.Object);
backupManager.Delete();
Assert.AreEqual(expectedBackups, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length);
Assert.AreEqual(expectedBackups + 1, fileSystem.Directory.GetFiles(BackupPath).Length);
}
[DataTestMethod]
[DataRow(-10, -10, 30)]
[DataRow(-10, 0, 30)]
[DataRow(-10, 10, 5)]
[DataRow(0, -10, 30)]
[DataRow(0, 0, 30)]
[DataRow(0, 10, 5)]
[DataRow(10, -10, 30)]
[DataRow(10, 0, 30)]
[DataRow(5, 1, 5)]
[DataRow(1, 15, 10)]
[DataRow(2, 2, 2)]
public void Hosts_Backups_Delete_ByAge(int count, int days, int expectedBackups)
{
var fileSystem = new MockFileSystem();
SetupFiles(fileSystem, false);
var userSettings = new Mock<IUserSettings>();
userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
userSettings.Setup(m => m.DeleteBackupsMode).Returns(HostsDeleteBackupMode.Age);
userSettings.Setup(m => m.DeleteBackupsCount).Returns(count);
userSettings.Setup(m => m.DeleteBackupsDays).Returns(days);
var backupManager = new BackupManager(fileSystem, userSettings.Object);
backupManager.Delete();
Assert.AreEqual(expectedBackups, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length);
Assert.AreEqual(expectedBackups + 1, fileSystem.Directory.GetFiles(BackupPath).Length);
}
private void SetupFiles(MockFileSystem fileSystem, bool hostsOnly)
{
fileSystem.AddDirectory(BackupPath);
fileSystem.AddFile(HostsPath, new MockFileData("HOSTS FILE CONTENT"));
if (hostsOnly)
{
return;
}
var today = new DateTimeOffset(DateTime.Today);
var notBackupData = new MockFileData("NOT A BACKUP")
{
CreationTime = today.AddDays(-100),
};
fileSystem.AddFile(fileSystem.Path.Combine(BackupPath, "hosts_not_a_backup"), notBackupData);
// The first backup is from 5 days ago. There are 30 backups, one for each day.
var offset = 5;
for (var i = 0; i < 30; i++)
{
var backupData = new MockFileData("THIS IS A BACKUP")
{
CreationTime = today.AddDays(-i - offset),
};
fileSystem.AddFile(fileSystem.Path.Combine(BackupPath, $"hosts_PowerToysBackup_{i}"), backupData);
}
}
}
}

View File

@@ -20,8 +20,10 @@ namespace Hosts.Tests
[TestClass] [TestClass]
public class HostsServiceTest public class HostsServiceTest
{ {
private const string BackupPath = @"C:\Backup\hosts";
private static Mock<IUserSettings> _userSettings; private static Mock<IUserSettings> _userSettings;
private static Mock<IElevationHelper> _elevationHelper; private static Mock<IElevationHelper> _elevationHelper;
private static Mock<IBackupManager> _backupManager;
[ClassInitialize] [ClassInitialize]
public static void ClassInitialize(TestContext context) public static void ClassInitialize(TestContext context)
@@ -29,27 +31,7 @@ namespace Hosts.Tests
_userSettings = new Mock<IUserSettings>(); _userSettings = new Mock<IUserSettings>();
_elevationHelper = new Mock<IElevationHelper>(); _elevationHelper = new Mock<IElevationHelper>();
_elevationHelper.Setup(m => m.IsElevated).Returns(true); _elevationHelper.Setup(m => m.IsElevated).Returns(true);
} _backupManager = new Mock<IBackupManager>();
[TestMethod]
public void Hosts_Exists()
{
var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
var result = service.Exists();
Assert.IsTrue(result);
}
[TestMethod]
public void Hosts_Not_Exists()
{
var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
var result = service.Exists();
Assert.IsFalse(result);
} }
[TestMethod] [TestMethod]
@@ -67,7 +49,7 @@ namespace Hosts.Tests
"; ";
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync(); var data = await service.ReadAsync();
@@ -92,7 +74,7 @@ namespace Hosts.Tests
"; ";
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync(); var data = await service.ReadAsync();
@@ -118,7 +100,7 @@ namespace Hosts.Tests
"; ";
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync(); var data = await service.ReadAsync();
@@ -137,7 +119,7 @@ namespace Hosts.Tests
public async Task Empty_Hosts() public async Task Empty_Hosts()
{ {
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
await service.WriteAsync(string.Empty, Enumerable.Empty<Entry>()); await service.WriteAsync(string.Empty, Enumerable.Empty<Entry>());
@@ -168,7 +150,7 @@ namespace Hosts.Tests
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>(); var userSettings = new Mock<IUserSettings>();
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Top); userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Top);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync(); var data = await service.ReadAsync();
@@ -200,7 +182,7 @@ namespace Hosts.Tests
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock<IUserSettings>(); var userSettings = new Mock<IUserSettings>();
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Bottom); userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Bottom);
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync(); var data = await service.ReadAsync();
@@ -224,7 +206,7 @@ namespace Hosts.Tests
"; ";
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync(); var data = await service.ReadAsync();
@@ -241,7 +223,7 @@ namespace Hosts.Tests
var elevationHelper = new Mock<IElevationHelper>(); var elevationHelper = new Mock<IElevationHelper>();
elevationHelper.Setup(m => m.IsElevated).Returns(false); elevationHelper.Setup(m => m.IsElevated).Returns(false);
var service = new HostsService(fileSystem, _userSettings.Object, elevationHelper.Object); var service = new HostsService(fileSystem, _userSettings.Object, elevationHelper.Object, _backupManager.Object);
await Assert.ThrowsExceptionAsync<NotRunningElevatedException>(async () => await service.WriteAsync("# Empty hosts file", Enumerable.Empty<Entry>())); await Assert.ThrowsExceptionAsync<NotRunningElevatedException>(async () => await service.WriteAsync("# Empty hosts file", Enumerable.Empty<Entry>()));
} }
@@ -249,7 +231,7 @@ namespace Hosts.Tests
public async Task Save_ReadOnlyHostsException() public async Task Save_ReadOnlyHostsException()
{ {
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
var hostsFile = new MockFileData(string.Empty) var hostsFile = new MockFileData(string.Empty)
{ {
@@ -265,7 +247,7 @@ namespace Hosts.Tests
public void Remove_ReadOnly_Attribute() public void Remove_ReadOnly_Attribute()
{ {
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
var hostsFile = new MockFileData(string.Empty) var hostsFile = new MockFileData(string.Empty)
{ {
@@ -284,7 +266,7 @@ namespace Hosts.Tests
public async Task Save_Hidden_Hosts() public async Task Save_Hidden_Hosts()
{ {
var fileSystem = new CustomMockFileSystem(); var fileSystem = new CustomMockFileSystem();
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
var hostsFile = new MockFileData(string.Empty) var hostsFile = new MockFileData(string.Empty)
{ {
@@ -316,7 +298,7 @@ namespace Hosts.Tests
var fs = new CustomMockFileSystem(); var fs = new CustomMockFileSystem();
var settings = new Mock<IUserSettings>(); var settings = new Mock<IUserSettings>();
settings.Setup(s => s.NoLeadingSpaces).Returns(true); settings.Setup(s => s.NoLeadingSpaces).Returns(true);
var svc = new HostsService(fs, settings.Object, _elevationHelper.Object); var svc = new HostsService(fs, settings.Object, _elevationHelper.Object, _backupManager.Object);
fs.AddFile(svc.HostsFilePath, new MockFileData(content)); fs.AddFile(svc.HostsFilePath, new MockFileData(content));
var data = await svc.ReadAsync(); var data = await svc.ReadAsync();
@@ -327,5 +309,57 @@ namespace Hosts.Tests
var result = fs.GetFile(svc.HostsFilePath); var result = fs.GetFile(svc.HostsFilePath);
Assert.AreEqual(expected, result.TextContents); Assert.AreEqual(expected, result.TextContents);
} }
[TestMethod]
public async Task Hosts_Backup_Not_Executed()
{
var content =
@"10.1.1.1 host host.local # comment
10.1.1.2 host2 host2.local # another comment
";
var fileSystem = new CustomMockFileSystem();
fileSystem.AddDirectory(BackupPath);
_userSettings.Setup(m => m.BackupHosts).Returns(false);
_userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
var backupManager = new BackupManager(fileSystem, _userSettings.Object);
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, backupManager);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync();
var entries = data.Entries.ToList();
entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false));
await service.WriteAsync(data.AdditionalLines, data.Entries);
Assert.AreEqual(0, fileSystem.Directory.GetFiles(BackupPath).Length);
}
[TestMethod]
public async Task Hosts_Backup_Executed_Once()
{
var content =
@"10.1.1.1 host host.local # comment
10.1.1.2 host2 host2.local # another comment
";
var fileSystem = new CustomMockFileSystem();
_userSettings.Setup(m => m.BackupHosts).Returns(true);
_userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
var backupManager = new BackupManager(fileSystem, _userSettings.Object);
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, backupManager);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync();
var entries = data.Entries.ToList();
entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false));
await service.WriteAsync(data.AdditionalLines, data.Entries);
await service.WriteAsync(data.AdditionalLines, data.Entries);
Assert.AreEqual(1, fileSystem.Directory.GetFiles(BackupPath).Length);
var backupContent = fileSystem.File.ReadAllText(fileSystem.Directory.GetFiles(BackupPath)[0]);
Assert.AreEqual(content, backupContent);
}
} }
} }

View File

@@ -56,6 +56,7 @@ namespace Hosts
{ {
// Core Services // Core Services
services.AddSingleton<IFileSystem, FileSystem>(); services.AddSingleton<IFileSystem, FileSystem>();
services.AddSingleton<IBackupManager, BackupManager>();
services.AddSingleton<IHostsService, HostsService>(); services.AddSingleton<IHostsService, HostsService>();
services.AddSingleton<IUserSettings, Hosts.Settings.UserSettings>(); services.AddSingleton<IUserSettings, Hosts.Settings.UserSettings>();
services.AddSingleton<IElevationHelper, ElevationHelper>(); services.AddSingleton<IElevationHelper, ElevationHelper>();
@@ -74,7 +75,7 @@ namespace Hosts
}). }).
Build(); Build();
var cleanupBackupThread = new Thread(() => var deleteBackupThread = new Thread(() =>
{ {
// Delete old backups only if running elevated // Delete old backups only if running elevated
if (!Host.GetService<IElevationHelper>().IsElevated) if (!Host.GetService<IElevationHelper>().IsElevated)
@@ -84,7 +85,7 @@ namespace Hosts
try try
{ {
Host.GetService<IHostsService>().CleanupBackup(); Host.GetService<IBackupManager>().Delete();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -92,8 +93,8 @@ namespace Hosts
} }
}); });
cleanupBackupThread.IsBackground = true; deleteBackupThread.IsBackground = true;
cleanupBackupThread.Start(); deleteBackupThread.Start();
UnhandledException += App_UnhandledException; UnhandledException += App_UnhandledException;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
@@ -45,17 +45,34 @@ namespace Hosts.Settings
public HostsAdditionalLinesPosition AdditionalLinesPosition { get; private set; } public HostsAdditionalLinesPosition AdditionalLinesPosition { get; private set; }
// Moved from Settings.UI.Library // Moved from Settings.UI.Library
public HostsEncoding Encoding { get; set; } public HostsEncoding Encoding { get; private set; }
public bool BackupHosts { get; private set; }
public string BackupPath { get; private set; }
// Moved from Settings.UI.Library
public HostsDeleteBackupMode DeleteBackupsMode { get; private set; }
public int DeleteBackupsDays { get; private set; }
public int DeleteBackupsCount { get; private set; }
public event EventHandler LoopbackDuplicatesChanged; public event EventHandler LoopbackDuplicatesChanged;
public UserSettings() public UserSettings()
{ {
_settingsUtils = new SettingsUtils(); _settingsUtils = new SettingsUtils();
ShowStartupWarning = true; var defaultSettings = new HostsProperties();
LoopbackDuplicates = false; ShowStartupWarning = defaultSettings.ShowStartupWarning;
AdditionalLinesPosition = HostsAdditionalLinesPosition.Top; LoopbackDuplicates = defaultSettings.LoopbackDuplicates;
Encoding = HostsEncoding.Utf8; AdditionalLinesPosition = (HostsAdditionalLinesPosition)defaultSettings.AdditionalLinesPosition;
Encoding = (HostsEncoding)defaultSettings.Encoding;
BackupHosts = defaultSettings.BackupHosts;
BackupPath = defaultSettings.BackupPath;
DeleteBackupsMode = (HostsDeleteBackupMode)defaultSettings.DeleteBackupsMode;
DeleteBackupsDays = defaultSettings.DeleteBackupsDays;
DeleteBackupsCount = defaultSettings.DeleteBackupsCount;
LoadSettingsFromJson(); LoadSettingsFromJson();
@@ -91,6 +108,11 @@ namespace Hosts.Settings
Encoding = (HostsEncoding)settings.Properties.Encoding; Encoding = (HostsEncoding)settings.Properties.Encoding;
LoopbackDuplicates = settings.Properties.LoopbackDuplicates; LoopbackDuplicates = settings.Properties.LoopbackDuplicates;
NoLeadingSpaces = settings.Properties.NoLeadingSpaces; NoLeadingSpaces = settings.Properties.NoLeadingSpaces;
BackupHosts = settings.Properties.BackupHosts;
BackupPath = settings.Properties.BackupPath;
DeleteBackupsMode = (HostsDeleteBackupMode)settings.Properties.DeleteBackupsMode;
DeleteBackupsDays = settings.Properties.DeleteBackupsDays;
DeleteBackupsCount = settings.Properties.DeleteBackupsCount;
} }
retry = false; retry = false;

View File

@@ -2,7 +2,7 @@
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" /> <Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild"> <Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h HostsModuleInterface.base.rc HostsModuleInterface.rc" /> <Exec Command="powershell -NonInteractive -executionpolicy Unrestricted ..\..\..\..\tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h HostsModuleInterface.base.rc HostsModuleInterface.rc" />
</Target> </Target>
<PropertyGroup Label="Globals"> <PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion> <VCProjectVersion>16.0</VCProjectVersion>
@@ -46,7 +46,7 @@
</PropertyGroup> </PropertyGroup>
<ItemDefinitionGroup> <ItemDefinitionGroup>
<ClCompile> <ClCompile>
<AdditionalIncludeDirectories>$(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile> </ClCompile>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,112 @@
// 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.Globalization;
using System.IO.Abstractions;
using System.Linq;
using HostsUILib.Settings;
namespace HostsUILib.Helpers
{
public class BackupManager : IBackupManager
{
private const string BackupSuffix = "_PowerToysBackup_";
private readonly IFileSystem _fileSystem;
private readonly IUserSettings _userSettings;
private bool _backupDone;
public BackupManager(IFileSystem fileSystem, IUserSettings userSettings)
{
_fileSystem = fileSystem;
_userSettings = userSettings;
}
public void Create(string hostsFilePath)
{
if (_backupDone || !_userSettings.BackupHosts || !_fileSystem.File.Exists(hostsFilePath))
{
return;
}
try
{
if (!_fileSystem.Directory.Exists(_userSettings.BackupPath))
{
_fileSystem.Directory.CreateDirectory(_userSettings.BackupPath);
}
var backupPath = _fileSystem.Path.Combine(_userSettings.BackupPath, $"hosts{BackupSuffix}{DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)}");
_fileSystem.File.Copy(hostsFilePath, backupPath);
_backupDone = true;
}
catch (Exception ex)
{
LoggerInstance.Logger.LogError("Backup failed", ex);
}
}
public void Delete()
{
switch (_userSettings.DeleteBackupsMode)
{
case HostsDeleteBackupMode.Count:
DeleteByCount(_userSettings.DeleteBackupsCount);
break;
case HostsDeleteBackupMode.Age:
DeleteByAge(_userSettings.DeleteBackupsDays, _userSettings.DeleteBackupsCount);
break;
}
}
public void DeleteByCount(int count)
{
if (count < 1)
{
return;
}
var backups = GetAll().OrderByDescending(f => f.CreationTime).Skip(count).ToArray();
DeleteAll(backups);
}
public void DeleteByAge(int days, int count)
{
if (days < 1)
{
return;
}
var backupsEnumerable = GetAll();
if (count > 0)
{
backupsEnumerable = backupsEnumerable.OrderByDescending(f => f.CreationTime).Skip(count);
}
var backups = backupsEnumerable.Where(f => f.CreationTime < DateTime.Now.AddDays(-days)).ToArray();
DeleteAll(backups);
}
private IEnumerable<IFileInfo> GetAll()
{
if (!_fileSystem.Directory.Exists(_userSettings.BackupPath))
{
return [];
}
return _fileSystem.Directory.GetFiles(_userSettings.BackupPath, $"*{BackupSuffix}*").Select(_fileSystem.FileInfo.New);
}
private void DeleteAll(IFileInfo[] files)
{
foreach (var f in files)
{
_fileSystem.File.Delete(f.FullName);
}
}
}
}

View File

@@ -5,7 +5,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq; using System.Linq;
@@ -23,16 +22,15 @@ namespace HostsUILib.Helpers
{ {
public partial class HostsService : IHostsService, IDisposable public partial class HostsService : IHostsService, IDisposable
{ {
private const string _backupSuffix = $"_PowerToysBackup_"; private const int DefaultBufferSize = 4096; // From System.IO.File source code
private const int _defaultBufferSize = 4096; // From System.IO.File source code
private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IUserSettings _userSettings; private readonly IUserSettings _userSettings;
private readonly IElevationHelper _elevationHelper; private readonly IElevationHelper _elevationHelper;
private readonly IFileSystemWatcher _fileSystemWatcher; private readonly IFileSystemWatcher _fileSystemWatcher;
private readonly IBackupManager _backupManager;
private readonly string _hostsFilePath; private readonly string _hostsFilePath;
private bool _backupDone;
private bool _disposed; private bool _disposed;
public string HostsFilePath => _hostsFilePath; public string HostsFilePath => _hostsFilePath;
@@ -44,11 +42,13 @@ namespace HostsUILib.Helpers
public HostsService( public HostsService(
IFileSystem fileSystem, IFileSystem fileSystem,
IUserSettings userSettings, IUserSettings userSettings,
IElevationHelper elevationHelper) IElevationHelper elevationHelper,
IBackupManager backupManager)
{ {
_fileSystem = fileSystem; _fileSystem = fileSystem;
_userSettings = userSettings; _userSettings = userSettings;
_elevationHelper = elevationHelper; _elevationHelper = elevationHelper;
_backupManager = backupManager;
_hostsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"System32\drivers\etc\hosts"); _hostsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"System32\drivers\etc\hosts");
@@ -60,18 +60,13 @@ namespace HostsUILib.Helpers
_fileSystemWatcher.EnableRaisingEvents = true; _fileSystemWatcher.EnableRaisingEvents = true;
} }
public bool Exists()
{
return _fileSystem.File.Exists(HostsFilePath);
}
public async Task<HostsData> ReadAsync() public async Task<HostsData> ReadAsync()
{ {
var entries = new List<Entry>(); var entries = new List<Entry>();
var unparsedBuilder = new StringBuilder(); var unparsedBuilder = new StringBuilder();
var splittedEntries = false; var splittedEntries = false;
if (!Exists()) if (!_fileSystem.File.Exists(HostsFilePath))
{ {
return new HostsData(entries, unparsedBuilder.ToString(), false); return new HostsData(entries, unparsedBuilder.ToString(), false);
} }
@@ -192,15 +187,10 @@ namespace HostsUILib.Helpers
{ {
await _asyncLock.WaitAsync(); await _asyncLock.WaitAsync();
_fileSystemWatcher.EnableRaisingEvents = false; _fileSystemWatcher.EnableRaisingEvents = false;
_backupManager.Create(HostsFilePath);
if (!_backupDone && Exists())
{
_fileSystem.File.Copy(HostsFilePath, HostsFilePath + _backupSuffix + DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture));
_backupDone = true;
}
// FileMode.OpenOrCreate is necessary to prevent UnauthorizedAccessException when the hosts file is hidden // FileMode.OpenOrCreate is necessary to prevent UnauthorizedAccessException when the hosts file is hidden
using var stream = _fileSystem.FileStream.New(HostsFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read, _defaultBufferSize, FileOptions.Asynchronous); using var stream = _fileSystem.FileStream.New(HostsFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read, DefaultBufferSize, FileOptions.Asynchronous);
using var writer = new StreamWriter(stream, Encoding); using var writer = new StreamWriter(stream, Encoding);
foreach (var line in lines) foreach (var line in lines)
{ {
@@ -231,15 +221,6 @@ namespace HostsUILib.Helpers
} }
} }
public void CleanupBackup()
{
Directory.GetFiles(Path.GetDirectoryName(HostsFilePath), $"*{_backupSuffix}*")
.Select(f => new FileInfo(f))
.Where(f => f.CreationTime < DateTime.Now.AddDays(-15))
.ToList()
.ForEach(f => f.Delete());
}
public void OpenHostsFile() public void OpenHostsFile()
{ {
var notepadFallback = false; var notepadFallback = false;

View File

@@ -0,0 +1,13 @@
// 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 HostsUILib.Helpers
{
public interface IBackupManager
{
void Create(string hostsFilePath);
void Delete();
}
}

View File

@@ -22,8 +22,6 @@ namespace HostsUILib.Helpers
Task<bool> PingAsync(string address); Task<bool> PingAsync(string address);
void CleanupBackup();
void OpenHostsFile(); void OpenHostsFile();
void RemoveReadOnlyAttribute(); void RemoveReadOnlyAttribute();

View File

@@ -0,0 +1,13 @@
// 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 HostsUILib.Settings
{
public enum HostsDeleteBackupMode
{
Never = 0,
Count = 1,
Age = 2,
}
}

View File

@@ -16,6 +16,16 @@ namespace HostsUILib.Settings
public HostsEncoding Encoding { get; } public HostsEncoding Encoding { get; }
public bool BackupHosts { get; }
public string BackupPath { get; }
public HostsDeleteBackupMode DeleteBackupsMode { get; }
public int DeleteBackupsDays { get; }
public int DeleteBackupsCount { get; }
event EventHandler LoopbackDuplicatesChanged; event EventHandler LoopbackDuplicatesChanged;
public delegate void OpenSettingsFunction(); public delegate void OpenSettingsFunction();

View File

@@ -11,7 +11,6 @@
#include <logger/logger_settings.h> #include <logger/logger_settings.h>
#include <logger/logger.h> #include <logger/logger.h>
#include <utils/logger_helper.h> #include <utils/logger_helper.h>
#include <LightSwitchServiceObserver.h>
SERVICE_STATUS g_ServiceStatus = {}; SERVICE_STATUS g_ServiceStatus = {};
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr; SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
@@ -20,6 +19,7 @@ extern int g_lastUpdatedDay = -1;
static ScheduleMode prevMode = ScheduleMode::Off; static ScheduleMode prevMode = ScheduleMode::Off;
static std::wstring prevLat, prevLon; static std::wstring prevLat, prevLon;
static int prevMinutes = -1; static int prevMinutes = -1;
static bool lastOverrideStatus = false;
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv); VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl); VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
@@ -164,48 +164,8 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
LightSwitchSettings::instance().InitFileWatcher(); LightSwitchSettings::instance().InitFileWatcher();
LightSwitchServiceObserver observer({ SettingId::LightTime,
SettingId::DarkTime,
SettingId::ScheduleMode,
SettingId::Sunrise_Offset,
SettingId::Sunset_Offset });
HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
auto applyTheme = [](int nowMinutes, int lightMinutes, int darkMinutes, const auto& settings) {
bool isLightActive = (lightMinutes < darkMinutes) ? (nowMinutes >= lightMinutes && nowMinutes < darkMinutes) : (nowMinutes >= lightMinutes || nowMinutes < darkMinutes);
bool isSystemCurrentlyLight = GetCurrentSystemTheme();
bool isAppsCurrentlyLight = GetCurrentAppsTheme();
if (isLightActive)
{
if (settings.changeSystem && !isSystemCurrentlyLight)
{
SetSystemTheme(true);
Logger::info(L"[LightSwitchService] Changing system theme to light mode.");
}
if (settings.changeApps && !isAppsCurrentlyLight)
{
SetAppsTheme(true);
Logger::info(L"[LightSwitchService] Changing apps theme to light mode.");
}
}
else
{
if (settings.changeSystem && isSystemCurrentlyLight)
{
SetSystemTheme(false);
Logger::info(L"[LightSwitchService] Changing system theme to dark mode.");
}
if (settings.changeApps && isAppsCurrentlyLight)
{
SetAppsTheme(false);
Logger::info(L"[LightSwitchService] Changing apps theme to dark mode.");
}
}
};
LightSwitchSettings::instance().LoadSettings(); LightSwitchSettings::instance().LoadSettings();
auto& settings = LightSwitchSettings::instance().settings(); auto& settings = LightSwitchSettings::instance().settings();
@@ -213,22 +173,22 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
GetLocalTime(&st); GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute; int nowMinutes = st.wHour * 60 + st.wMinute;
// Handle initial theme application if necessary
if (settings.scheduleMode != ScheduleMode::Off) if (settings.scheduleMode != ScheduleMode::Off)
{ {
applyTheme(nowMinutes, Logger::info(L"[LightSwitchService] Schedule mode is set to {}. Applying theme if necessary.", settings.scheduleMode);
settings.lightTime + settings.sunrise_offset, LightSwitchSettings::instance().ApplyThemeIfNecessary();
settings.darkTime + settings.sunset_offset,
settings);
Logger::trace(L"[LightSwitchService] Initialized g_lastUpdatedDay = {}", g_lastUpdatedDay);
} }
else else
{ {
Logger::info(L"[LightSwitchService] Schedule mode is OFF - ticker suspended, waiting for manual action or mode change."); Logger::info(L"[LightSwitchService] Schedule mode is set to Off.");
} }
g_lastUpdatedDay = st.wDay; g_lastUpdatedDay = st.wDay;
Logger::info(L"[LightSwitchService] Initializing g_lastUpdatedDay to {}.", g_lastUpdatedDay);
ULONGLONG lastSettingsReload = 0; ULONGLONG lastSettingsReload = 0;
// ticker loop
for (;;) for (;;)
{ {
HANDLE waits[2] = { g_ServiceStopEvent, hParent }; HANDLE waits[2] = { g_ServiceStopEvent, hParent };
@@ -237,13 +197,10 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
const auto& settings = LightSwitchSettings::instance().settings(); const auto& settings = LightSwitchSettings::instance().settings();
bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off); // If the mode is set to Off, suspend the scheduler and avoid extra work
prevMode = settings.scheduleMode;
// ─── Handle "Schedule Off" Mode ─────────────────────────────────────────────
if (settings.scheduleMode == ScheduleMode::Off) if (settings.scheduleMode == ScheduleMode::Off)
{ {
Logger::info(L"[LightSwitchService] Schedule mode OFF - suspending scheduler but keeping service alive."); Logger::info(L"[LightSwitchService] Schedule mode is OFF - suspending scheduler but keeping service alive.");
if (!hManualOverride) if (!hManualOverride)
hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
@@ -283,7 +240,6 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
{ {
Logger::trace(L"[LightSwitchService] Settings change event triggered, reloading settings..."); Logger::trace(L"[LightSwitchService] Settings change event triggered, reloading settings...");
ResetEvent(LightSwitchSettings::instance().GetSettingsChangedEvent()); ResetEvent(LightSwitchSettings::instance().GetSettingsChangedEvent());
LightSwitchSettings::instance().LoadSettings();
const auto& newSettings = LightSwitchSettings::instance().settings(); const auto& newSettings = LightSwitchSettings::instance().settings();
lastSettingsReload = GetTickCount64(); lastSettingsReload = GetTickCount64();
@@ -298,73 +254,150 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
continue; continue;
} }
// ─── Normal Schedule Loop ─────────────────────────────────────────────────── bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off);
prevMode = settings.scheduleMode;
ULONGLONG nowTick = GetTickCount64(); ULONGLONG nowTick = GetTickCount64();
bool recentSettingsReload = (nowTick - lastSettingsReload < 5000); bool recentSettingsReload = (nowTick - lastSettingsReload < 2000);
if (g_lastUpdatedDay != -1) Logger::debug(L"[LightSwitchService] Current g_lastUpdatedDay value = {}.", g_lastUpdatedDay);
// Manual Override Detection Logic
bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
if (manualOverrideActive != lastOverrideStatus)
{ {
bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); Logger::debug(L"[LightSwitchService] Manual override active = {}", manualOverrideActive);
lastOverrideStatus = manualOverrideActive;
}
if (settings.scheduleMode != ScheduleMode::Off && !recentSettingsReload && !scheduleJustEnabled) if (settings.scheduleMode != ScheduleMode::Off && !recentSettingsReload && !scheduleJustEnabled && !manualOverrideActive)
{
bool currentSystemTheme = GetCurrentSystemTheme();
bool currentAppsTheme = GetCurrentAppsTheme();
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
int lightBoundary = 0;
int darkBoundary = 0;
if (settings.scheduleMode == ScheduleMode::SunsetToSunrise)
{ {
Logger::debug(L"[LightSwitchService] Checking if manual override is active..."); lightBoundary = (settings.lightTime + settings.sunrise_offset) % 1440;
bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); darkBoundary = (settings.darkTime + settings.sunset_offset) % 1440;
Logger::debug(L"[LightSwitchService] Manual override active = {}", manualOverrideActive);
if (!manualOverrideActive)
{
bool currentSystemTheme = GetCurrentSystemTheme();
bool currentAppsTheme = GetCurrentAppsTheme();
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
bool shouldBeLight = (settings.lightTime < settings.darkTime) ? (nowMinutes >= settings.lightTime && nowMinutes < settings.darkTime) : (nowMinutes >= settings.lightTime || nowMinutes < settings.darkTime);
Logger::debug(L"[LightSwitchService] shouldBeLight = {}", shouldBeLight);
if ((settings.changeSystem && (currentSystemTheme != shouldBeLight)) ||
(settings.changeApps && (currentAppsTheme != shouldBeLight)))
{
Logger::debug(L"[LightSwitchService] External theme change detected - enabling manual override");
if (!hManualOverride)
{
hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
if (!hManualOverride)
hManualOverride = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
}
if (hManualOverride)
{
SetEvent(hManualOverride);
Logger::info(L"[LightSwitchService] Detected manual theme change outside of LightSwitch. Triggering manual override.");
skipRest = true;
}
}
}
} }
else else
{ {
Logger::debug(L"[LightSwitchService] Skipping external-change detection (schedule off, recent reload, or just enabled)."); lightBoundary = settings.lightTime;
darkBoundary = settings.darkTime;
}
bool shouldBeLight = (lightBoundary < darkBoundary) ? (nowMinutes >= lightBoundary && nowMinutes < darkBoundary) : (nowMinutes >= lightBoundary || nowMinutes < darkBoundary);
Logger::debug(L"[LightSwitchService] shouldBeLight = {}", shouldBeLight);
bool systemMismatch = settings.changeSystem && (currentSystemTheme != shouldBeLight);
bool appsMismatch = settings.changeApps && (currentAppsTheme != shouldBeLight);
if (systemMismatch || appsMismatch)
{
// Make sure this is not because we crossed a boundary
bool crossedBoundary = false;
if (prevMinutes != -1)
{
if (nowMinutes < prevMinutes)
{
// wrapped around midnight
crossedBoundary = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary) ||
(prevMinutes <= darkBoundary || nowMinutes >= darkBoundary);
}
else
{
crossedBoundary = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary) ||
(prevMinutes < darkBoundary && nowMinutes >= darkBoundary);
}
}
if (crossedBoundary)
{
Logger::info(L"[LightSwitchService] Missed boundary detected. Applying theme instead of triggering manual override.");
LightSwitchSettings::instance().ApplyThemeIfNecessary();
}
else
{
Logger::info(L"[LightSwitchService] External {} theme change detected, enabling manual override.",
systemMismatch && appsMismatch ? L"system/app" :
systemMismatch ? L"system" :
L"app");
SetEvent(hManualOverride);
skipRest = true;
}
}
}
else
{
Logger::debug(L"[LightSwitchService] Skipping external-change detection (schedule off, recent reload, or just enabled).");
}
if (hManualOverride)
manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
if (manualOverrideActive)
{
int lightBoundary = (settings.lightTime + settings.sunrise_offset) % 1440;
int darkBoundary = (settings.darkTime + settings.sunset_offset) % 1440;
SYSTEMTIME st;
GetLocalTime(&st);
nowMinutes = st.wHour * 60 + st.wMinute;
bool crossedLight = false;
bool crossedDark = false;
if (prevMinutes != -1)
{
// this means we are in a new day cycle
if (nowMinutes < prevMinutes)
{
crossedLight = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary);
crossedDark = (prevMinutes <= darkBoundary || nowMinutes >= darkBoundary);
}
else
{
crossedLight = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary);
crossedDark = (prevMinutes < darkBoundary && nowMinutes >= darkBoundary);
}
}
if (crossedLight || crossedDark)
{
ResetEvent(hManualOverride);
Logger::info(L"[LightSwitchService] Manual override cleared after crossing schedule boundary.");
}
else
{
Logger::debug(L"[LightSwitchService] Skipping schedule due to manual override");
skipRest = true;
} }
} }
// ─── Apply Schedule Logic ─────────────────────────────────────────────────── // Apply theme if nothing has made us skip
if (!skipRest) if (!skipRest)
{ {
// Next two conditionals check for any updates necessary to the sun times.
bool modeChangedToSunset = (prevMode != settings.scheduleMode && bool modeChangedToSunset = (prevMode != settings.scheduleMode &&
settings.scheduleMode == ScheduleMode::SunsetToSunrise); settings.scheduleMode == ScheduleMode::SunsetToSunrise);
bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude); bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude);
if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise) if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise)
{ {
Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times.");
update_sun_times(settings);
SYSTEMTIME st; SYSTEMTIME st;
GetLocalTime(&st); GetLocalTime(&st);
Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times.");
update_sun_times(settings);
g_lastUpdatedDay = st.wDay; g_lastUpdatedDay = st.wDay;
prevMode = settings.scheduleMode; prevMode = settings.scheduleMode;
prevLat = settings.latitude; prevLat = settings.latitude;
@@ -383,70 +416,23 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary."); Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary.");
} }
// settings after any necessary updates.
LightSwitchSettings::instance().LoadSettings(); LightSwitchSettings::instance().LoadSettings();
const auto& currentSettings = LightSwitchSettings::instance().settings(); const auto& currentSettings = LightSwitchSettings::instance().settings();
wchar_t msg[160]; wchar_t msg[160];
swprintf_s(msg, swprintf_s(msg,
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%d", L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%s",
st.wHour, st.wHour,
st.wMinute, st.wMinute,
currentSettings.lightTime / 60, currentSettings.lightTime / 60,
currentSettings.lightTime % 60, currentSettings.lightTime % 60,
currentSettings.darkTime / 60, currentSettings.darkTime / 60,
currentSettings.darkTime % 60, currentSettings.darkTime % 60,
static_cast<int>(currentSettings.scheduleMode)); ToString(currentSettings.scheduleMode).c_str());
Logger::info(msg); Logger::info(msg);
bool manualOverrideActive = false; LightSwitchSettings::instance().ApplyThemeIfNecessary();
if (hManualOverride)
manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
if (manualOverrideActive)
{
int lightBoundary = (currentSettings.lightTime + currentSettings.sunrise_offset) % 1440;
int darkBoundary = (currentSettings.darkTime + currentSettings.sunset_offset) % 1440;
bool crossedLight = false;
bool crossedDark = false;
if (prevMinutes != -1)
{
if (nowMinutes < prevMinutes)
{
crossedLight = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary);
crossedDark = (prevMinutes <= darkBoundary || nowMinutes >= darkBoundary);
}
else
{
crossedLight = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary);
crossedDark = (prevMinutes < darkBoundary && nowMinutes >= darkBoundary);
}
}
Logger::debug(L"[LightSwitchService] prevMinutes={} nowMinutes={} light={} dark={}",
prevMinutes,
nowMinutes,
lightBoundary,
darkBoundary);
if (crossedLight || crossedDark)
{
ResetEvent(hManualOverride);
Logger::info(L"[LightSwitchService] Manual override cleared after crossing schedule boundary.");
}
else
{
Logger::info(L"[LightSwitchService] Skipping schedule due to manual override");
skipRest = true;
}
}
if (!skipRest)
applyTheme(nowMinutes,
currentSettings.lightTime + currentSettings.sunrise_offset,
currentSettings.darkTime + currentSettings.sunset_offset,
currentSettings);
} }
// ─── Wait For Next Minute Tick Or Stop Event ──────────────────────────────── // ─── Wait For Next Minute Tick Or Stop Event ────────────────────────────────
@@ -480,54 +466,6 @@ cleanup:
return 0; return 0;
} }
void ApplyThemeNow()
{
LightSwitchSettings::instance().LoadSettings();
const auto& settings = LightSwitchSettings::instance().settings();
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
bool shouldBeLight = false;
if (settings.lightTime < settings.darkTime)
shouldBeLight = (nowMinutes >= settings.lightTime && nowMinutes < settings.darkTime);
else
shouldBeLight = (nowMinutes >= settings.lightTime || nowMinutes < settings.darkTime);
bool isSystemCurrentlyLight = GetCurrentSystemTheme();
bool isAppsCurrentlyLight = GetCurrentAppsTheme();
Logger::info(L"[LightSwitchService] Applying (if needed) theme immediately due to schedule change.");
if (shouldBeLight)
{
if (settings.changeSystem && !isSystemCurrentlyLight)
{
SetSystemTheme(true);
Logger::info(L"[LightSwitchService] Changing system theme to light mode.");
}
if (settings.changeApps && !isAppsCurrentlyLight)
{
SetAppsTheme(true);
Logger::info(L"[LightSwitchService] Changing apps theme to light mode.");
}
}
else
{
if (settings.changeSystem && isSystemCurrentlyLight)
{
SetSystemTheme(false);
Logger::info(L"[LightSwitchService] Changing system theme to dark mode.");
}
if (settings.changeApps && isAppsCurrentlyLight)
{
SetAppsTheme(false);
Logger::info(L"[LightSwitchService] Changing apps theme to dark mode.");
}
}
}
int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{ {
if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled) if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)

View File

@@ -74,7 +74,6 @@
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="LightSwitchService.cpp" /> <ClCompile Include="LightSwitchService.cpp" />
<ClCompile Include="LightSwitchServiceObserver.cpp" />
<ClCompile Include="LightSwitchSettings.cpp" /> <ClCompile Include="LightSwitchSettings.cpp" />
<ClCompile Include="SettingsConstants.cpp" /> <ClCompile Include="SettingsConstants.cpp" />
<ClCompile Include="ThemeHelper.cpp" /> <ClCompile Include="ThemeHelper.cpp" />
@@ -85,7 +84,6 @@
<ResourceCompile Include="LightSwitchService.rc" /> <ResourceCompile Include="LightSwitchService.rc" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="LightSwitchServiceObserver.h" />
<ClInclude Include="LightSwitchSettings.h" /> <ClInclude Include="LightSwitchSettings.h" />
<ClInclude Include="SettingsConstants.h" /> <ClInclude Include="SettingsConstants.h" />
<ClInclude Include="SettingsObserver.h" /> <ClInclude Include="SettingsObserver.h" />

View File

@@ -33,9 +33,6 @@
<ClCompile Include="WinHookEventIDs.cpp"> <ClCompile Include="WinHookEventIDs.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="LightSwitchServiceObserver.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="ThemeScheduler.h"> <ClInclude Include="ThemeScheduler.h">
@@ -56,9 +53,6 @@
<ClInclude Include="WinHookEventIDs.h"> <ClInclude Include="WinHookEventIDs.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="LightSwitchServiceObserver.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" /> <Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />

View File

@@ -1,29 +0,0 @@
#include "LightSwitchServiceObserver.h"
#include <logger.h>
#include "LightSwitchSettings.h"
// These are defined elsewhere in your service module (ServiceWorkerThread.cpp)
extern int g_lastUpdatedDay;
void ApplyThemeNow();
void LightSwitchServiceObserver::SettingsUpdate(SettingId id)
{
Logger::info(L"[LightSwitchService] Setting changed: {}", static_cast<int>(id));
g_lastUpdatedDay = -1;
ApplyThemeNow();
}
bool LightSwitchServiceObserver::WantsToBeNotified(SettingId id) const noexcept
{
switch (id)
{
case SettingId::LightTime:
case SettingId::DarkTime:
case SettingId::ScheduleMode:
case SettingId::Sunrise_Offset:
case SettingId::Sunset_Offset:
return true;
default:
return false;
}
}

View File

@@ -1,16 +0,0 @@
#pragma once
#include "SettingsObserver.h"
// The LightSwitchServiceObserver reacts when LightSwitchSettings changes.
class LightSwitchServiceObserver : public SettingsObserver
{
public:
explicit LightSwitchServiceObserver(std::unordered_set<SettingId> observedSettings) :
SettingsObserver(std::move(observedSettings))
{
}
void SettingsUpdate(SettingId id) override;
bool WantsToBeNotified(SettingId id) const noexcept override;
};

View File

@@ -2,7 +2,7 @@
#include <common/utils/json.h> #include <common/utils/json.h>
#include <common/SettingsAPI/settings_helpers.h> #include <common/SettingsAPI/settings_helpers.h>
#include "SettingsObserver.h" #include "SettingsObserver.h"
#include "ThemeHelper.h"
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <WinHookEventIDs.h> #include <WinHookEventIDs.h>
@@ -38,13 +38,80 @@ void LightSwitchSettings::InitFileWatcher()
m_settingsFileWatcher = std::make_unique<FileWatcher>( m_settingsFileWatcher = std::make_unique<FileWatcher>(
GetSettingsFileName(), GetSettingsFileName(),
[this]() { [this]() {
Logger::info(L"[LightSwitchSettings] Settings file changed, signaling event."); using namespace std::chrono;
LoadSettings();
SetEvent(m_settingsChangedEvent); {
std::lock_guard<std::mutex> lock(m_debounceMutex);
m_lastChangeTime = steady_clock::now();
if (m_debouncePending)
return;
m_debouncePending = true;
}
m_debounceThread = std::jthread([this](std::stop_token stop) {
using namespace std::chrono;
while (!stop.stop_requested())
{
std::this_thread::sleep_for(seconds(3));
auto elapsed = steady_clock::now() - m_lastChangeTime;
if (elapsed >= seconds(1))
break;
}
{
std::lock_guard<std::mutex> lock(m_debounceMutex);
m_debouncePending = false;
}
Logger::info(L"[LightSwitchSettings] Settings file stabilized, reloading.");
try
{
LoadSettings();
ApplyThemeIfNecessary();
SetEvent(m_settingsChangedEvent);
}
catch (const std::exception& e)
{
std::wstring wmsg;
wmsg.assign(e.what(), e.what() + strlen(e.what()));
Logger::error(L"[LightSwitchSettings] Exception during debounced reload: {}", wmsg);
}
});
}); });
} }
} }
LightSwitchSettings::~LightSwitchSettings()
{
Logger::info(L"[LightSwitchSettings] Cleaning up settings resources...");
// Stop and join the debounce thread (std::jthread auto-joins, but we can signal stop too)
if (m_debounceThread.joinable())
{
m_debounceThread.request_stop();
}
// Release the file watcher so it closes file handles and background threads
if (m_settingsFileWatcher)
{
m_settingsFileWatcher.reset();
Logger::info(L"[LightSwitchSettings] File watcher stopped.");
}
// Close the Windows event handle
if (m_settingsChangedEvent)
{
CloseHandle(m_settingsChangedEvent);
m_settingsChangedEvent = nullptr;
Logger::info(L"[LightSwitchSettings] Settings changed event closed.");
}
Logger::info(L"[LightSwitchSettings] Cleanup complete.");
}
void LightSwitchSettings::AddObserver(SettingsObserver& observer) void LightSwitchSettings::AddObserver(SettingsObserver& observer)
{ {
m_observers.insert(&observer); m_observers.insert(&observer);
@@ -73,6 +140,7 @@ HANDLE LightSwitchSettings::GetSettingsChangedEvent() const
void LightSwitchSettings::LoadSettings() void LightSwitchSettings::LoadSettings()
{ {
std::lock_guard<std::mutex> guard(m_settingsMutex);
try try
{ {
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues values =
@@ -181,4 +249,49 @@ void LightSwitchSettings::LoadSettings()
{ {
// Keeps defaults if load fails // Keeps defaults if load fails
} }
}
void LightSwitchSettings::ApplyThemeIfNecessary()
{
std::lock_guard<std::mutex> guard(m_settingsMutex);
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
bool shouldBeLight = false;
if (m_settings.lightTime < m_settings.darkTime)
shouldBeLight = (nowMinutes >= m_settings.lightTime && nowMinutes < m_settings.darkTime);
else
shouldBeLight = (nowMinutes >= m_settings.lightTime || nowMinutes < m_settings.darkTime);
bool isSystemCurrentlyLight = GetCurrentSystemTheme();
bool isAppsCurrentlyLight = GetCurrentAppsTheme();
if (shouldBeLight)
{
if (m_settings.changeSystem && !isSystemCurrentlyLight)
{
SetSystemTheme(true);
Logger::info(L"[LightSwitchService] Changing system theme to light mode.");
}
if (m_settings.changeApps && !isAppsCurrentlyLight)
{
SetAppsTheme(true);
Logger::info(L"[LightSwitchService] Changing apps theme to light mode.");
}
}
else
{
if (m_settings.changeSystem && isSystemCurrentlyLight)
{
SetSystemTheme(false);
Logger::info(L"[LightSwitchService] Changing system theme to dark mode.");
}
if (m_settings.changeApps && isAppsCurrentlyLight)
{
SetAppsTheme(false);
Logger::info(L"[LightSwitchService] Changing apps theme to dark mode.");
}
}
} }

View File

@@ -5,7 +5,10 @@
#include <vector> #include <vector>
#include <memory> #include <memory>
#include <windows.h> #include <windows.h>
#include <mutex>
#include <atomic>
#include <thread>
#include <chrono>
#include <common/SettingsAPI/FileWatcher.h> #include <common/SettingsAPI/FileWatcher.h>
#include <common/SettingsAPI/settings_objects.h> #include <common/SettingsAPI/settings_objects.h>
#include <SettingsConstants.h> #include <SettingsConstants.h>
@@ -78,12 +81,13 @@ public:
void RemoveObserver(SettingsObserver& observer); void RemoveObserver(SettingsObserver& observer);
void LoadSettings(); void LoadSettings();
void ApplyThemeIfNecessary();
HANDLE GetSettingsChangedEvent() const; HANDLE GetSettingsChangedEvent() const;
private: private:
LightSwitchSettings(); LightSwitchSettings();
~LightSwitchSettings() = default; ~LightSwitchSettings();
LightSwitchConfig m_settings; LightSwitchConfig m_settings;
std::unique_ptr<FileWatcher> m_settingsFileWatcher; std::unique_ptr<FileWatcher> m_settingsFileWatcher;
@@ -92,4 +96,11 @@ private:
void NotifyObservers(SettingId id) const; void NotifyObservers(SettingId id) const;
HANDLE m_settingsChangedEvent = nullptr; HANDLE m_settingsChangedEvent = nullptr;
mutable std::mutex m_settingsMutex;
// Debounce state
std::atomic_bool m_debouncePending{ false };
std::mutex m_debounceMutex;
std::chrono::steady_clock::time_point m_lastChangeTime{};
std::jthread m_debounceThread;
}; };

View File

@@ -0,0 +1,46 @@
#include <windows.h>
#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"
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", "PowerToys CursorWrap"
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", "CursorWrap"
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", "PowerToys.CursorWrap.dll"
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
STRINGTABLE
BEGIN
IDS_CURSORWRAP_NAME L"CursorWrap"
IDS_CURSORWRAP_DISABLE_WRAP_DURING_DRAG L"Disable wrapping during drag"
END

View File

@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{48a1db8c-5df8-4fb3-9e14-2b67f3f2d8b5}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>CursorWrap</RootNamespace>
<ProjectName>CursorWrap</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
<TargetName>PowerToys.CursorWrap</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="CursorWrapTests.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="CursorWrap.rc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="COMPLETE_REWRITE_SUMMARY.md" />
<None Include="CRITICAL_BUG_ANALYSIS.md" />
<None Include="CURSOR_WRAP_FIX_ANALYSIS.md" />
<None Include="DEBUG_GUIDE.md" />
<None Include="packages.config" />
<None Include="VERTICAL_WRAP_BUG_FIX.md" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,213 @@
#pragma once
#include <vector>
#include <string>
// Test case structure for comprehensive monitor layout testing
struct MonitorTestCase
{
std::string name;
std::string description;
int grid[3][3]; // 3x3 grid representing monitor layout (0 = no monitor, 1-9 = monitor ID)
// Test scenarios to validate
struct TestScenario
{
int sourceMonitor; // Which monitor to start cursor on (1-based)
int edgeDirection; // 0=top, 1=right, 2=bottom, 3=left
int expectedTargetMonitor; // Expected destination monitor (1-based, -1 = wrap within same monitor)
std::string description;
};
std::vector<TestScenario> scenarios;
};
// Comprehensive test cases for all possible 3x3 monitor grid configurations
class CursorWrapTestSuite
{
public:
static std::vector<MonitorTestCase> GetAllTestCases()
{
std::vector<MonitorTestCase> testCases;
// Test Case 1: Single monitor (center)
testCases.push_back({
"Single_Center",
"Single monitor in center position",
{
{0, 0, 0},
{0, 1, 0},
{0, 0, 0}
},
{
{1, 0, -1, "Top edge wraps to bottom of same monitor"},
{1, 1, -1, "Right edge wraps to left of same monitor"},
{1, 2, -1, "Bottom edge wraps to top of same monitor"},
{1, 3, -1, "Left edge wraps to right of same monitor"}
}
});
// Test Case 2: Two monitors horizontal (left + right)
testCases.push_back({
"Dual_Horizontal_Left_Right",
"Two monitors: left + right",
{
{0, 0, 0},
{1, 0, 2},
{0, 0, 0}
},
{
{1, 0, -1, "Monitor 1 top wraps to bottom of monitor 1"},
{1, 1, 2, "Monitor 1 right edge moves to monitor 2 left"},
{1, 2, -1, "Monitor 1 bottom wraps to top of monitor 1"},
{1, 3, -1, "Monitor 1 left edge wraps to right of monitor 1"},
{2, 0, -1, "Monitor 2 top wraps to bottom of monitor 2"},
{2, 1, -1, "Monitor 2 right edge wraps to left of monitor 2"},
{2, 2, -1, "Monitor 2 bottom wraps to top of monitor 2"},
{2, 3, 1, "Monitor 2 left edge moves to monitor 1 right"}
}
});
// Test Case 3: Two monitors vertical (Monitor 2 above Monitor 1) - CORRECTED FOR USER'S SETUP
testCases.push_back({
"Dual_Vertical_2_Above_1",
"Two monitors: Monitor 2 (top) above Monitor 1 (bottom/main)",
{
{0, 2, 0}, // Row 0: Monitor 2 (physically top monitor)
{0, 0, 0}, // Row 1: Empty
{0, 1, 0} // Row 2: Monitor 1 (physically bottom/main monitor)
},
{
// Monitor 1 (bottom/main monitor) tests
{1, 0, 2, "Monitor 1 (bottom) top edge should move to Monitor 2 (top) bottom"},
{1, 1, -1, "Monitor 1 right wraps to left of monitor 1"},
{1, 2, -1, "Monitor 1 bottom wraps to top of monitor 1"},
{1, 3, -1, "Monitor 1 left wraps to right of monitor 1"},
// Monitor 2 (top monitor) tests
{2, 0, -1, "Monitor 2 (top) top wraps to bottom of monitor 2"},
{2, 1, -1, "Monitor 2 right wraps to left of monitor 2"},
{2, 2, 1, "Monitor 2 (top) bottom edge should move to Monitor 1 (bottom) top"},
{2, 3, -1, "Monitor 2 left wraps to right of monitor 2"}
}
});
// Test Case 4: Three monitors L-shape (center + left + top)
testCases.push_back({
"Triple_L_Shape",
"Three monitors in L-shape: center + left + top",
{
{0, 3, 0},
{2, 1, 0},
{0, 0, 0}
},
{
{1, 0, 3, "Monitor 1 top moves to monitor 3 bottom"},
{1, 1, -1, "Monitor 1 right wraps to left of monitor 1"},
{1, 2, -1, "Monitor 1 bottom wraps to top of monitor 1"},
{1, 3, 2, "Monitor 1 left moves to monitor 2 right"},
{2, 0, -1, "Monitor 2 top wraps to bottom of monitor 2"},
{2, 1, 1, "Monitor 2 right moves to monitor 1 left"},
{2, 2, -1, "Monitor 2 bottom wraps to top of monitor 2"},
{2, 3, -1, "Monitor 2 left wraps to right of monitor 2"},
{3, 0, -1, "Monitor 3 top wraps to bottom of monitor 3"},
{3, 1, -1, "Monitor 3 right wraps to left of monitor 3"},
{3, 2, 1, "Monitor 3 bottom moves to monitor 1 top"},
{3, 3, -1, "Monitor 3 left wraps to right of monitor 3"}
}
});
// Test Case 5: Three monitors horizontal (left + center + right)
testCases.push_back({
"Triple_Horizontal",
"Three monitors horizontal: left + center + right",
{
{0, 0, 0},
{1, 2, 3},
{0, 0, 0}
},
{
{1, 0, -1, "Monitor 1 top wraps to bottom"},
{1, 1, 2, "Monitor 1 right moves to monitor 2"},
{1, 2, -1, "Monitor 1 bottom wraps to top"},
{1, 3, -1, "Monitor 1 left wraps to right"},
{2, 0, -1, "Monitor 2 top wraps to bottom"},
{2, 1, 3, "Monitor 2 right moves to monitor 3"},
{2, 2, -1, "Monitor 2 bottom wraps to top"},
{2, 3, 1, "Monitor 2 left moves to monitor 1"},
{3, 0, -1, "Monitor 3 top wraps to bottom"},
{3, 1, -1, "Monitor 3 right wraps to left"},
{3, 2, -1, "Monitor 3 bottom wraps to top"},
{3, 3, 2, "Monitor 3 left moves to monitor 2"}
}
});
// Test Case 6: Three monitors vertical (top + center + bottom)
testCases.push_back({
"Triple_Vertical",
"Three monitors vertical: top + center + bottom",
{
{0, 1, 0},
{0, 2, 0},
{0, 3, 0}
},
{
{1, 0, -1, "Monitor 1 top wraps to bottom"},
{1, 1, -1, "Monitor 1 right wraps to left"},
{1, 2, 2, "Monitor 1 bottom moves to monitor 2"},
{1, 3, -1, "Monitor 1 left wraps to right"},
{2, 0, 1, "Monitor 2 top moves to monitor 1"},
{2, 1, -1, "Monitor 2 right wraps to left"},
{2, 2, 3, "Monitor 2 bottom moves to monitor 3"},
{2, 3, -1, "Monitor 2 left wraps to right"},
{3, 0, 2, "Monitor 3 top moves to monitor 2"},
{3, 1, -1, "Monitor 3 right wraps to left"},
{3, 2, -1, "Monitor 3 bottom wraps to top"},
{3, 3, -1, "Monitor 3 left wraps to right"}
}
});
return testCases;
}
// Helper function to print test case in a readable format
static std::string FormatTestCase(const MonitorTestCase& testCase)
{
std::string result = "Test Case: " + testCase.name + "\n";
result += "Description: " + testCase.description + "\n";
result += "Layout:\n";
for (int row = 0; row < 3; row++)
{
result += " ";
for (int col = 0; col < 3; col++)
{
if (testCase.grid[row][col] == 0)
{
result += ". ";
}
else
{
result += std::to_string(testCase.grid[row][col]) + " ";
}
}
result += "\n";
}
result += "Test Scenarios:\n";
for (const auto& scenario : testCase.scenarios)
{
result += " - " + scenario.description + "\n";
}
return result;
}
// Helper function to validate a specific test case against actual behavior
static bool ValidateTestCase(const MonitorTestCase& testCase)
{
// This would be called with actual CursorWrap instance to validate behavior
// For now, just return true - this would need actual implementation
return true;
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,13 @@
#pragma once
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#include <windows.h>
#include <atomic>
#include <thread>
#include <vector>
// Note: Common includes moved to individual source files due to include path issues
// #include <common/SettingsAPI/settings_helpers.h>
// #include <common/logger/logger.h>
// #include <common/utils/logger_helper.h>

View File

@@ -0,0 +1,4 @@
#pragma once
#define IDS_CURSORWRAP_NAME 101
#define IDS_CURSORWRAP_DISABLE_WRAP_DURING_DRAG 102

View File

@@ -0,0 +1,31 @@
#include "pch.h"
#include "trace.h"
#include "../../../../common/Telemetry/TraceBase.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()
{
TraceLoggingRegister(g_hProvider);
}
void Trace::UnregisterProvider()
{
TraceLoggingUnregister(g_hProvider);
}
void Trace::EnableCursorWrap(const bool enabled) noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"CursorWrap_EnableCursorWrap",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include <common/Telemetry/TraceBase.h>
class Trace : public telemetry::TraceBase
{
public:
static void RegisterProvider();
static void UnregisterProvider();
static void EnableCursorWrap(const bool enabled) noexcept;
};

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>

View File

@@ -11,7 +11,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
/// <summary> /// <summary>
/// Built-in Provider for a top-level command which can quit the application. Invokes the <see cref="QuitCommand"/>, which sends a <see cref="QuitMessage"/>. /// Built-in Provider for a top-level command which can quit the application. Invokes the <see cref="QuitCommand"/>, which sends a <see cref="QuitMessage"/>.
/// </summary> /// </summary>
public partial class BuiltInsCommandProvider : CommandProvider public sealed partial class BuiltInsCommandProvider : CommandProvider
{ {
private readonly OpenSettingsCommand openSettings = new(); private readonly OpenSettingsCommand openSettings = new();
private readonly QuitCommand quitCommand = new(); private readonly QuitCommand quitCommand = new();
@@ -21,7 +21,7 @@ public partial class BuiltInsCommandProvider : CommandProvider
public override ICommandItem[] TopLevelCommands() => public override ICommandItem[] TopLevelCommands() =>
[ [
new CommandItem(openSettings) { Subtitle = Properties.Resources.builtin_open_settings_subtitle }, new CommandItem(openSettings) { },
new CommandItem(_newExtension) { Title = _newExtension.Title, Subtitle = Properties.Resources.builtin_new_extension_subtitle }, new CommandItem(_newExtension) { Title = _newExtension.Title, Subtitle = Properties.Resources.builtin_new_extension_subtitle },
]; ];
@@ -34,7 +34,7 @@ public partial class BuiltInsCommandProvider : CommandProvider
public BuiltInsCommandProvider() public BuiltInsCommandProvider()
{ {
Id = "Core"; Id = "com.microsoft.cmdpal.builtin.core";
DisplayName = Properties.Resources.builtin_display_name; DisplayName = Properties.Resources.builtin_display_name;
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png"); Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
} }

View File

@@ -30,7 +30,12 @@ public partial class MainListPage : DynamicListPage,
{ {
private readonly string[] _specialFallbacks = [ private readonly string[] _specialFallbacks = [
"com.microsoft.cmdpal.builtin.run", "com.microsoft.cmdpal.builtin.run",
"com.microsoft.cmdpal.builtin.calculator" "com.microsoft.cmdpal.builtin.calculator",
"com.microsoft.cmdpal.builtin.system",
"com.microsoft.cmdpal.builtin.core",
"com.microsoft.cmdpal.builtin.websearch",
"com.microsoft.cmdpal.builtin.windowssettings",
"com.microsoft.cmdpal.builtin.datetime",
]; ];
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;

View File

@@ -117,11 +117,8 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="builtin_open_settings_subtitle" xml:space="preserve">
<value>Open Command Palette settings</value>
</data>
<data name="builtin_new_extension_subtitle" xml:space="preserve"> <data name="builtin_new_extension_subtitle" xml:space="preserve">
<value>Creates a project for a new Command Palette extension</value> <value>Generate a new Command Palette extension project</value>
</data> </data>
<data name="builtin_quit_subtitle" xml:space="preserve"> <data name="builtin_quit_subtitle" xml:space="preserve">
<value>Exit Command Palette</value> <value>Exit Command Palette</value>
@@ -157,10 +154,10 @@
<value>Open</value> <value>Open</value>
</data> </data>
<data name="builtin_create_extension_title" xml:space="preserve"> <data name="builtin_create_extension_title" xml:space="preserve">
<value>Create a new extension</value> <value>Create extension</value>
</data> </data>
<data name="builtin_open_settings_name" xml:space="preserve"> <data name="builtin_open_settings_name" xml:space="preserve">
<value>Open Settings</value> <value>Open Command Palette settings</value>
</data> </data>
<data name="builtin_create_extension_success" xml:space="preserve"> <data name="builtin_create_extension_success" xml:space="preserve">
<value>Successfully created your new extension!</value> <value>Successfully created your new extension!</value>

View File

@@ -25,7 +25,7 @@ public class QueryTests : CommandPaletteUnitTestBase
[DataRow("hibernate", "Hibernate")] [DataRow("hibernate", "Hibernate")]
[DataRow("open recycle", "Open Recycle Bin")] [DataRow("open recycle", "Open Recycle Bin")]
[DataRow("empty recycle", "Empty Recycle Bin")] [DataRow("empty recycle", "Empty Recycle Bin")]
[DataRow("uefi", "UEFI Firmware Settings")] [DataRow("uefi", "UEFI firmware settings")]
public void TopLevelPageQueryTest(string input, string matchedTitle) public void TopLevelPageQueryTest(string input, string matchedTitle)
{ {
var settings = new Settings(); var settings = new Settings();
@@ -143,6 +143,6 @@ public class QueryTests : CommandPaletteUnitTestBase
Assert.IsNotNull(result); Assert.IsNotNull(result);
var firstItem = result.FirstOrDefault(); var firstItem = result.FirstOrDefault();
var firstItemIsUefiCommand = firstItem?.Title.Contains("UEFI", StringComparison.OrdinalIgnoreCase) ?? false; var firstItemIsUefiCommand = firstItem?.Title.Contains("UEFI", StringComparison.OrdinalIgnoreCase) ?? false;
Assert.AreEqual(hasCommand, firstItemIsUefiCommand, $"Expected to match (or not match) 'UEFI Firmware Settings' but got '{firstItem?.Title}'"); Assert.AreEqual(hasCommand, firstItemIsUefiCommand, $"Expected to match (or not match) 'UEFI firmware settings' but got '{firstItem?.Title}'");
} }
} }

View File

@@ -41,7 +41,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests
// Assert // Assert
Assert.IsNotNull(provider); Assert.IsNotNull(provider);
Assert.IsNotNull(provider.DisplayName); Assert.IsNotNull(provider.DisplayName);
Assert.AreEqual("DateTime", provider.Id); Assert.AreEqual("com.microsoft.cmdpal.builtin.datetime", provider.Id);
Assert.IsNotNull(provider.Icon); Assert.IsNotNull(provider.Icon);
Assert.IsNotNull(provider.Settings); Assert.IsNotNull(provider.Settings);
} }
@@ -103,7 +103,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests
// Assert // Assert
Assert.IsFalse(string.IsNullOrEmpty(subtitle)); Assert.IsFalse(string.IsNullOrEmpty(subtitle));
Assert.IsTrue(subtitle.Contains("Provides time and date values in different formats")); Assert.IsTrue(subtitle.Contains("Show time and date values in different formats"));
} }
} }
} }

View File

@@ -16,7 +16,7 @@ public class WebSearchCommandProviderTests
var provider = new WebSearchCommandsProvider(); var provider = new WebSearchCommandsProvider();
// Assert // Assert
Assert.AreEqual("WebSearch", provider.Id); Assert.AreEqual("com.microsoft.cmdpal.builtin.websearch", provider.Id);
} }
[TestMethod] [TestMethod]

View File

@@ -49,8 +49,8 @@ public class BasicTests : CommandPaletteTestBase
{ {
SetSearchBox("time and date"); SetSearchBox("time and date");
var searchFileItem = this.Find<NavigationViewItem>("Time and Date"); var searchFileItem = this.Find<NavigationViewItem>("Time and date");
Assert.AreEqual(searchFileItem.Name, "Time and Date"); Assert.AreEqual(searchFileItem.Name, "Time and date");
searchFileItem.DoubleClick(); searchFileItem.DoubleClick();
SetTimeAndDaterExtensionSearchBox("year"); SetTimeAndDaterExtensionSearchBox("year");
@@ -63,8 +63,8 @@ public class BasicTests : CommandPaletteTestBase
{ {
SetSearchBox("Windows Terminal"); SetSearchBox("Windows Terminal");
var searchFileItem = this.Find<NavigationViewItem>("Open Windows Terminal Profiles"); var searchFileItem = this.Find<NavigationViewItem>("Open Windows Terminal profiles");
Assert.AreEqual(searchFileItem.Name, "Open Windows Terminal Profiles"); Assert.AreEqual(searchFileItem.Name, "Open Windows Terminal profiles");
searchFileItem.DoubleClick(); searchFileItem.DoubleClick();
// SetSearchBox("PowerShell"); // SetSearchBox("PowerShell");
@@ -74,10 +74,10 @@ public class BasicTests : CommandPaletteTestBase
[TestMethod] [TestMethod]
public void BasicWindowsSettingsTest() public void BasicWindowsSettingsTest()
{ {
SetSearchBox("Windows Settings"); SetSearchBox("Windows settings");
var searchFileItem = this.Find<NavigationViewItem>("Windows Settings"); var searchFileItem = this.Find<NavigationViewItem>("Windows settings");
Assert.AreEqual(searchFileItem.Name, "Windows Settings"); Assert.AreEqual(searchFileItem.Name, "Windows settings");
searchFileItem.DoubleClick(); searchFileItem.DoubleClick();
SetSearchBox("power"); SetSearchBox("power");

View File

@@ -35,7 +35,6 @@ public partial class AllAppsCommandProvider : CommandProvider
_listItem = new(_page) _listItem = new(_page)
{ {
Subtitle = Resources.search_installed_apps,
MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)], MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)],
}; };

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
// class via a tool like ResGen or Visual Studio. // class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen // To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project. // with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources { internal class Resources {
@@ -61,7 +61,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to All Apps. /// Looks up a localized string similar to Search apps.
/// </summary> /// </summary>
internal static string all_apps { internal static string all_apps {
get { get {
@@ -313,16 +313,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Search installed apps. /// Looks up a localized string similar to Search apps....
/// </summary>
internal static string search_installed_apps {
get {
return ResourceManager.GetString("search_installed_apps", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search installed apps....
/// </summary> /// </summary>
internal static string search_installed_apps_placeholder { internal static string search_installed_apps_placeholder {
get { get {

View File

@@ -124,14 +124,11 @@
<data name="installed_apps" xml:space="preserve"> <data name="installed_apps" xml:space="preserve">
<value>Installed apps</value> <value>Installed apps</value>
</data> </data>
<data name="search_installed_apps" xml:space="preserve">
<value>Search installed apps</value>
</data>
<data name="all_apps" xml:space="preserve"> <data name="all_apps" xml:space="preserve">
<value>All Apps</value> <value>Search apps</value>
</data> </data>
<data name="search_installed_apps_placeholder" xml:space="preserve"> <data name="search_installed_apps_placeholder" xml:space="preserve">
<value>Search installed apps...</value> <value>Search apps...</value>
</data> </data>
<data name="open_path_in_console" xml:space="preserve"> <data name="open_path_in_console" xml:space="preserve">
<value>Open path in console</value> <value>Open path in console</value>

View File

@@ -120,7 +120,6 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
var indexerPage = new IndexerPage(query, _searchEngine, _queryCookie, results); var indexerPage = new IndexerPage(query, _searchEngine, _queryCookie, results);
Title = string.Format(CultureInfo.CurrentCulture, fallbackItemSearchPageTitleCompositeFormat, query); Title = string.Format(CultureInfo.CurrentCulture, fallbackItemSearchPageTitleCompositeFormat, query);
Icon = Icons.FileExplorerIcon; Icon = Icons.FileExplorerIcon;
Subtitle = Resources.Indexer_Subtitle;
Command = indexerPage; Command = indexerPage;
return; return;

View File

@@ -33,7 +33,6 @@ public partial class IndexerCommandsProvider : CommandProvider
new CommandItem(new IndexerPage()) new CommandItem(new IndexerPage())
{ {
Title = Resources.Indexer_Title, Title = Resources.Indexer_Title,
Subtitle = Resources.Indexer_Subtitle,
} }
]; ];
} }

View File

@@ -68,7 +68,6 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
_noSearchEmptyContent = new CommandItem(new NoOpCommand()) _noSearchEmptyContent = new CommandItem(new NoOpCommand())
{ {
Icon = Icon, Icon = Icon,
Title = Resources.Indexer_Subtitle,
Subtitle = Resources.Indexer_NoSearchQueryMessageTip, Subtitle = Resources.Indexer_NoSearchQueryMessageTip,
}; };

View File

@@ -295,15 +295,6 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to Search files on this device.
/// </summary>
internal static string Indexer_Subtitle {
get {
return ResourceManager.GetString("Indexer_Subtitle", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Search files. /// Looks up a localized string similar to Search files.
/// </summary> /// </summary>

View File

@@ -171,9 +171,6 @@
<data name="Indexer_Settings_FallbackCommand_FilePathExist" xml:space="preserve"> <data name="Indexer_Settings_FallbackCommand_FilePathExist" xml:space="preserve">
<value>Only when file path exist</value> <value>Only when file path exist</value>
</data> </data>
<data name="Indexer_Subtitle" xml:space="preserve">
<value>Search files on this device</value>
</data>
<data name="Indexer_Title" xml:space="preserve"> <data name="Indexer_Title" xml:space="preserve">
<value>Search files</value> <value>Search files</value>
</data> </data>

View File

@@ -25,8 +25,7 @@ public partial class RegistryCommandsProvider : CommandProvider
return [ return [
new CommandItem(new RegistryListPage(_settingsManager)) new CommandItem(new RegistryListPage(_settingsManager))
{ {
Title = "Registry", Title = "Browse the Windows registry",
Subtitle = "Navigate the Windows registry",
} }
]; ];
} }

View File

@@ -97,7 +97,7 @@ namespace Microsoft.CmdPal.Ext.Shell.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Executes commands (e.g. &apos;ping&apos;, &apos;cmd&apos;). /// Looks up a localized string similar to Execute system commands like &apos;ping&apos; and &apos;cmd&apos;.
/// </summary> /// </summary>
public static string cmd_plugin_description { public static string cmd_plugin_description {
get { get {

View File

@@ -121,7 +121,7 @@
<value>Run commands</value> <value>Run commands</value>
</data> </data>
<data name="cmd_plugin_description" xml:space="preserve"> <data name="cmd_plugin_description" xml:space="preserve">
<value>Executes commands (e.g. 'ping', 'cmd')</value> <value>Execute system commands like 'ping' and 'cmd'</value>
</data> </data>
<data name="cmd_has_been_executed_times" xml:space="preserve"> <data name="cmd_has_been_executed_times" xml:space="preserve">
<value>this command has been executed {0} times</value> <value>this command has been executed {0} times</value>

View File

@@ -160,7 +160,7 @@ namespace Microsoft.CmdPal.Ext.System {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Adapter Details. /// Looks up a localized string similar to Adapter details.
/// </summary> /// </summary>
public static string Microsoft_plugin_ext_adapter_details { public static string Microsoft_plugin_ext_adapter_details {
get { get {
@@ -169,7 +169,7 @@ namespace Microsoft.CmdPal.Ext.System {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Connection Details. /// Looks up a localized string similar to Connection details.
/// </summary> /// </summary>
public static string Microsoft_plugin_ext_connection_details { public static string Microsoft_plugin_ext_connection_details {
get { get {
@@ -187,7 +187,7 @@ namespace Microsoft.CmdPal.Ext.System {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Open System Command. /// Looks up a localized string similar to Open system command.
/// </summary> /// </summary>
public static string Microsoft_plugin_ext_fallback_display_title { public static string Microsoft_plugin_ext_fallback_display_title {
get { get {
@@ -205,7 +205,7 @@ namespace Microsoft.CmdPal.Ext.System {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to System Commands. /// Looks up a localized string similar to System commands.
/// </summary> /// </summary>
public static string Microsoft_plugin_ext_system_page_name { public static string Microsoft_plugin_ext_system_page_name {
get { get {
@@ -214,7 +214,7 @@ namespace Microsoft.CmdPal.Ext.System {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Windows System Commands. /// Looks up a localized string similar to Windows system commands.
/// </summary> /// </summary>
public static string Microsoft_plugin_ext_system_page_title { public static string Microsoft_plugin_ext_system_page_title {
get { get {
@@ -628,7 +628,7 @@ namespace Microsoft.CmdPal.Ext.System {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to You are about to restart this computer, are you sure?. /// Looks up a localized string similar to You are about to restart this computer. Are you sure?.
/// </summary> /// </summary>
public static string Microsoft_plugin_sys_restart_computer_confirmation { public static string Microsoft_plugin_sys_restart_computer_confirmation {
get { get {
@@ -790,7 +790,7 @@ namespace Microsoft.CmdPal.Ext.System {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to DNS Suffix. /// Looks up a localized string similar to DNS suffix.
/// </summary> /// </summary>
public static string Microsoft_plugin_sys_Suffix { public static string Microsoft_plugin_sys_Suffix {
get { get {
@@ -817,7 +817,7 @@ namespace Microsoft.CmdPal.Ext.System {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to UEFI Firmware Settings. /// Looks up a localized string similar to UEFI firmware settings.
/// </summary> /// </summary>
public static string Microsoft_plugin_sys_uefi { public static string Microsoft_plugin_sys_uefi {
get { get {
@@ -826,7 +826,7 @@ namespace Microsoft.CmdPal.Ext.System {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to You are about to reboot this computer into UEFI Firmware Settings menu, are you sure?. /// Looks up a localized string similar to You are about to reboot this computer into UEFI firmware settings menu, are you sure?.
/// </summary> /// </summary>
public static string Microsoft_plugin_sys_uefi_confirmation { public static string Microsoft_plugin_sys_uefi_confirmation {
get { get {
@@ -835,7 +835,7 @@ namespace Microsoft.CmdPal.Ext.System {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Reboot computer into UEFI Firmware Settings (Requires administrative permissions.). /// Looks up a localized string similar to Reboot computer into UEFI firmware Settings (requires administrative permissions.).
/// </summary> /// </summary>
public static string Microsoft_plugin_sys_uefi_description { public static string Microsoft_plugin_sys_uefi_description {
get { get {

View File

@@ -149,10 +149,10 @@
<value>Shutdown</value> <value>Shutdown</value>
</data> </data>
<data name="Microsoft_plugin_ext_connection_details" xml:space="preserve"> <data name="Microsoft_plugin_ext_connection_details" xml:space="preserve">
<value>Connection Details</value> <value>Connection details</value>
</data> </data>
<data name="Microsoft_plugin_ext_adapter_details" xml:space="preserve"> <data name="Microsoft_plugin_ext_adapter_details" xml:space="preserve">
<value>Adapter Details</value> <value>Adapter details</value>
</data> </data>
<data name="Microsoft_plugin_ext_copy" xml:space="preserve"> <data name="Microsoft_plugin_ext_copy" xml:space="preserve">
<value>Copy to clipboard</value> <value>Copy to clipboard</value>
@@ -161,10 +161,10 @@
<value>Hide disconnected network info</value> <value>Hide disconnected network info</value>
</data> </data>
<data name="Microsoft_plugin_ext_system_page_title" xml:space="preserve"> <data name="Microsoft_plugin_ext_system_page_title" xml:space="preserve">
<value>Windows System Commands</value> <value>Windows system commands</value>
</data> </data>
<data name="Microsoft_plugin_ext_system_page_name" xml:space="preserve"> <data name="Microsoft_plugin_ext_system_page_name" xml:space="preserve">
<value>System Commands</value> <value>System commands</value>
</data> </data>
<data name="Microsoft_plugin_sys_AdapterName" xml:space="preserve"> <data name="Microsoft_plugin_sys_AdapterName" xml:space="preserve">
<value>Adapter name</value> <value>Adapter name</value>
@@ -327,7 +327,7 @@
<comment>This should align to the action in Windows of a restarting your computer.</comment> <comment>This should align to the action in Windows of a restarting your computer.</comment>
</data> </data>
<data name="Microsoft_plugin_sys_restart_computer_confirmation" xml:space="preserve"> <data name="Microsoft_plugin_sys_restart_computer_confirmation" xml:space="preserve">
<value>You are about to restart this computer, are you sure?</value> <value>You are about to restart this computer. Are you sure?</value>
<comment>This should align to the action in Windows of a restarting your computer.</comment> <comment>This should align to the action in Windows of a restarting your computer.</comment>
</data> </data>
<data name="Microsoft_plugin_sys_restart_computer_description" xml:space="preserve"> <data name="Microsoft_plugin_sys_restart_computer_description" xml:space="preserve">
@@ -381,7 +381,7 @@
<value>State</value> <value>State</value>
</data> </data>
<data name="Microsoft_plugin_sys_Suffix" xml:space="preserve"> <data name="Microsoft_plugin_sys_Suffix" xml:space="preserve">
<value>DNS Suffix</value> <value>DNS suffix</value>
</data> </data>
<data name="Microsoft_plugin_sys_TunnelConnection" xml:space="preserve"> <data name="Microsoft_plugin_sys_TunnelConnection" xml:space="preserve">
<value>Tunnel</value> <value>Tunnel</value>
@@ -391,15 +391,15 @@
<comment>Means type like category. Here it means network interface type (ethernet, wifi, ...).</comment> <comment>Means type like category. Here it means network interface type (ethernet, wifi, ...).</comment>
</data> </data>
<data name="Microsoft_plugin_sys_uefi" xml:space="preserve"> <data name="Microsoft_plugin_sys_uefi" xml:space="preserve">
<value>UEFI Firmware Settings</value> <value>UEFI firmware settings</value>
<comment>This should align to the action in Windows Recovery Environment that restart into uefi settings.</comment> <comment>This should align to the action in Windows Recovery Environment that restart into uefi settings.</comment>
</data> </data>
<data name="Microsoft_plugin_sys_uefi_confirmation" xml:space="preserve"> <data name="Microsoft_plugin_sys_uefi_confirmation" xml:space="preserve">
<value>You are about to reboot this computer into UEFI Firmware Settings menu, are you sure?</value> <value>You are about to reboot this computer into UEFI firmware settings menu, are you sure?</value>
<comment>This should align to the action in Windows Recovery Environment that restart into uefi settings.</comment> <comment>This should align to the action in Windows Recovery Environment that restart into uefi settings.</comment>
</data> </data>
<data name="Microsoft_plugin_sys_uefi_description" xml:space="preserve"> <data name="Microsoft_plugin_sys_uefi_description" xml:space="preserve">
<value>Reboot computer into UEFI Firmware Settings (Requires administrative permissions.)</value> <value>Reboot computer into UEFI firmware Settings (requires administrative permissions.)</value>
<comment>This should align to the action in Windows Recovery Environment that restart into uefi settings.</comment> <comment>This should align to the action in Windows Recovery Environment that restart into uefi settings.</comment>
</data> </data>
<data name="Microsoft_plugin_sys_Unknown" xml:space="preserve"> <data name="Microsoft_plugin_sys_Unknown" xml:space="preserve">
@@ -415,7 +415,7 @@
<value>Sleep</value> <value>Sleep</value>
</data> </data>
<data name="Microsoft_plugin_ext_fallback_display_title" xml:space="preserve"> <data name="Microsoft_plugin_ext_fallback_display_title" xml:space="preserve">
<value>Open System Command</value> <value>Execute system commands</value>
</data> </data>
<data name="Microsoft_plugin_sys_RestartShell" xml:space="preserve"> <data name="Microsoft_plugin_sys_RestartShell" xml:space="preserve">
<value>Restart Windows Explorer</value> <value>Restart Windows Explorer</value>

View File

@@ -9,7 +9,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.System; namespace Microsoft.CmdPal.Ext.System;
public partial class SystemCommandExtensionProvider : CommandProvider public sealed partial class SystemCommandExtensionProvider : CommandProvider
{ {
private readonly ICommandItem[] _commands; private readonly ICommandItem[] _commands;
private static readonly SettingsManager _settingsManager = new(); private static readonly SettingsManager _settingsManager = new();
@@ -19,7 +19,7 @@ public partial class SystemCommandExtensionProvider : CommandProvider
public SystemCommandExtensionProvider() public SystemCommandExtensionProvider()
{ {
DisplayName = Resources.Microsoft_plugin_ext_system_page_name; DisplayName = Resources.Microsoft_plugin_ext_system_page_name;
Id = "System"; Id = "com.microsoft.cmdpal.builtin.system";
_commands = [ _commands = [
new CommandItem(Page) new CommandItem(Page)
{ {

View File

@@ -17,11 +17,6 @@ public sealed partial class TimeDateCalculator
/// </summary> /// </summary>
private const string InputDelimiter = "::"; private const string InputDelimiter = "::";
/// <summary>
/// A list of conjunctions that we ignore on search
/// </summary>
private static readonly string[] _conjunctionList = Resources.Microsoft_plugin_timedate_Search_ConjunctionList.Split("; ");
/// <summary> /// <summary>
/// Searches for results /// Searches for results
/// </summary> /// </summary>

View File

@@ -349,7 +349,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Time and Date. /// Looks up a localized string similar to Time and date.
/// </summary> /// </summary>
public static string Microsoft_plugin_timedate_main_page_title { public static string Microsoft_plugin_timedate_main_page_title {
get { get {
@@ -448,7 +448,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Provides time and date values in different formats. /// Looks up a localized string similar to Show time and date values in different formats.
/// </summary> /// </summary>
public static string Microsoft_plugin_timedate_plugin_description { public static string Microsoft_plugin_timedate_plugin_description {
get { get {
@@ -484,7 +484,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Time and Date. /// Looks up a localized string similar to Time and date.
/// </summary> /// </summary>
public static string Microsoft_plugin_timedate_plugin_name { public static string Microsoft_plugin_timedate_plugin_name {
get { get {
@@ -501,15 +501,6 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
} }
} }
/// <summary>
/// Looks up a localized string similar to "for; and; nor; but; or; so".
/// </summary>
public static string Microsoft_plugin_timedate_Search_ConjunctionList {
get {
return ResourceManager.GetString("Microsoft_plugin_timedate_Search_ConjunctionList", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Date and time; Time and Date; Custom format. /// Looks up a localized string similar to Date and time; Time and Date; Custom format.
/// </summary> /// </summary>

View File

@@ -202,7 +202,7 @@
<comment>'UTC' means here 'Universal Time Convention'</comment> <comment>'UTC' means here 'Universal Time Convention'</comment>
</data> </data>
<data name="Microsoft_plugin_timedate_plugin_description" xml:space="preserve"> <data name="Microsoft_plugin_timedate_plugin_description" xml:space="preserve">
<value>Provides time and date values in different formats</value> <value>Show time and date values in different formats</value>
<comment>Do not translate the placeholders like '{0}' because it will be replaced in code.</comment> <comment>Do not translate the placeholders like '{0}' because it will be replaced in code.</comment>
</data> </data>
<data name="Microsoft_plugin_timedate_plugin_description_example_calendarWeek" xml:space="preserve"> <data name="Microsoft_plugin_timedate_plugin_description_example_calendarWeek" xml:space="preserve">
@@ -215,7 +215,7 @@
<value>Time</value> <value>Time</value>
</data> </data>
<data name="Microsoft_plugin_timedate_plugin_name" xml:space="preserve"> <data name="Microsoft_plugin_timedate_plugin_name" xml:space="preserve">
<value>Time and Date</value> <value>Time and date</value>
</data> </data>
<data name="Microsoft_plugin_timedate_Rfc1123" xml:space="preserve"> <data name="Microsoft_plugin_timedate_Rfc1123" xml:space="preserve">
<value>RFC1123</value> <value>RFC1123</value>
@@ -252,10 +252,6 @@
<value>Current Time; Now</value> <value>Current Time; Now</value>
<comment>Don't change order</comment> <comment>Don't change order</comment>
</data> </data>
<data name="Microsoft_plugin_timedate_Search_ConjunctionList" xml:space="preserve">
<value>for; and; nor; but; or; so</value>
<comment>List of conjunctions. We don't add 'yet' because this can be a synonym of 'now' which might be problematic on localized searches.</comment>
</data>
<data name="Microsoft_plugin_timedate_Second" xml:space="preserve"> <data name="Microsoft_plugin_timedate_Second" xml:space="preserve">
<value>Second</value> <value>Second</value>
</data> </data>
@@ -358,7 +354,7 @@
<value>Error: Invalid input</value> <value>Error: Invalid input</value>
</data> </data>
<data name="Microsoft_plugin_timedate_main_page_title" xml:space="preserve"> <data name="Microsoft_plugin_timedate_main_page_title" xml:space="preserve">
<value>Time and Date</value> <value>Time and date</value>
</data> </data>
<data name="Microsoft_plugin_timedate_InvalidInput_SupportedInput" xml:space="preserve"> <data name="Microsoft_plugin_timedate_InvalidInput_SupportedInput" xml:space="preserve">
<value>A {0}format name{0}, a {0}valid date or time value{0}, or a {0}prefixed number{0}. To search for a format in a specific date/time please use the syntax {0}format::date/time/number{0}.{1}Supported prefixes:{2}'{0}u{0}' for Unix Timestamp{2}'{0}ums{0}' for Unix Timestamp in milliseconds{2}'{0}ft{0}' for Windows file time{2}'{0}oa{0}' for OLE Automation Date{2}'{0}exc{0}' for Excel's 1900 date value{2}'{0}exf{0}' for Excel's 1904 date value</value> <value>A {0}format name{0}, a {0}valid date or time value{0}, or a {0}prefixed number{0}. To search for a format in a specific date/time please use the syntax {0}format::date/time/number{0}.{1}Supported prefixes:{2}'{0}u{0}' for Unix Timestamp{2}'{0}ums{0}' for Unix Timestamp in milliseconds{2}'{0}ft{0}' for Windows file time{2}'{0}oa{0}' for OLE Automation Date{2}'{0}exc{0}' for Excel's 1900 date value{2}'{0}exf{0}' for Excel's 1904 date value</value>

View File

@@ -12,7 +12,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.TimeDate; namespace Microsoft.CmdPal.Ext.TimeDate;
public partial class TimeDateCommandsProvider : CommandProvider public sealed partial class TimeDateCommandsProvider : CommandProvider
{ {
private readonly CommandItem _command; private readonly CommandItem _command;
private static readonly SettingsManager _settingsManager = new SettingsManager(); private static readonly SettingsManager _settingsManager = new SettingsManager();
@@ -23,7 +23,7 @@ public partial class TimeDateCommandsProvider : CommandProvider
public TimeDateCommandsProvider() public TimeDateCommandsProvider()
{ {
DisplayName = Resources.Microsoft_plugin_timedate_plugin_name; DisplayName = Resources.Microsoft_plugin_timedate_plugin_name;
Id = "DateTime"; Id = "com.microsoft.cmdpal.builtin.datetime";
_command = new CommandItem(_timeDateExtensionPage) _command = new CommandItem(_timeDateExtensionPage)
{ {
Icon = _timeDateExtensionPage.Icon, Icon = _timeDateExtensionPage.Icon,

View File

@@ -11,7 +11,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch; namespace Microsoft.CmdPal.Ext.WebSearch;
public partial class WebSearchCommandsProvider : CommandProvider public sealed partial class WebSearchCommandsProvider : CommandProvider
{ {
private readonly SettingsManager _settingsManager = new(); private readonly SettingsManager _settingsManager = new();
private readonly FallbackExecuteSearchItem _fallbackItem; private readonly FallbackExecuteSearchItem _fallbackItem;
@@ -22,7 +22,7 @@ public partial class WebSearchCommandsProvider : CommandProvider
public WebSearchCommandsProvider() public WebSearchCommandsProvider()
{ {
Id = "WebSearch"; Id = "com.microsoft.cmdpal.builtin.websearch";
DisplayName = Resources.extension_name; DisplayName = Resources.extension_name;
Icon = Icons.WebSearch; Icon = Icons.WebSearch;
Settings = _settingsManager.Settings; Settings = _settingsManager.Settings;

View File

@@ -124,16 +124,7 @@ namespace Microsoft.CmdPal.Ext.WinGet.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Search for extensions on WinGet. /// Looks up a localized string similar to Add Command Palette extensions from WinGet.
/// </summary>
public static string winget_install_extensions_subtitle {
get {
return ResourceManager.GetString("winget_install_extensions_subtitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Install Command Palette extensions.
/// </summary> /// </summary>
public static string winget_install_extensions_title { public static string winget_install_extensions_title {
get { get {
@@ -205,7 +196,7 @@ namespace Microsoft.CmdPal.Ext.WinGet.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Search WinGet. /// Looks up a localized string similar to Find apps on WinGet.
/// </summary> /// </summary>
public static string winget_page_name { public static string winget_page_name {
get { get {
@@ -268,7 +259,7 @@ namespace Microsoft.CmdPal.Ext.WinGet.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Search for extensions in the Store. /// Looks up a localized string similar to Add Command Palette extensions from the Microsoft Store.
/// </summary> /// </summary>
public static string winget_search_store_title { public static string winget_search_store_title {
get { get {

View File

@@ -127,19 +127,15 @@
<comment></comment> <comment></comment>
</data> </data>
<data name="winget_install_extensions_title" xml:space="preserve"> <data name="winget_install_extensions_title" xml:space="preserve">
<value>Install Command Palette extensions</value> <value>Add Command Palette extensions from WinGet</value>
<comment></comment>
</data>
<data name="winget_install_extensions_subtitle" xml:space="preserve">
<value>Search for extensions on WinGet</value>
<comment></comment> <comment></comment>
</data> </data>
<data name="winget_search_store_title" xml:space="preserve"> <data name="winget_search_store_title" xml:space="preserve">
<value>Search for extensions in the Store</value> <value>Add Command Palette extensions from the Microsoft Store</value>
<comment></comment> <comment></comment>
</data> </data>
<data name="winget_page_name" xml:space="preserve"> <data name="winget_page_name" xml:space="preserve">
<value>Search WinGet</value> <value>Find apps on WinGet</value>
<comment></comment> <comment></comment>
</data> </data>
<data name="winget_create_catalog_error" xml:space="preserve"> <data name="winget_create_catalog_error" xml:space="preserve">

View File

@@ -27,7 +27,6 @@ public partial class WinGetExtensionCommandsProvider : CommandProvider
new WinGetExtensionPage(WinGetExtensionPage.ExtensionsTag) { Title = Properties.Resources.winget_install_extensions_title }) new WinGetExtensionPage(WinGetExtensionPage.ExtensionsTag) { Title = Properties.Resources.winget_install_extensions_title })
{ {
Title = Properties.Resources.winget_install_extensions_title, Title = Properties.Resources.winget_install_extensions_title,
Subtitle = Properties.Resources.winget_install_extensions_subtitle,
}, },
new ListItem( new ListItem(

View File

@@ -70,7 +70,7 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to On all Desktops. /// Looks up a localized string similar to On all desktops.
/// </summary> /// </summary>
public static string VirtualDesktopHelper_AllDesktops { public static string VirtualDesktopHelper_AllDesktops {
get { get {
@@ -196,7 +196,7 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Not Responding. /// Looks up a localized string similar to Not responding.
/// </summary> /// </summary>
public static string windowwalker_NotResponding { public static string windowwalker_NotResponding {
get { get {

View File

@@ -209,7 +209,7 @@
<value>When disabled, windows will be sorted by title</value> <value>When disabled, windows will be sorted by title</value>
</data> </data>
<data name="windowwalker_NotResponding" xml:space="preserve"> <data name="windowwalker_NotResponding" xml:space="preserve">
<value>Not Responding</value> <value>Not responding</value>
</data> </data>
<data name="window_walker_top_level_command_title" xml:space="preserve"> <data name="window_walker_top_level_command_title" xml:space="preserve">
<value>Switch between open windows</value> <value>Switch between open windows</value>
@@ -218,7 +218,7 @@
<value>Switch to</value> <value>Switch to</value>
</data> </data>
<data name="VirtualDesktopHelper_AllDesktops" xml:space="preserve"> <data name="VirtualDesktopHelper_AllDesktops" xml:space="preserve">
<value>On all Desktops</value> <value>On all desktops</value>
</data> </data>
<data name="VirtualDesktopHelper_Desktop" xml:space="preserve"> <data name="VirtualDesktopHelper_Desktop" xml:space="preserve">
<value>Desktop {0}</value> <value>Desktop {0}</value>

View File

@@ -23,8 +23,7 @@ public partial class WindowsServicesCommandsProvider : CommandProvider
return [ return [
new CommandItem(new ServicesListPage()) new CommandItem(new ServicesListPage())
{ {
Title = "Windows Services", Title = "Manage Windows services",
Subtitle = "Manage Windows Services",
} }
]; ];
} }

View File

@@ -10,7 +10,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WindowsSettings; namespace Microsoft.CmdPal.Ext.WindowsSettings;
public partial class WindowsSettingsCommandsProvider : CommandProvider public sealed partial class WindowsSettingsCommandsProvider : CommandProvider
{ {
private readonly CommandItem _searchSettingsListItem; private readonly CommandItem _searchSettingsListItem;
@@ -22,7 +22,7 @@ public partial class WindowsSettingsCommandsProvider : CommandProvider
public WindowsSettingsCommandsProvider() public WindowsSettingsCommandsProvider()
{ {
Id = "Windows.Settings"; Id = "com.microsoft.cmdpal.builtin.windowssettings";
DisplayName = Resources.WindowsSettingsProvider_DisplayName; DisplayName = Resources.WindowsSettingsProvider_DisplayName;
Icon = Icons.WindowsSettingsIcon; Icon = Icons.WindowsSettingsIcon;

View File

@@ -97,7 +97,7 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Open Windows Terminal Profiles. /// Looks up a localized string similar to Open Windows Terminal profiles.
/// </summary> /// </summary>
internal static string list_item_title { internal static string list_item_title {
get { get {

View File

@@ -156,7 +156,7 @@
<value>Settings</value> <value>Settings</value>
</data> </data>
<data name="list_item_title" xml:space="preserve"> <data name="list_item_title" xml:space="preserve">
<value>Open Windows Terminal Profiles</value> <value>Open Windows Terminal profiles</value>
</data> </data>
<data name="preferred_channel" xml:space="preserve"> <data name="preferred_channel" xml:space="preserve">
<value>Preferred channel</value> <value>Preferred channel</value>

View File

@@ -62,10 +62,18 @@
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu> <HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <!-- Only enable Native AOT for Release builds to avoid System.Private.CoreLib.dll version conflicts during development -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<PublishTrimmed>true</PublishTrimmed> <PublishTrimmed>true</PublishTrimmed>
<PublishSingleFile>true</PublishSingleFile> <PublishSingleFile>true</PublishSingleFile>
<PublishAot>true</PublishAot> <PublishAot>true</PublishAot>
</PropertyGroup> </PropertyGroup>
<!-- For Debug builds, use standard JIT compilation -->
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<PublishTrimmed>false</PublishTrimmed>
<PublishSingleFile>false</PublishSingleFile>
<PublishAot>false</PublishAot>
</PropertyGroup>
</Project> </Project>

View File

@@ -74,7 +74,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.UnitTests
var result = main.Object.Query(expectedQuery).FirstOrDefault().SubTitle; var result = main.Object.Query(expectedQuery).FirstOrDefault().SubTitle;
// Assert // Assert
Assert.AreEqual("Reboot computer into UEFI Firmware Settings (Requires administrative permissions.)", result); Assert.AreEqual("Reboot computer into UEFI firmware settings (Requires administrative permissions.)", result);
} }
[TestMethod] [TestMethod]

View File

@@ -19,7 +19,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.Properties {
// class via a tool like ResGen or Visual Studio. // class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen // To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project. // with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources { internal class Resources {
@@ -682,7 +682,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to You are about to reboot this computer into UEFI Firmware Settings menu, are you sure?. /// Looks up a localized string similar to You are about to reboot this computer into UEFI firmware settings menu, are you sure?.
/// </summary> /// </summary>
internal static string Microsoft_plugin_sys_uefi_confirmation { internal static string Microsoft_plugin_sys_uefi_confirmation {
get { get {
@@ -691,7 +691,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Reboot computer into UEFI Firmware Settings (Requires administrative permissions.). /// Looks up a localized string similar to Reboot computer into UEFI firmware settings (Requires administrative permissions.).
/// </summary> /// </summary>
internal static string Microsoft_plugin_sys_uefi_description { internal static string Microsoft_plugin_sys_uefi_description {
get { get {

View File

@@ -362,15 +362,15 @@
<comment>Means type like category. Here it means network interface type (ethernet, wifi, ...).</comment> <comment>Means type like category. Here it means network interface type (ethernet, wifi, ...).</comment>
</data> </data>
<data name="Microsoft_plugin_sys_uefi" xml:space="preserve"> <data name="Microsoft_plugin_sys_uefi" xml:space="preserve">
<value>UEFI Firmware Settings</value> <value>UEFI firmware settings</value>
<comment>This should align to the action in Windows Recovery Environment that restart into uefi settings.</comment> <comment>This should align to the action in Windows Recovery Environment that restart into uefi settings.</comment>
</data> </data>
<data name="Microsoft_plugin_sys_uefi_confirmation" xml:space="preserve"> <data name="Microsoft_plugin_sys_uefi_confirmation" xml:space="preserve">
<value>You are about to reboot this computer into UEFI Firmware Settings menu, are you sure?</value> <value>You are about to reboot this computer into UEFI firmware settings menu, are you sure?</value>
<comment>This should align to the action in Windows Recovery Environment that restart into uefi settings.</comment> <comment>This should align to the action in Windows Recovery Environment that restart into uefi settings.</comment>
</data> </data>
<data name="Microsoft_plugin_sys_uefi_description" xml:space="preserve"> <data name="Microsoft_plugin_sys_uefi_description" xml:space="preserve">
<value>Reboot computer into UEFI Firmware Settings (Requires administrative permissions.)</value> <value>Reboot computer into UEFI firmware settings (Requires administrative permissions.)</value>
<comment>This should align to the action in Windows Recovery Environment that restart into uefi settings.</comment> <comment>This should align to the action in Windows Recovery Environment that restart into uefi settings.</comment>
</data> </data>
<data name="Microsoft_plugin_sys_Unknown" xml:space="preserve"> <data name="Microsoft_plugin_sys_Unknown" xml:space="preserve">

View File

@@ -208,7 +208,7 @@
<comment>'UTC' means here 'Universal Time Convention'</comment> <comment>'UTC' means here 'Universal Time Convention'</comment>
</data> </data>
<data name="Microsoft_plugin_timedate_plugin_description" xml:space="preserve"> <data name="Microsoft_plugin_timedate_plugin_description" xml:space="preserve">
<value>Provides time and date values for the system time or a custom time stamp (e.g.'{0}', '{1}', '{2}', '{3}')</value> <value>Shows time and date values for the system time or a custom time stamp (e.g.'{0}', '{1}', '{2}', '{3}')</value>
<comment>Do not translate the placeholders like '{0}' because it will be replaced in code.</comment> <comment>Do not translate the placeholders like '{0}' because it will be replaced in code.</comment>
</data> </data>
<data name="Microsoft_plugin_timedate_plugin_description_example_calendarWeek" xml:space="preserve"> <data name="Microsoft_plugin_timedate_plugin_description_example_calendarWeek" xml:space="preserve">

View File

@@ -61,7 +61,7 @@ namespace PowerLauncher.Helper
// Many bug reports because users see the "Report problem UI" after "the" crash with System.Runtime.InteropServices.COMException 0xD0000701 or 0x80263001. // Many bug reports because users see the "Report problem UI" after "the" crash with System.Runtime.InteropServices.COMException 0xD0000701 or 0x80263001.
// However, displaying this "Report problem UI" during WPF crashes, especially when DWM composition is changing, is not ideal; some users reported it hangs for up to a minute before the "Report problem UI" appears. // However, displaying this "Report problem UI" during WPF crashes, especially when DWM composition is changing, is not ideal; some users reported it hangs for up to a minute before the "Report problem UI" appears.
// This change modifies the behavior to log the exception instead of showing the "Report problem UI". // This change modifies the behavior to log the exception instead of showing the "Report problem UI".
if (IsDwmCompositionException(e as System.Runtime.InteropServices.COMException)) if (ExceptionHelper.IsRecoverableDwmCompositionException(e as System.Runtime.InteropServices.COMException))
{ {
var logger = LogManager.GetLogger(LoggerName); var logger = LogManager.GetLogger(LoggerName);
logger.Error($"From {(isNotUIThread ? "non" : string.Empty)} UI thread's exception: {ExceptionFormatter.FormatException(e)}"); logger.Error($"From {(isNotUIThread ? "non" : string.Empty)} UI thread's exception: {ExceptionFormatter.FormatException(e)}");
@@ -91,22 +91,5 @@ namespace PowerLauncher.Helper
} }
} }
} }
private static bool IsDwmCompositionException(System.Runtime.InteropServices.COMException comException)
{
if (comException == null)
{
return false;
}
var stackTrace = comException.StackTrace;
if (string.IsNullOrEmpty(stackTrace))
{
return false;
}
// Check for common DWM composition changed patterns in the stack trace
return stackTrace.Contains("DwmCompositionChanged");
}
} }
} }

View File

@@ -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.Runtime.InteropServices;
namespace PowerLauncher.Helper
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Win32 naming conventions")]
internal static class ExceptionHelper
{
private const string PresentationFrameworkExceptionSource = "PresentationFramework";
private const int DWM_E_COMPOSITIONDISABLED = unchecked((int)0x80263001);
// HRESULT for NT STATUS STATUS_MESSAGE_LOST (0xC0000701 | 0x10000000 == 0xD0000701)
private const int STATUS_MESSAGE_LOST_HR = unchecked((int)0xD0000701);
/// <summary>
/// Returns true if the exception is a recoverable DWM composition exception.
/// </summary>
internal static bool IsRecoverableDwmCompositionException(Exception exception)
{
if (exception is not COMException comException)
{
return false;
}
if (comException.HResult is DWM_E_COMPOSITIONDISABLED)
{
return true;
}
if (comException.HResult is STATUS_MESSAGE_LOST_HR && comException.Source == PresentationFrameworkExceptionSource)
{
return true;
}
// Check for common DWM composition changed patterns in the stack trace
var stackTrace = comException.StackTrace;
return !string.IsNullOrEmpty(stackTrace) &&
stackTrace.Contains("DwmCompositionChanged");
}
}
}

View File

@@ -3,13 +3,16 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic; using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using ManagedCommon; using ManagedCommon;
using Microsoft.Win32; using Microsoft.Win32;
using Wox.Infrastructure.Image; using Wox.Infrastructure.Image;
using Wox.Infrastructure.UserSettings; using Wox.Infrastructure.UserSettings;
using Wox.Plugin.Logger;
namespace PowerLauncher.Helper namespace PowerLauncher.Helper
{ {
@@ -20,6 +23,9 @@ namespace PowerLauncher.Helper
private readonly ThemeHelper _themeHelper = new(); private readonly ThemeHelper _themeHelper = new();
private bool _disposed; private bool _disposed;
private CancellationTokenSource _themeUpdateTokenSource;
private const int MaxRetries = 5;
private const int InitialDelayMs = 2000;
public Theme CurrentTheme { get; private set; } public Theme CurrentTheme { get; private set; }
@@ -108,10 +114,80 @@ namespace PowerLauncher.Helper
{ {
Theme newTheme = _themeHelper.DetermineTheme(_settings.Theme); Theme newTheme = _themeHelper.DetermineTheme(_settings.Theme);
_mainWindow.Dispatcher.Invoke(() => // Cancel any existing theme update operation
_themeUpdateTokenSource?.Cancel();
_themeUpdateTokenSource?.Dispose();
_themeUpdateTokenSource = new CancellationTokenSource();
// Start theme update with retry logic in the background
_ = UpdateThemeWithRetryAsync(newTheme, _themeUpdateTokenSource.Token);
}
/// <summary>
/// Applies the theme with retry logic for desktop composition errors.
/// </summary>
/// <param name="theme">The theme to apply.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
private async Task UpdateThemeWithRetryAsync(Theme theme, CancellationToken cancellationToken)
{
var delayMs = 0;
const int maxAttempts = MaxRetries + 1;
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{ {
SetSystemTheme(newTheme); try
}); {
if (delayMs > 0)
{
await Task.Delay(delayMs, cancellationToken);
}
if (cancellationToken.IsCancellationRequested)
{
Log.Debug("Theme update operation was cancelled.", typeof(ThemeManager));
return;
}
await _mainWindow.Dispatcher.InvokeAsync(() =>
{
SetSystemTheme(theme);
});
if (attempt > 1)
{
Log.Info($"Successfully applied theme after {attempt - 1} retry attempt(s).", typeof(ThemeManager));
}
return;
}
catch (COMException ex) when (ExceptionHelper.IsRecoverableDwmCompositionException(ex))
{
switch (attempt)
{
case 1:
Log.Warn($"Desktop composition is disabled (HRESULT: 0x{ex.HResult:X}). Scheduling retries for theme update.", typeof(ThemeManager));
delayMs = InitialDelayMs;
break;
case < maxAttempts:
Log.Warn($"Retry {attempt - 1}/{MaxRetries} failed: Desktop composition still disabled. Retrying in {delayMs * 2}ms...", typeof(ThemeManager));
delayMs *= 2;
break;
default:
Log.Exception($"Failed to set theme after {MaxRetries} retry attempts. Desktop composition remains disabled.", ex, typeof(ThemeManager));
break;
}
}
catch (OperationCanceledException)
{
Log.Debug("Theme update operation was cancelled.", typeof(ThemeManager));
return;
}
catch (Exception ex)
{
Log.Exception($"Unexpected error during theme update (attempt {attempt}/{maxAttempts}): {ex.Message}", ex, typeof(ThemeManager));
throw;
}
}
} }
public void Dispose() public void Dispose()
@@ -130,6 +206,8 @@ namespace PowerLauncher.Helper
if (disposing) if (disposing)
{ {
SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged; SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
_themeUpdateTokenSource?.Cancel();
_themeUpdateTokenSource?.Dispose();
} }
_disposed = true; _disposed = true;

View File

@@ -62,6 +62,12 @@ namespace Peek.UI
[ObservableProperty] [ObservableProperty]
private IFileSystemItem? _currentItem; private IFileSystemItem? _currentItem;
/// <summary>
/// Work around missing navigation when peeking from CLI.
/// TODO: Implement navigation when peeking from CLI.
/// </summary>
private bool _isFromCli;
partial void OnCurrentItemChanged(IFileSystemItem? value) partial void OnCurrentItemChanged(IFileSystemItem? value)
{ {
WindowTitle = value != null WindowTitle = value != null
@@ -129,7 +135,24 @@ namespace Peek.UI
NavigationThrottleTimer.Interval = TimeSpan.FromMilliseconds(NavigationThrottleDelayMs); NavigationThrottleTimer.Interval = TimeSpan.FromMilliseconds(NavigationThrottleDelayMs);
} }
public void Initialize(HWND foregroundWindowHandle) public void Initialize(SelectedItem selectedItem)
{
switch (selectedItem)
{
case SelectedItemByPath selectedItemByPath:
InitializeFromCli(selectedItemByPath.Path);
break;
case SelectedItemByWindowHandle selectedItemByWindowHandle:
InitializeFromExplorer(selectedItemByWindowHandle.WindowHandle);
break;
default:
throw new NotImplementedException($"Invalid type of selected item: '{selectedItem.GetType().FullName}'");
}
}
private void InitializeFromExplorer(HWND foregroundWindowHandle)
{ {
try try
{ {
@@ -141,10 +164,20 @@ namespace Peek.UI
} }
_currentIndex = DisplayIndex = 0; _currentIndex = DisplayIndex = 0;
_isFromCli = false;
CurrentItem = (Items != null && Items.Count > 0) ? Items[0] : null; CurrentItem = (Items != null && Items.Count > 0) ? Items[0] : null;
} }
private void InitializeFromCli(string path)
{
// TODO: implement navigation
_isFromCli = true;
Items = null;
_currentIndex = DisplayIndex = 0;
CurrentItem = new FileItem(path, Path.GetFileName(path));
}
public void Uninitialize() public void Uninitialize()
{ {
_currentIndex = DisplayIndex = 0; _currentIndex = DisplayIndex = 0;
@@ -153,6 +186,7 @@ namespace Peek.UI
Items = null; Items = null;
_navigationDirection = NavigationDirection.Forwards; _navigationDirection = NavigationDirection.Forwards;
IsErrorVisible = false; IsErrorVisible = false;
_isFromCli = false;
} }
public void AttemptPreviousNavigation() => Navigate(NavigationDirection.Backwards); public void AttemptPreviousNavigation() => Navigate(NavigationDirection.Backwards);
@@ -166,6 +200,12 @@ namespace Peek.UI
return; return;
} }
// TODO: implement navigation.
if (_isFromCli)
{
return;
}
if (Items == null || Items.Count == _deletedItemIndexes.Count) if (Items == null || Items.Count == _deletedItemIndexes.Count)
{ {
_currentIndex = DisplayIndex = 0; _currentIndex = DisplayIndex = 0;

View File

@@ -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 Peek.UI.Models
{
public abstract class SelectedItem
{
public abstract bool Matches(string? path);
}
}

View File

@@ -0,0 +1,23 @@
// 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 Peek.UI.Models
{
public class SelectedItemByPath : SelectedItem
{
public string Path { get; }
public SelectedItemByPath(string path)
{
Path = path;
}
public override bool Matches(string? path)
{
return string.Equals(Path, path, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@@ -0,0 +1,34 @@
// 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 Peek.UI.Extensions;
using Peek.UI.Helpers;
using Windows.Win32.Foundation;
namespace Peek.UI.Models
{
public class SelectedItemByWindowHandle : SelectedItem
{
public HWND WindowHandle { get; }
public SelectedItemByWindowHandle(HWND windowHandle)
{
WindowHandle = windowHandle;
}
public override bool Matches(string? path)
{
var selectedItems = FileExplorerHelper.GetSelectedItems(WindowHandle);
var selectedItemsCount = selectedItems?.GetCount() ?? 0;
if (selectedItems == null || selectedItemsCount == 0 || selectedItemsCount > 1)
{
return false;
}
var fileExplorerSelectedItemPath = selectedItems.GetItemAt(0).ToIFileSystemItem().Path;
var currentItemPath = path;
return fileExplorerSelectedItemPath != null && currentItemPath != null && fileExplorerSelectedItemPath != currentItemPath;
}
}
}

View File

@@ -3,16 +3,19 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.IO;
using System.Threading;
using ManagedCommon; using ManagedCommon;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.PowerToys.Telemetry; using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls.Primitives;
using Peek.Common; using Peek.Common;
using Peek.FilePreviewer; using Peek.FilePreviewer;
using Peek.FilePreviewer.Models; using Peek.FilePreviewer.Models;
using Peek.FilePreviewer.Previewers; using Peek.FilePreviewer.Previewers;
using Peek.UI.Models;
using Peek.UI.Native; using Peek.UI.Native;
using Peek.UI.Telemetry.Events; using Peek.UI.Telemetry.Events;
using Peek.UI.Views; using Peek.UI.Views;
@@ -23,7 +26,7 @@ namespace Peek.UI
/// <summary> /// <summary>
/// Provides application-specific behavior to supplement the default Application class. /// Provides application-specific behavior to supplement the default Application class.
/// </summary> /// </summary>
public partial class App : Application, IApp public partial class App : Application, IApp, IDisposable
{ {
public static int PowerToysPID { get; set; } public static int PowerToysPID { get; set; }
@@ -36,6 +39,10 @@ namespace Peek.UI
private MainWindow? Window { get; set; } private MainWindow? Window { get; set; }
private bool _disposed;
private SelectedItem? _selectedItem;
private bool _launchedFromCli;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="App"/> class. /// Initializes a new instance of the <see cref="App"/> class.
/// Initializes the singleton application object. This is the first line of authored code /// Initializes the singleton application object. This is the first line of authored code
@@ -52,22 +59,22 @@ namespace Peek.UI
InitializeComponent(); InitializeComponent();
Logger.InitializeLogger("\\Peek\\Logs"); Logger.InitializeLogger("\\Peek\\Logs");
Host = Microsoft.Extensions.Hosting.Host. Host = Microsoft.Extensions.Hosting.Host
CreateDefaultBuilder(). .CreateDefaultBuilder()
UseContentRoot(AppContext.BaseDirectory). .UseContentRoot(AppContext.BaseDirectory)
ConfigureServices((context, services) => .ConfigureServices((context, services) =>
{ {
// Core Services // Core Services
services.AddTransient<NeighboringItemsQuery>(); services.AddTransient<NeighboringItemsQuery>();
services.AddSingleton<IUserSettings, UserSettings>(); services.AddSingleton<IUserSettings, UserSettings>();
services.AddSingleton<IPreviewSettings, PreviewSettings>(); services.AddSingleton<IPreviewSettings, PreviewSettings>();
// Views and ViewModels // Views and ViewModels
services.AddTransient<TitleBar>(); services.AddTransient<TitleBar>();
services.AddTransient<FilePreview>(); services.AddTransient<FilePreview>();
services.AddTransient<MainWindowViewModel>(); services.AddTransient<MainWindowViewModel>();
}). })
Build(); .Build();
UnhandledException += App_UnhandledException; UnhandledException += App_UnhandledException;
} }
@@ -99,6 +106,7 @@ namespace Peek.UI
var cmdArgs = Environment.GetCommandLineArgs(); var cmdArgs = Environment.GetCommandLineArgs();
if (cmdArgs?.Length > 1) if (cmdArgs?.Length > 1)
{ {
// Check if the last argument is a PowerToys Runner PID
if (int.TryParse(cmdArgs[^1], out int powerToysRunnerPid)) if (int.TryParse(cmdArgs[^1], out int powerToysRunnerPid))
{ {
RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () => RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () =>
@@ -107,9 +115,25 @@ namespace Peek.UI
Environment.Exit(0); Environment.Exit(0);
}); });
} }
else
{
// Command line argument is a file path - activate Peek with that file
string filePath = cmdArgs[^1];
if (File.Exists(filePath) || Directory.Exists(filePath))
{
_selectedItem = new SelectedItemByPath(filePath);
_launchedFromCli = true;
OnShowPeek();
return;
}
else
{
Logger.LogError($"Command line argument is not a valid file or directory: {filePath}");
}
}
} }
NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnPeekHotkey); NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnShowPeek);
NativeEventWaiter.WaitForEventLoop(Constants.TerminatePeekEvent(), () => NativeEventWaiter.WaitForEventLoop(Constants.TerminatePeekEvent(), () =>
{ {
ShellPreviewHandlerPreviewer.ReleaseHandlerFactories(); ShellPreviewHandlerPreviewer.ReleaseHandlerFactories();
@@ -126,11 +150,16 @@ namespace Peek.UI
/// <summary> /// <summary>
/// Handle Peek hotkey /// Handle Peek hotkey
/// </summary> /// </summary>
private void OnPeekHotkey() private void OnShowPeek()
{ {
// Need to read the foreground HWND before activating Peek to avoid focus stealing // null means explorer, not null means CLI
// Foreground HWND must always be Explorer or Desktop if (_selectedItem == null)
var foregroundWindowHandle = Windows.Win32.PInvoke_PeekUI.GetForegroundWindow(); {
// Need to read the foreground HWND before activating Peek to avoid focus stealing
// Foreground HWND must always be Explorer or Desktop
var foregroundWindowHandle = Windows.Win32.PInvoke_PeekUI.GetForegroundWindow();
_selectedItem = new SelectedItemByWindowHandle(foregroundWindowHandle);
}
bool firstActivation = false; bool firstActivation = false;
@@ -140,7 +169,38 @@ namespace Peek.UI
Window = new MainWindow(); Window = new MainWindow();
} }
Window.Toggle(firstActivation, foregroundWindowHandle); Window.Toggle(firstActivation, _selectedItem, _launchedFromCli);
_launchedFromCli = false;
_selectedItem = null;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// dispose managed state (managed objects)
}
// free unmanaged resources (unmanaged objects) and override finalizer
// set large fields to null
_disposed = true;
}
}
/* // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~App()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// } */
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
} }
} }
} }

View File

@@ -18,6 +18,7 @@ using Peek.FilePreviewer.Models;
using Peek.FilePreviewer.Previewers; using Peek.FilePreviewer.Previewers;
using Peek.UI.Extensions; using Peek.UI.Extensions;
using Peek.UI.Helpers; using Peek.UI.Helpers;
using Peek.UI.Models;
using Peek.UI.Telemetry.Events; using Peek.UI.Telemetry.Events;
using Windows.Foundation; using Windows.Foundation;
using WinUIEx; using WinUIEx;
@@ -38,6 +39,7 @@ namespace Peek.UI
/// dialog is open at a time. /// dialog is open at a time.
/// </summary> /// </summary>
private bool _isDeleteInProgress; private bool _isDeleteInProgress;
private bool _exitAfterClose;
public MainWindow() public MainWindow()
{ {
@@ -116,12 +118,17 @@ namespace Peek.UI
/// <summary> /// <summary>
/// Toggling the window visibility and querying files when necessary. /// Toggling the window visibility and querying files when necessary.
/// </summary> /// </summary>
public void Toggle(bool firstActivation, Windows.Win32.Foundation.HWND foregroundWindowHandle) public void Toggle(bool firstActivation, SelectedItem selectedItem, bool exitAfterClose)
{ {
if (exitAfterClose)
{
_exitAfterClose = true;
}
if (firstActivation) if (firstActivation)
{ {
Activate(); Activate();
Initialize(foregroundWindowHandle); Initialize(selectedItem);
return; return;
} }
@@ -132,9 +139,9 @@ namespace Peek.UI
if (AppWindow.IsVisible) if (AppWindow.IsVisible)
{ {
if (IsNewSingleSelectedItem(foregroundWindowHandle)) if (IsNewSingleSelectedItem(selectedItem))
{ {
Initialize(foregroundWindowHandle); Initialize(selectedItem);
Activate(); // Brings existing window into focus in case it was previously minimized Activate(); // Brings existing window into focus in case it was previously minimized
} }
else else
@@ -144,7 +151,7 @@ namespace Peek.UI
} }
else else
{ {
Initialize(foregroundWindowHandle); Initialize(selectedItem);
} }
} }
@@ -182,12 +189,12 @@ namespace Peek.UI
Uninitialize(); Uninitialize();
} }
private void Initialize(Windows.Win32.Foundation.HWND foregroundWindowHandle) private void Initialize(SelectedItem selectedItem)
{ {
var bootTime = new System.Diagnostics.Stopwatch(); var bootTime = new System.Diagnostics.Stopwatch();
bootTime.Start(); bootTime.Start();
ViewModel.Initialize(foregroundWindowHandle); ViewModel.Initialize(selectedItem);
ViewModel.ScalingFactor = this.GetMonitorScale(); ViewModel.ScalingFactor = this.GetMonitorScale();
this.Content.KeyUp += Content_KeyUp; this.Content.KeyUp += Content_KeyUp;
@@ -207,6 +214,11 @@ namespace Peek.UI
this.Content.KeyUp -= Content_KeyUp; this.Content.KeyUp -= Content_KeyUp;
ShellPreviewHandlerPreviewer.ReleaseHandlerFactories(); ShellPreviewHandlerPreviewer.ReleaseHandlerFactories();
if (_exitAfterClose)
{
Environment.Exit(0);
}
} }
/// <summary> /// <summary>
@@ -272,20 +284,11 @@ namespace Peek.UI
Uninitialize(); Uninitialize();
} }
private bool IsNewSingleSelectedItem(Windows.Win32.Foundation.HWND foregroundWindowHandle) private bool IsNewSingleSelectedItem(SelectedItem selectedItem)
{ {
try try
{ {
var selectedItems = FileExplorerHelper.GetSelectedItems(foregroundWindowHandle); return selectedItem.Matches(ViewModel.CurrentItem?.Path);
var selectedItemsCount = selectedItems?.GetCount() ?? 0;
if (selectedItems == null || selectedItemsCount == 0 || selectedItemsCount > 1)
{
return false;
}
var fileExplorerSelectedItemPath = selectedItems.GetItemAt(0).ToIFileSystemItem().Path;
var currentItemPath = ViewModel.CurrentItem?.Path;
return fileExplorerSelectedItemPath != null && currentItemPath != null && fileExplorerSelectedItemPath != currentItemPath;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -35,6 +35,31 @@ namespace
ensure_ignored_conflict_properties_shape(obj); ensure_ignored_conflict_properties_shape(obj);
return obj; return obj;
} }
DashboardSortOrder parse_dashboard_sort_order(const json::JsonObject& obj, DashboardSortOrder fallback)
{
if (json::has(obj, L"dashboard_sort_order", json::JsonValueType::Number))
{
const auto raw_value = static_cast<int>(obj.GetNamedNumber(L"dashboard_sort_order", static_cast<double>(static_cast<int>(fallback))));
return raw_value == static_cast<int>(DashboardSortOrder::ByStatus) ? DashboardSortOrder::ByStatus : DashboardSortOrder::Alphabetical;
}
if (json::has(obj, L"dashboard_sort_order", json::JsonValueType::String))
{
const auto raw = obj.GetNamedString(L"dashboard_sort_order");
if (raw == L"ByStatus")
{
return DashboardSortOrder::ByStatus;
}
if (raw == L"Alphabetical")
{
return DashboardSortOrder::Alphabetical;
}
}
return fallback;
}
} }
// TODO: would be nice to get rid of these globals, since they're basically cached json settings // TODO: would be nice to get rid of these globals, since they're basically cached json settings
@@ -46,6 +71,7 @@ static bool download_updates_automatically = true;
static bool show_whats_new_after_updates = true; static bool show_whats_new_after_updates = true;
static bool enable_experimentation = true; static bool enable_experimentation = true;
static bool enable_warnings_elevated_apps = true; static bool enable_warnings_elevated_apps = true;
static DashboardSortOrder dashboard_sort_order = DashboardSortOrder::Alphabetical;
static json::JsonObject ignored_conflict_properties = create_default_ignored_conflict_properties(); static json::JsonObject ignored_conflict_properties = create_default_ignored_conflict_properties();
json::JsonObject GeneralSettings::to_json() json::JsonObject GeneralSettings::to_json()
@@ -75,6 +101,7 @@ json::JsonObject GeneralSettings::to_json()
result.SetNamedValue(L"download_updates_automatically", json::value(downloadUpdatesAutomatically)); result.SetNamedValue(L"download_updates_automatically", json::value(downloadUpdatesAutomatically));
result.SetNamedValue(L"show_whats_new_after_updates", json::value(showWhatsNewAfterUpdates)); result.SetNamedValue(L"show_whats_new_after_updates", json::value(showWhatsNewAfterUpdates));
result.SetNamedValue(L"enable_experimentation", json::value(enableExperimentation)); result.SetNamedValue(L"enable_experimentation", json::value(enableExperimentation));
result.SetNamedValue(L"dashboard_sort_order", json::value(static_cast<int>(dashboardSortOrder)));
result.SetNamedValue(L"is_admin", json::value(isAdmin)); result.SetNamedValue(L"is_admin", json::value(isAdmin));
result.SetNamedValue(L"enable_warnings_elevated_apps", json::value(enableWarningsElevatedApps)); result.SetNamedValue(L"enable_warnings_elevated_apps", json::value(enableWarningsElevatedApps));
result.SetNamedValue(L"theme", json::value(theme)); result.SetNamedValue(L"theme", json::value(theme));
@@ -99,6 +126,7 @@ json::JsonObject load_general_settings()
show_whats_new_after_updates = loaded.GetNamedBoolean(L"show_whats_new_after_updates", true); show_whats_new_after_updates = loaded.GetNamedBoolean(L"show_whats_new_after_updates", true);
enable_experimentation = loaded.GetNamedBoolean(L"enable_experimentation", true); enable_experimentation = loaded.GetNamedBoolean(L"enable_experimentation", true);
enable_warnings_elevated_apps = loaded.GetNamedBoolean(L"enable_warnings_elevated_apps", true); enable_warnings_elevated_apps = loaded.GetNamedBoolean(L"enable_warnings_elevated_apps", true);
dashboard_sort_order = parse_dashboard_sort_order(loaded, dashboard_sort_order);
if (json::has(loaded, L"ignored_conflict_properties", json::JsonValueType::Object)) if (json::has(loaded, L"ignored_conflict_properties", json::JsonValueType::Object))
{ {
@@ -128,6 +156,7 @@ GeneralSettings get_general_settings()
.downloadUpdatesAutomatically = download_updates_automatically && is_user_admin, .downloadUpdatesAutomatically = download_updates_automatically && is_user_admin,
.showWhatsNewAfterUpdates = show_whats_new_after_updates, .showWhatsNewAfterUpdates = show_whats_new_after_updates,
.enableExperimentation = enable_experimentation, .enableExperimentation = enable_experimentation,
.dashboardSortOrder = dashboard_sort_order,
.theme = settings_theme, .theme = settings_theme,
.systemTheme = WindowsColors::is_dark_mode() ? L"dark" : L"light", .systemTheme = WindowsColors::is_dark_mode() ? L"dark" : L"light",
.powerToysVersion = get_product_version(), .powerToysVersion = get_product_version(),
@@ -159,6 +188,7 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save)
show_whats_new_after_updates = general_configs.GetNamedBoolean(L"show_whats_new_after_updates", true); show_whats_new_after_updates = general_configs.GetNamedBoolean(L"show_whats_new_after_updates", true);
enable_experimentation = general_configs.GetNamedBoolean(L"enable_experimentation", true); enable_experimentation = general_configs.GetNamedBoolean(L"enable_experimentation", true);
dashboard_sort_order = parse_dashboard_sort_order(general_configs, dashboard_sort_order);
// apply_general_settings is called by the runner's WinMain, so we can just force the run at startup gpo rule here. // apply_general_settings is called by the runner's WinMain, so we can just force the run at startup gpo rule here.
auto gpo_run_as_startup = powertoys_gpo::getConfiguredRunAtStartupValue(); auto gpo_run_as_startup = powertoys_gpo::getConfiguredRunAtStartupValue();

View File

@@ -2,6 +2,12 @@
#include <common/utils/json.h> #include <common/utils/json.h>
enum class DashboardSortOrder
{
Alphabetical = 0,
ByStatus = 1,
};
struct GeneralSettings struct GeneralSettings
{ {
bool isStartupEnabled; bool isStartupEnabled;
@@ -16,6 +22,7 @@ struct GeneralSettings
bool downloadUpdatesAutomatically; bool downloadUpdatesAutomatically;
bool showWhatsNewAfterUpdates; bool showWhatsNewAfterUpdates;
bool enableExperimentation; bool enableExperimentation;
DashboardSortOrder dashboardSortOrder;
std::wstring theme; std::wstring theme;
std::wstring systemTheme; std::wstring systemTheme;
std::wstring powerToysVersion; std::wstring powerToysVersion;

View File

@@ -161,6 +161,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
L"PowerToys.MouseJump.dll", L"PowerToys.MouseJump.dll",
L"PowerToys.AlwaysOnTopModuleInterface.dll", L"PowerToys.AlwaysOnTopModuleInterface.dll",
L"PowerToys.MousePointerCrosshairs.dll", L"PowerToys.MousePointerCrosshairs.dll",
L"PowerToys.CursorWrap.dll",
L"PowerToys.PowerAccentModuleInterface.dll", L"PowerToys.PowerAccentModuleInterface.dll",
L"PowerToys.PowerOCRModuleInterface.dll", L"PowerToys.PowerOCRModuleInterface.dll",
L"PowerToys.AdvancedPasteModuleInterface.dll", L"PowerToys.AdvancedPasteModuleInterface.dll",

View File

@@ -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.Text.Json.Serialization;
using Settings.UI.Library.Attributes;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class CursorWrapProperties
{
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, true, false, 0x55); // Win + Alt + U
[JsonPropertyName("activation_shortcut")]
public HotkeySettings ActivationShortcut { get; set; }
[JsonPropertyName("auto_activate")]
public BoolProperty AutoActivate { get; set; }
[JsonPropertyName("disable_wrap_during_drag")]
public BoolProperty DisableWrapDuringDrag { get; set; }
public CursorWrapProperties()
{
ActivationShortcut = DefaultActivationShortcut;
AutoActivate = new BoolProperty(false);
DisableWrapDuringDrag = new BoolProperty(true);
}
}
}

View File

@@ -0,0 +1,53 @@
// 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.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class CursorWrapSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "CursorWrap";
[JsonPropertyName("properties")]
public CursorWrapProperties Properties { get; set; }
public CursorWrapSettings()
{
Name = ModuleName;
Properties = new CursorWrapProperties();
Version = "1.0";
}
public string GetModuleName()
{
return Name;
}
public ModuleType GetModuleType() => ModuleType.CursorWrap;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ActivationShortcut,
value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
"MouseUtils_CursorWrap_ActivationShortcut"),
};
return hotkeyAccessors.ToArray();
}
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
{
return false;
}
}
}

View File

@@ -513,6 +513,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
} }
} }
private bool cursorWrap; // defaulting to off
[JsonPropertyName("CursorWrap")]
public bool CursorWrap
{
get => cursorWrap;
set
{
if (cursorWrap != value)
{
LogTelemetryEvent(value);
cursorWrap = value;
}
}
}
private bool lightSwitch; private bool lightSwitch;
[JsonPropertyName("LightSwitch")] [JsonPropertyName("LightSwitch")]

View File

@@ -0,0 +1,13 @@
// 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 Settings.UI.Library.Enumerations
{
public enum HostsDeleteBackupMode
{
Never = 0,
Count = 1,
Age = 2,
}
}

View File

@@ -13,6 +13,12 @@ using Settings.UI.Library.Attributes;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public enum DashboardSortOrder
{
Alphabetical,
ByStatus,
}
public class GeneralSettings : ISettingsConfig public class GeneralSettings : ISettingsConfig
{ {
// Gets or sets a value indicating whether run powertoys on start-up. // Gets or sets a value indicating whether run powertoys on start-up.
@@ -76,6 +82,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("enable_experimentation")] [JsonPropertyName("enable_experimentation")]
public bool EnableExperimentation { get; set; } public bool EnableExperimentation { get; set; }
[JsonPropertyName("dashboard_sort_order")]
public DashboardSortOrder DashboardSortOrder { get; set; }
[JsonPropertyName("ignored_conflict_properties")] [JsonPropertyName("ignored_conflict_properties")]
public ShortcutConflictProperties IgnoredConflictProperties { get; set; } public ShortcutConflictProperties IgnoredConflictProperties { get; set; }
@@ -89,6 +98,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
ShowNewUpdatesToastNotification = true; ShowNewUpdatesToastNotification = true;
AutoDownloadUpdates = false; AutoDownloadUpdates = false;
EnableExperimentation = true; EnableExperimentation = true;
DashboardSortOrder = DashboardSortOrder.Alphabetical;
Theme = "system"; Theme = "system";
SystemTheme = "light"; SystemTheme = "light";
try try

Some files were not shown because too many files have changed in this diff Show More