Compare commits

..

2 Commits

Author SHA1 Message Date
Jeremy Sinclair
3bc1cf4e4c Merge branch 'main' into dev/snickler/net910_patch 2025-11-05 10:30:20 -05:00
Jeremy Sinclair
426405b530 [Deps] bump Microsoft.* and System.* package versions to 9.0.10 2025-11-04 22:16:17 -05:00
67 changed files with 588 additions and 688 deletions

View File

@@ -258,7 +258,6 @@ cominterop
commandnotfound
commandpalette
compmgmt
COMPOSITIONDISABLED
COMPOSITIONFULL
CONFIGW
CONFLICTINGMODIFIERKEY

3
.gitignore vendored
View File

@@ -349,7 +349,10 @@ src/common/Telemetry/*.etl
/src/modules/powerrename/ui/RCb24464
# Generated installer file for Monaco source files.
/installer/PowerToysSetup/MonacoSRC.wxs
/installer/PowerToysSetup/DscResources.wxs
/installer/PowerToysSetupVNext/MonacoSRC.wxs
/installer/PowerToysSetupVNext/DscResources.wxs
# MSBuildCache
/MSBuildCacheLogs/

View File

@@ -65,28 +65,21 @@ if (-not (Test-Path $outputDir)) {
New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
}
# DSC v3 manifests go to DSCModules subfolder
$dscOutputDir = Join-Path $outputDir 'DSCModules'
if (-not (Test-Path $dscOutputDir)) {
Write-Host "Creating DSCModules subfolder at '$dscOutputDir'."
New-Item -Path $dscOutputDir -ItemType Directory -Force | Out-Null
}
Write-Host "DSC manifests will be generated to: '$outputDir'"
Write-Host "DSC manifests will be generated to: '$dscOutputDir'"
Write-Host "Cleaning previously generated DSC manifest files from '$outputDir'."
Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force
Write-Host "Cleaning previously generated DSC manifest files from '$dscOutputDir'."
Get-ChildItem -Path $dscOutputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force
$arguments = @('manifest', '--resource', 'settings', '--outputDir', $dscOutputDir)
$arguments = @('manifest', '--resource', 'settings', '--outputDir', $outputDir)
Write-Host "Invoking DSC manifest generator: '$exePath' $($arguments -join ' ')"
& $exePath @arguments
if ($LASTEXITCODE -ne 0) {
throw "PowerToys.DSC.exe exited with code $LASTEXITCODE"
}
$generatedFiles = Get-ChildItem -Path $dscOutputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
$generatedFiles = Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
if ($generatedFiles.Count -eq 0) {
throw "No DSC manifest files were generated in '$dscOutputDir'."
throw "No DSC manifest files were generated in '$outputDir'."
}
Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):"

View File

@@ -54,7 +54,6 @@
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\CmdPal.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\CmdPal.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\ColorPicker.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\ColorPicker.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\Core.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\Core.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\DscResources.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\DscResources.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\EnvironmentVariables.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\EnvironmentVariables.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\FileExplorerPreview.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\FileExplorerPreview.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\FileLocksmith.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\FileLocksmith.wxs.bk""""

View File

@@ -15,8 +15,8 @@
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="powertoys_env_path_user" Value="" KeyPath="yes" />
</RegistryKey>
<!-- Append DSCModules folder to current user's PATH for DSC v3 usage -->
<Environment Id="AddPowerToysToUserPath" Name="PATH" Action="set" Part="last" System="no" Value="[DSCModulesReferenceFolder]" />
<!-- Append install folder to current user's PATH -->
<Environment Id="AddPowerToysToUserPath" Name="PATH" Action="set" Part="last" System="no" Value="[INSTALLFOLDER]" />
</Component>
<?else?>
<Component Id="powertoys_env_path_machine" Bitness="always64">
@@ -24,8 +24,8 @@
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="powertoys_env_path_machine" Value="" KeyPath="yes" />
</RegistryKey>
<!-- Append DSCModules folder to machine PATH for DSC v3 usage -->
<Environment Id="AddPowerToysToMachinePath" Name="PATH" Action="set" Part="last" System="yes" Value="[DSCModulesReferenceFolder]" />
<!-- Append install folder to machine PATH -->
<Environment Id="AddPowerToysToMachinePath" Name="PATH" Action="set" Part="last" System="yes" Value="[INSTALLFOLDER]" />
</Component>
<?endif?>
<Component Id="powertoys_toast_clsid" Bitness="always64">
@@ -63,6 +63,16 @@
</Component>
</DirectoryRef>
<DirectoryRef Id="DSCModulesReferenceFolder">
<Component Id="PowerToysDSCReference" Guid="40869ACB-0BEB-4911-AE41-5E73BC1586A9" Bitness="always64">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="DSCModulesReference" Value="" KeyPath="yes" />
</RegistryKey>
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version).0\Microsoft.PowerToys.Configure.psd1" Id="PTConfReference.psd1" />
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version).0\Microsoft.PowerToys.Configure.psm1" Id="PTConfReference.psm1" />
</Component>
</DirectoryRef>
<?if $(var.PerUser) = "true" ?>
<!-- DSC module files for PerUser handled in InstallDSCModule custom action. -->
<?else?>
@@ -110,6 +120,7 @@
<RegistryValue Type="string" Name="RemoveCoreFolder" Value="" KeyPath="yes" />
</RegistryKey>
<RemoveFolder Id="RemoveBaseApplicationsAssetsFolder" Directory="BaseApplicationsAssetsFolder" On="uninstall" />
<RemoveFolder Id="RemoveDSCModulesReferenceFolder" Directory="DSCModulesReferenceFolder" On="uninstall" />
<RemoveFolder Id="RemoveWinUI3AppsInstallFolder" Directory="WinUI3AppsInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveWinUI3AppsAssetsFolder" Directory="WinUI3AppsAssetsFolder" On="uninstall" />
<RemoveFolder Id="RemoveINSTALLFOLDER" Directory="INSTALLFOLDER" On="uninstall" />
@@ -117,15 +128,16 @@
<ComponentRef Id="powertoys_exe" />
<ComponentRef Id="PowerToysStartMenuShortcut" />
<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="License_rtf" />
<ComponentRef Id="Notice_md" />
<ComponentRef Id="DesktopShortcut" />
<?if $(var.PerUser) = "true" ?>
<ComponentRef Id="powertoys_env_path_user" />
<?else?>
<ComponentRef Id="powertoys_env_path_machine" />
<?endif?>
<ComponentRef Id="PowerToysDSCReference" />
<?if $(var.PerUser) = "false" ?>
<ComponentRef Id="PowerToysDSC" />
<?endif?>

View File

@@ -1,33 +0,0 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<?include $(sys.CURRENTDIR)\Common.wxi?>
<?define DscJsonFiles=?>
<?define DscJsonFilesPath=$(var.BinDir)\DSCModules?>
<Fragment>
<DirectoryRef Id="DSCModulesReferenceFolder" FileSource="$(var.DscJsonFilesPath)">
<!-- DSC v2 PowerShell module files -->
<Component Id="PowerToysDSCReference" Guid="40869ACB-0BEB-4911-AE41-5E73BC1586A9" Bitness="always64">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="DSCModulesReference" Value="" KeyPath="yes" />
</RegistryKey>
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version).0\Microsoft.PowerToys.Configure.psd1" Id="PTConfReference.psd1" />
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version).0\Microsoft.PowerToys.Configure.psm1" Id="PTConfReference.psm1" />
</Component>
<!-- DSC v3 JSON manifest files - Generated by generateAllFileComponents.ps1 -->
<!--DscJsonFiles_Component_Def-->
</DirectoryRef>
<ComponentGroup Id="DscResourcesComponentGroup">
<ComponentRef Id="PowerToysDSCReference" />
<Component Id="RemoveDSCModulesFolder" Guid="A3C77D92-4E97-4C1A-9F2E-8B3C5D6E7F80" Directory="DSCModulesReferenceFolder">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemoveDSCModulesFolder" Value="" KeyPath="yes" />
</RegistryKey>
<RemoveFolder Id="RemoveDSCModulesReferenceFolder" Directory="DSCModulesReferenceFolder" On="uninstall" />
</Component>
</ComponentGroup>
</Fragment>
</Wix>

View File

@@ -14,6 +14,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" x64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2"
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup Condition="'$(Platform)' != 'x64'">
@@ -24,6 +25,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" arm64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2"
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup>
@@ -35,7 +37,6 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
call move /Y ..\..\..\CmdPal.wxs.bk ..\..\..\CmdPal.wxs
call move /Y ..\..\..\ColorPicker.wxs.bk ..\..\..\ColorPicker.wxs
call move /Y ..\..\..\Core.wxs.bk ..\..\..\Core.wxs
call move /Y ..\..\..\DscResources.wxs.bk ..\..\..\DscResources.wxs
call move /Y ..\..\..\EnvironmentVariables.wxs.bk ..\..\..\EnvironmentVariables.wxs
call move /Y ..\..\..\FileExplorerPreview.wxs.bk ..\..\..\FileExplorerPreview.wxs
call move /Y ..\..\..\FileLocksmith.wxs.bk ..\..\..\FileLocksmith.wxs

View File

@@ -317,7 +317,3 @@ Generate-FileComponents -fileListName "SettingsV2IconsModelsFiles" -wxsFilePath
#Workspaces
Generate-FileList -fileDepsJson "" -fileListName WorkspacesImagesComponentFiles -wxsFilePath $PSScriptRoot\Workspaces.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Workspaces\"
Generate-FileComponents -fileListName "WorkspacesImagesComponentFiles" -wxsFilePath $PSScriptRoot\Workspaces.wxs
#DSC Resources - JSON manifest files in DSCModules subfolder
Generate-FileList -fileDepsJson "" -fileListName DscJsonFiles -wxsFilePath $PSScriptRoot\DscResources.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\DSCModules\"
Generate-FileComponents -fileListName "DscJsonFiles" -wxsFilePath $PSScriptRoot\DscResources.wxs

View File

@@ -0,0 +1,102 @@
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True)]
[string]$dscWxsFile,
[Parameter(Mandatory = $True)]
[string]$Platform,
[Parameter(Mandatory = $True)]
[string]$Configuration
)
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
# Find build output directory
$buildOutputDir = Join-Path $scriptDir "..\..\$Platform\$Configuration"
if (-not (Test-Path $buildOutputDir)) {
Write-Error "Build output directory not found: '$buildOutputDir'"
exit 1
}
# Find all DSC manifest JSON files
$dscFiles = Get-ChildItem -Path $buildOutputDir -Filter "microsoft.powertoys.*.settings.dsc.resource.json" -File
if (-not $dscFiles) {
Write-Warning "No DSC manifest files found in '$buildOutputDir'"
# Create empty component group
$wxsContent = @"
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<?include `$(sys.CURRENTDIR)\Common.wxi?>
<Fragment>
<ComponentGroup Id="DscResourcesComponentGroup">
</ComponentGroup>
</Fragment>
</Wix>
"@
Set-Content -Path $dscWxsFile -Value $wxsContent
exit 0
}
Write-Host "Found $($dscFiles.Count) DSC manifest file(s)"
# Generate WiX fragment
$wxsContent = @"
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<?include `$(sys.CURRENTDIR)\Common.wxi?>
<Fragment>
<DirectoryRef Id="DSCModulesReferenceFolder">
"@
$componentRefs = @()
foreach ($file in $dscFiles) {
$componentId = "DscResource_" + ($file.BaseName -replace '[^A-Za-z0-9_]', '_')
$fileId = $componentId + "_File"
$guid = [System.Guid]::NewGuid().ToString().ToUpper()
$componentRefs += $componentId
$wxsContent += @"
<Component Id="$componentId" Guid="{$guid}" Directory="DSCModulesReferenceFolder">
<RegistryKey Root="`$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="$componentId" Value="" KeyPath="yes"/>
</RegistryKey>
<File Id="$fileId" Source="`$(var.BinDir)$($file.Name)" Vital="no"/>
</Component>
"@
}
$wxsContent += @"
</DirectoryRef>
</Fragment>
<Fragment>
<ComponentGroup Id="DscResourcesComponentGroup">
"@
foreach ($componentId in $componentRefs) {
$wxsContent += @"
<ComponentRef Id="$componentId"/>
"@
}
$wxsContent += @"
</ComponentGroup>
</Fragment>
</Wix>
"@
# Write the WiX file
Set-Content -Path $dscWxsFile -Value $wxsContent
Write-Host "Generated DSC resources WiX fragment: '$dscWxsFile'"

View File

@@ -13,7 +13,7 @@ namespace PowerToys.DSC.Models;
public sealed class DscManifest
{
private const string Schema = "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.vscode.json";
private const string Executable = @"..\PowerToys.DSC.exe";
private const string Executable = @"PowerToys.DSC.exe";
private readonly string _type;
private readonly string _version;

View File

@@ -40,11 +40,9 @@
</EmbeddedResource>
</ItemGroup>
<!-- Generate the DSC resource JSON files to DSCModules subfolder -->
<!-- Skip generation in CI/CD builds (CIBuild=true) to avoid unnecessary work during pipeline -->
<Target Name="GenerateDscResourceJsonFiles" AfterTargets="Build" Condition="'$(CIBuild)' != 'true'">
<Message Text="Generating DSC resource JSON files to DSCModules subfolder..." Importance="high" />
<MakeDir Directories="$(TargetDir)DSCModules" />
<Exec Command="dotnet &quot;$(TargetPath)&quot; manifest --resource settings --outputDir &quot;$(TargetDir)DSCModules&quot;" />
<!-- In debug mode, generate the DSC resource JSON files -->
<Target Name="GenerateDscResourceJsonFiles" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
<Message Text="Generating DSC resource JSON files inside ..." Importance="high" />
<Exec Command="dotnet &quot;$(TargetPath)&quot; manifest --resource settings --outputDir &quot;$(TargetDir)\&quot;" />
</Target>
</Project>

View File

@@ -542,10 +542,7 @@
Source="{x:Bind ViewModel.ActiveAIProvider?.ServiceType, Mode=OneWay, Converter={StaticResource ServiceTypeToIconConverter}}" />
</DropDownButton.Content>
<DropDownButton.Flyout>
<Flyout
Opened="AIProviderFlyout_Opened"
Placement="Bottom"
ShouldConstrainToRootBounds="False">
<Flyout Placement="Bottom" ShouldConstrainToRootBounds="False">
<Grid
Width="386"
Margin="-4"

View File

@@ -22,8 +22,6 @@ namespace AdvancedPaste.Controls
{
public OptionsViewModel ViewModel { get; private set; }
private bool _syncingProviderSelection;
public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(
nameof(PlaceholderText),
typeof(string),
@@ -76,11 +74,6 @@ namespace AdvancedPaste.Controls
var state = ViewModel.IsBusy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
VisualStateManager.GoToState(this, state, true);
}
if (e.PropertyName is nameof(ViewModel.ActiveAIProvider) or nameof(ViewModel.AIProviders))
{
SyncProviderSelection();
}
}
private void ViewModel_PreviewRequested(object sender, EventArgs e)
@@ -94,7 +87,6 @@ namespace AdvancedPaste.Controls
private void Grid_Loaded(object sender, RoutedEventArgs e)
{
InputTxtBox.Focus(FocusState.Programmatic);
SyncProviderSelection();
}
[RelayCommand]
@@ -134,56 +126,18 @@ namespace AdvancedPaste.Controls
Loader.IsLoading = loading;
}
private void SyncProviderSelection()
{
if (AIProviderListView is null)
{
return;
}
try
{
_syncingProviderSelection = true;
AIProviderListView.SelectedItem = ViewModel.ActiveAIProvider;
}
finally
{
_syncingProviderSelection = false;
}
}
private void AIProviderFlyout_Opened(object sender, object e)
{
SyncProviderSelection();
}
private async void AIProviderListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_syncingProviderSelection)
if (AIProviderListView.SelectedItem is PasteAIProviderDefinition provider)
{
return;
}
if (ViewModel.SetActiveProviderCommand.CanExecute(provider))
{
await ViewModel.SetActiveProviderCommand.ExecuteAsync(provider);
}
var flyout = FlyoutBase.GetAttachedFlyout(AIProviderButton);
if (AIProviderListView.SelectedItem is not PasteAIProviderDefinition provider)
{
return;
}
if (string.Equals(ViewModel.ActiveAIProvider?.Id, provider.Id, StringComparison.OrdinalIgnoreCase))
{
var flyout = FlyoutBase.GetAttachedFlyout(AIProviderButton);
flyout?.Hide();
return;
}
if (ViewModel.SetActiveProviderCommand.CanExecute(provider))
{
await ViewModel.SetActiveProviderCommand.ExecuteAsync(provider);
SyncProviderSelection();
}
flyout?.Hide();
}
}
}

View File

@@ -280,15 +280,13 @@
x:Uid="TermsLink"
Padding="0"
FontSize="12"
NavigateUri="{x:Bind ViewModel.TermsLinkUri, Mode=OneWay}"
Visibility="{x:Bind ViewModel.HasTermsLink, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
NavigateUri="https://openai.com/policies/terms-of-use" />
<HyperlinkButton
x:Name="PrivacyHyperLink"
x:Uid="PrivacyLink"
Padding="0"
FontSize="12"
NavigateUri="{x:Bind ViewModel.PrivacyLinkUri, Mode=OneWay}"
Visibility="{x:Bind ViewModel.HasPrivacyLink, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
NavigateUri="https://openai.com/policies/privacy-policy" />
</StackPanel>
</StackPanel>
</Flyout>

View File

@@ -71,11 +71,6 @@ namespace AdvancedPaste.ViewModels
[NotifyPropertyChangedFor(nameof(IsCustomAIAvailable))]
[NotifyPropertyChangedFor(nameof(AllowedAIProviders))]
[NotifyPropertyChangedFor(nameof(ActiveAIProvider))]
[NotifyPropertyChangedFor(nameof(ActiveAIProviderTooltip))]
[NotifyPropertyChangedFor(nameof(TermsLinkUri))]
[NotifyPropertyChangedFor(nameof(PrivacyLinkUri))]
[NotifyPropertyChangedFor(nameof(HasTermsLink))]
[NotifyPropertyChangedFor(nameof(HasPrivacyLink))]
private bool _isAllowedByGPO;
[ObservableProperty]
@@ -192,35 +187,6 @@ namespace AdvancedPaste.ViewModels
}
}
private AIServiceTypeMetadata GetActiveProviderMetadata()
{
var provider = ActiveAIProvider ?? AllowedAIProviders.FirstOrDefault();
var serviceType = provider?.ServiceTypeKind ?? AIServiceType.OpenAI;
return AIServiceTypeRegistry.GetMetadata(serviceType);
}
public Uri TermsLinkUri
{
get
{
var metadata = GetActiveProviderMetadata();
return metadata.HasTermsLink ? metadata.TermsUri : null;
}
}
public Uri PrivacyLinkUri
{
get
{
var metadata = GetActiveProviderMetadata();
return metadata.HasPrivacyLink ? metadata.PrivacyUri : null;
}
}
public bool HasTermsLink => GetActiveProviderMetadata().HasTermsLink;
public bool HasPrivacyLink => GetActiveProviderMetadata().HasPrivacyLink;
public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None;
public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats);
@@ -310,8 +276,8 @@ namespace AdvancedPaste.ViewModels
OnPropertyChanged(nameof(IsAdvancedAIEnabled));
OnPropertyChanged(nameof(AIProviders));
OnPropertyChanged(nameof(AllowedAIProviders));
NotifyActiveProviderChanged();
OnPropertyChanged(nameof(ActiveAIProvider));
OnPropertyChanged(nameof(ActiveAIProviderTooltip));
EnqueueRefreshPasteFormats();
}
@@ -350,17 +316,8 @@ namespace AdvancedPaste.ViewModels
}
}
NotifyActiveProviderChanged();
}
private void NotifyActiveProviderChanged()
{
OnPropertyChanged(nameof(ActiveAIProvider));
OnPropertyChanged(nameof(ActiveAIProviderTooltip));
OnPropertyChanged(nameof(TermsLinkUri));
OnPropertyChanged(nameof(PrivacyLinkUri));
OnPropertyChanged(nameof(HasTermsLink));
OnPropertyChanged(nameof(HasPrivacyLink));
}
private void RefreshPasteFormats()
@@ -880,7 +837,6 @@ namespace AdvancedPaste.ViewModels
UpdateAIProviderActiveFlags();
OnPropertyChanged(nameof(AIProviders));
NotifyActiveProviderChanged();
EnqueueRefreshPasteFormats();
}

View File

@@ -11,6 +11,7 @@
#include <logger/logger_settings.h>
#include <logger/logger.h>
#include <utils/logger_helper.h>
#include <LightSwitchServiceObserver.h>
SERVICE_STATUS g_ServiceStatus = {};
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
@@ -19,7 +20,6 @@ extern int g_lastUpdatedDay = -1;
static ScheduleMode prevMode = ScheduleMode::Off;
static std::wstring prevLat, prevLon;
static int prevMinutes = -1;
static bool lastOverrideStatus = false;
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
@@ -164,8 +164,48 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
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");
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();
auto& settings = LightSwitchSettings::instance().settings();
@@ -173,22 +213,22 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
// Handle initial theme application if necessary
if (settings.scheduleMode != ScheduleMode::Off)
{
Logger::info(L"[LightSwitchService] Schedule mode is set to {}. Applying theme if necessary.", settings.scheduleMode);
LightSwitchSettings::instance().ApplyThemeIfNecessary();
applyTheme(nowMinutes,
settings.lightTime + settings.sunrise_offset,
settings.darkTime + settings.sunset_offset,
settings);
Logger::trace(L"[LightSwitchService] Initialized g_lastUpdatedDay = {}", g_lastUpdatedDay);
}
else
{
Logger::info(L"[LightSwitchService] Schedule mode is set to Off.");
Logger::info(L"[LightSwitchService] Schedule mode is OFF - ticker suspended, waiting for manual action or mode change.");
}
g_lastUpdatedDay = st.wDay;
Logger::info(L"[LightSwitchService] Initializing g_lastUpdatedDay to {}.", g_lastUpdatedDay);
ULONGLONG lastSettingsReload = 0;
// ticker loop
for (;;)
{
HANDLE waits[2] = { g_ServiceStopEvent, hParent };
@@ -197,10 +237,13 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
const auto& settings = LightSwitchSettings::instance().settings();
// If the mode is set to Off, suspend the scheduler and avoid extra work
bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off);
prevMode = settings.scheduleMode;
// ─── Handle "Schedule Off" Mode ─────────────────────────────────────────────
if (settings.scheduleMode == ScheduleMode::Off)
{
Logger::info(L"[LightSwitchService] Schedule mode is OFF - suspending scheduler but keeping service alive.");
Logger::info(L"[LightSwitchService] Schedule mode OFF - suspending scheduler but keeping service alive.");
if (!hManualOverride)
hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
@@ -240,6 +283,7 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
{
Logger::trace(L"[LightSwitchService] Settings change event triggered, reloading settings...");
ResetEvent(LightSwitchSettings::instance().GetSettingsChangedEvent());
LightSwitchSettings::instance().LoadSettings();
const auto& newSettings = LightSwitchSettings::instance().settings();
lastSettingsReload = GetTickCount64();
@@ -254,150 +298,73 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
continue;
}
bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off);
prevMode = settings.scheduleMode;
// ─── Normal Schedule Loop ───────────────────────────────────────────────────
ULONGLONG nowTick = GetTickCount64();
bool recentSettingsReload = (nowTick - lastSettingsReload < 2000);
bool recentSettingsReload = (nowTick - lastSettingsReload < 5000);
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)
if (g_lastUpdatedDay != -1)
{
Logger::debug(L"[LightSwitchService] Manual override active = {}", manualOverrideActive);
lastOverrideStatus = manualOverrideActive;
}
bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
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)
if (settings.scheduleMode != ScheduleMode::Off && !recentSettingsReload && !scheduleJustEnabled)
{
lightBoundary = (settings.lightTime + settings.sunrise_offset) % 1440;
darkBoundary = (settings.darkTime + settings.sunset_offset) % 1440;
Logger::debug(L"[LightSwitchService] Checking if manual override is active...");
bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
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
{
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;
Logger::debug(L"[LightSwitchService] Skipping external-change detection (schedule off, recent reload, or just enabled).");
}
}
// Apply theme if nothing has made us skip
// ─── Apply Schedule Logic ───────────────────────────────────────────────────
if (!skipRest)
{
// Next two conditionals check for any updates necessary to the sun times.
bool modeChangedToSunset = (prevMode != settings.scheduleMode &&
settings.scheduleMode == ScheduleMode::SunsetToSunrise);
bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude);
if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise)
{
SYSTEMTIME st;
GetLocalTime(&st);
Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times.");
update_sun_times(settings);
SYSTEMTIME st;
GetLocalTime(&st);
g_lastUpdatedDay = st.wDay;
prevMode = settings.scheduleMode;
prevLat = settings.latitude;
@@ -416,23 +383,70 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary.");
}
// settings after any necessary updates.
LightSwitchSettings::instance().LoadSettings();
const auto& currentSettings = LightSwitchSettings::instance().settings();
wchar_t msg[160];
swprintf_s(msg,
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%s",
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%d",
st.wHour,
st.wMinute,
currentSettings.lightTime / 60,
currentSettings.lightTime % 60,
currentSettings.darkTime / 60,
currentSettings.darkTime % 60,
ToString(currentSettings.scheduleMode).c_str());
static_cast<int>(currentSettings.scheduleMode));
Logger::info(msg);
LightSwitchSettings::instance().ApplyThemeIfNecessary();
bool manualOverrideActive = false;
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 ────────────────────────────────
@@ -466,6 +480,54 @@ cleanup:
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)
{
if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
#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

@@ -0,0 +1,16 @@
#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/SettingsAPI/settings_helpers.h>
#include "SettingsObserver.h"
#include "ThemeHelper.h"
#include <filesystem>
#include <fstream>
#include <WinHookEventIDs.h>
@@ -38,80 +38,13 @@ void LightSwitchSettings::InitFileWatcher()
m_settingsFileWatcher = std::make_unique<FileWatcher>(
GetSettingsFileName(),
[this]() {
using namespace std::chrono;
{
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);
}
});
Logger::info(L"[LightSwitchSettings] Settings file changed, signaling event.");
LoadSettings();
SetEvent(m_settingsChangedEvent);
});
}
}
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)
{
m_observers.insert(&observer);
@@ -140,7 +73,6 @@ HANDLE LightSwitchSettings::GetSettingsChangedEvent() const
void LightSwitchSettings::LoadSettings()
{
std::lock_guard<std::mutex> guard(m_settingsMutex);
try
{
PowerToysSettings::PowerToyValues values =
@@ -249,49 +181,4 @@ void LightSwitchSettings::LoadSettings()
{
// 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,10 +5,7 @@
#include <vector>
#include <memory>
#include <windows.h>
#include <mutex>
#include <atomic>
#include <thread>
#include <chrono>
#include <common/SettingsAPI/FileWatcher.h>
#include <common/SettingsAPI/settings_objects.h>
#include <SettingsConstants.h>
@@ -81,13 +78,12 @@ public:
void RemoveObserver(SettingsObserver& observer);
void LoadSettings();
void ApplyThemeIfNecessary();
HANDLE GetSettingsChangedEvent() const;
private:
LightSwitchSettings();
~LightSwitchSettings();
~LightSwitchSettings() = default;
LightSwitchConfig m_settings;
std::unique_ptr<FileWatcher> m_settingsFileWatcher;
@@ -96,11 +92,4 @@ private:
void NotifyObservers(SettingId id) const;
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

@@ -1,12 +0,0 @@
<?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>
/// 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>
public sealed partial class BuiltInsCommandProvider : CommandProvider
public partial class BuiltInsCommandProvider : CommandProvider
{
private readonly OpenSettingsCommand openSettings = new();
private readonly QuitCommand quitCommand = new();
@@ -21,7 +21,7 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
public override ICommandItem[] TopLevelCommands() =>
[
new CommandItem(openSettings) { },
new CommandItem(openSettings) { Subtitle = Properties.Resources.builtin_open_settings_subtitle },
new CommandItem(_newExtension) { Title = _newExtension.Title, Subtitle = Properties.Resources.builtin_new_extension_subtitle },
];
@@ -34,7 +34,7 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
public BuiltInsCommandProvider()
{
Id = "com.microsoft.cmdpal.builtin.core";
Id = "Core";
DisplayName = Properties.Resources.builtin_display_name;
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
}

View File

@@ -30,12 +30,7 @@ public partial class MainListPage : DynamicListPage,
{
private readonly string[] _specialFallbacks = [
"com.microsoft.cmdpal.builtin.run",
"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",
"com.microsoft.cmdpal.builtin.calculator"
];
private readonly IServiceProvider _serviceProvider;

View File

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

View File

@@ -25,7 +25,7 @@ public class QueryTests : CommandPaletteUnitTestBase
[DataRow("hibernate", "Hibernate")]
[DataRow("open recycle", "Open 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)
{
var settings = new Settings();
@@ -143,6 +143,6 @@ public class QueryTests : CommandPaletteUnitTestBase
Assert.IsNotNull(result);
var firstItem = result.FirstOrDefault();
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.IsNotNull(provider);
Assert.IsNotNull(provider.DisplayName);
Assert.AreEqual("com.microsoft.cmdpal.builtin.datetime", provider.Id);
Assert.AreEqual("DateTime", provider.Id);
Assert.IsNotNull(provider.Icon);
Assert.IsNotNull(provider.Settings);
}
@@ -103,7 +103,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests
// Assert
Assert.IsFalse(string.IsNullOrEmpty(subtitle));
Assert.IsTrue(subtitle.Contains("Show time and date values in different formats"));
Assert.IsTrue(subtitle.Contains("Provides time and date values in different formats"));
}
}
}

View File

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

View File

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

View File

@@ -35,6 +35,7 @@ public partial class AllAppsCommandProvider : CommandProvider
_listItem = new(_page)
{
Subtitle = Resources.search_installed_apps,
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.
// To add or remove a member, edit your .ResX file then rerun ResGen
// 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.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
@@ -61,7 +61,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
/// <summary>
/// Looks up a localized string similar to Search apps.
/// Looks up a localized string similar to All Apps.
/// </summary>
internal static string all_apps {
get {
@@ -313,7 +313,16 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
/// <summary>
/// Looks up a localized string similar to Search apps....
/// Looks up a localized string similar to Search installed 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>
internal static string search_installed_apps_placeholder {
get {

View File

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

View File

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

View File

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

View File

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

View File

@@ -295,6 +295,15 @@ 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>
/// Looks up a localized string similar to Search files.
/// </summary>

View File

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

View File

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

View File

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

View File

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

View File

@@ -160,7 +160,7 @@ namespace Microsoft.CmdPal.Ext.System {
}
/// <summary>
/// Looks up a localized string similar to Adapter details.
/// Looks up a localized string similar to Adapter Details.
/// </summary>
public static string Microsoft_plugin_ext_adapter_details {
get {
@@ -169,7 +169,7 @@ namespace Microsoft.CmdPal.Ext.System {
}
/// <summary>
/// Looks up a localized string similar to Connection details.
/// Looks up a localized string similar to Connection Details.
/// </summary>
public static string Microsoft_plugin_ext_connection_details {
get {
@@ -187,7 +187,7 @@ namespace Microsoft.CmdPal.Ext.System {
}
/// <summary>
/// Looks up a localized string similar to Open system command.
/// Looks up a localized string similar to Open System Command.
/// </summary>
public static string Microsoft_plugin_ext_fallback_display_title {
get {
@@ -205,7 +205,7 @@ namespace Microsoft.CmdPal.Ext.System {
}
/// <summary>
/// Looks up a localized string similar to System commands.
/// Looks up a localized string similar to System Commands.
/// </summary>
public static string Microsoft_plugin_ext_system_page_name {
get {
@@ -214,7 +214,7 @@ namespace Microsoft.CmdPal.Ext.System {
}
/// <summary>
/// Looks up a localized string similar to Windows system commands.
/// Looks up a localized string similar to Windows System Commands.
/// </summary>
public static string Microsoft_plugin_ext_system_page_title {
get {
@@ -628,7 +628,7 @@ namespace Microsoft.CmdPal.Ext.System {
}
/// <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>
public static string Microsoft_plugin_sys_restart_computer_confirmation {
get {
@@ -790,7 +790,7 @@ namespace Microsoft.CmdPal.Ext.System {
}
/// <summary>
/// Looks up a localized string similar to DNS suffix.
/// Looks up a localized string similar to DNS Suffix.
/// </summary>
public static string Microsoft_plugin_sys_Suffix {
get {
@@ -817,7 +817,7 @@ namespace Microsoft.CmdPal.Ext.System {
}
/// <summary>
/// Looks up a localized string similar to UEFI firmware settings.
/// Looks up a localized string similar to UEFI Firmware Settings.
/// </summary>
public static string Microsoft_plugin_sys_uefi {
get {
@@ -826,7 +826,7 @@ namespace Microsoft.CmdPal.Ext.System {
}
/// <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>
public static string Microsoft_plugin_sys_uefi_confirmation {
get {
@@ -835,7 +835,7 @@ namespace Microsoft.CmdPal.Ext.System {
}
/// <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>
public static string Microsoft_plugin_sys_uefi_description {
get {

View File

@@ -149,10 +149,10 @@
<value>Shutdown</value>
</data>
<data name="Microsoft_plugin_ext_connection_details" xml:space="preserve">
<value>Connection details</value>
<value>Connection Details</value>
</data>
<data name="Microsoft_plugin_ext_adapter_details" xml:space="preserve">
<value>Adapter details</value>
<value>Adapter Details</value>
</data>
<data name="Microsoft_plugin_ext_copy" xml:space="preserve">
<value>Copy to clipboard</value>
@@ -161,10 +161,10 @@
<value>Hide disconnected network info</value>
</data>
<data name="Microsoft_plugin_ext_system_page_title" xml:space="preserve">
<value>Windows system commands</value>
<value>Windows System Commands</value>
</data>
<data name="Microsoft_plugin_ext_system_page_name" xml:space="preserve">
<value>System commands</value>
<value>System Commands</value>
</data>
<data name="Microsoft_plugin_sys_AdapterName" xml:space="preserve">
<value>Adapter name</value>
@@ -327,7 +327,7 @@
<comment>This should align to the action in Windows of a restarting your computer.</comment>
</data>
<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>
</data>
<data name="Microsoft_plugin_sys_restart_computer_description" xml:space="preserve">
@@ -381,7 +381,7 @@
<value>State</value>
</data>
<data name="Microsoft_plugin_sys_Suffix" xml:space="preserve">
<value>DNS suffix</value>
<value>DNS Suffix</value>
</data>
<data name="Microsoft_plugin_sys_TunnelConnection" xml:space="preserve">
<value>Tunnel</value>
@@ -391,15 +391,15 @@
<comment>Means type like category. Here it means network interface type (ethernet, wifi, ...).</comment>
</data>
<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>
</data>
<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>
</data>
<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>
</data>
<data name="Microsoft_plugin_sys_Unknown" xml:space="preserve">
@@ -415,7 +415,7 @@
<value>Sleep</value>
</data>
<data name="Microsoft_plugin_ext_fallback_display_title" xml:space="preserve">
<value>Execute system commands</value>
<value>Open System Command</value>
</data>
<data name="Microsoft_plugin_sys_RestartShell" xml:space="preserve">
<value>Restart Windows Explorer</value>

View File

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

View File

@@ -17,6 +17,11 @@ public sealed partial class TimeDateCalculator
/// </summary>
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>
/// Searches for results
/// </summary>

View File

@@ -349,7 +349,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
/// <summary>
/// Looks up a localized string similar to Time and date.
/// Looks up a localized string similar to Time and Date.
/// </summary>
public static string Microsoft_plugin_timedate_main_page_title {
get {
@@ -448,7 +448,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
/// <summary>
/// Looks up a localized string similar to Show time and date values in different formats.
/// Looks up a localized string similar to Provides time and date values in different formats.
/// </summary>
public static string Microsoft_plugin_timedate_plugin_description {
get {
@@ -484,7 +484,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
/// <summary>
/// Looks up a localized string similar to Time and date.
/// Looks up a localized string similar to Time and Date.
/// </summary>
public static string Microsoft_plugin_timedate_plugin_name {
get {
@@ -501,6 +501,15 @@ 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>
/// Looks up a localized string similar to Date and time; Time and Date; Custom format.
/// </summary>

View File

@@ -202,7 +202,7 @@
<comment>'UTC' means here 'Universal Time Convention'</comment>
</data>
<data name="Microsoft_plugin_timedate_plugin_description" xml:space="preserve">
<value>Show time and date values in different formats</value>
<value>Provides time and date values in different formats</value>
<comment>Do not translate the placeholders like '{0}' because it will be replaced in code.</comment>
</data>
<data name="Microsoft_plugin_timedate_plugin_description_example_calendarWeek" xml:space="preserve">
@@ -215,7 +215,7 @@
<value>Time</value>
</data>
<data name="Microsoft_plugin_timedate_plugin_name" xml:space="preserve">
<value>Time and date</value>
<value>Time and Date</value>
</data>
<data name="Microsoft_plugin_timedate_Rfc1123" xml:space="preserve">
<value>RFC1123</value>
@@ -252,6 +252,10 @@
<value>Current Time; Now</value>
<comment>Don't change order</comment>
</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">
<value>Second</value>
</data>
@@ -354,7 +358,7 @@
<value>Error: Invalid input</value>
</data>
<data name="Microsoft_plugin_timedate_main_page_title" xml:space="preserve">
<value>Time and date</value>
<value>Time and Date</value>
</data>
<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>

View File

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

View File

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

View File

@@ -124,7 +124,16 @@ namespace Microsoft.CmdPal.Ext.WinGet.Properties {
}
/// <summary>
/// Looks up a localized string similar to Add Command Palette extensions from WinGet.
/// Looks up a localized string similar to Search for extensions on 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>
public static string winget_install_extensions_title {
get {
@@ -196,7 +205,7 @@ namespace Microsoft.CmdPal.Ext.WinGet.Properties {
}
/// <summary>
/// Looks up a localized string similar to Find apps on WinGet.
/// Looks up a localized string similar to Search WinGet.
/// </summary>
public static string winget_page_name {
get {
@@ -259,7 +268,7 @@ namespace Microsoft.CmdPal.Ext.WinGet.Properties {
}
/// <summary>
/// Looks up a localized string similar to Add Command Palette extensions from the Microsoft Store.
/// Looks up a localized string similar to Search for extensions in the Store.
/// </summary>
public static string winget_search_store_title {
get {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -74,7 +74,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.UnitTests
var result = main.Object.Query(expectedQuery).FirstOrDefault().SubTitle;
// 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]

View File

@@ -19,7 +19,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// 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.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
@@ -682,7 +682,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.Properties {
}
/// <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>
internal static string Microsoft_plugin_sys_uefi_confirmation {
get {
@@ -691,7 +691,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.Properties {
}
/// <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>
internal static string Microsoft_plugin_sys_uefi_description {
get {

View File

@@ -362,15 +362,15 @@
<comment>Means type like category. Here it means network interface type (ethernet, wifi, ...).</comment>
</data>
<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>
</data>
<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>
</data>
<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>
</data>
<data name="Microsoft_plugin_sys_Unknown" xml:space="preserve">

View File

@@ -208,7 +208,7 @@
<comment>'UTC' means here 'Universal Time Convention'</comment>
</data>
<data name="Microsoft_plugin_timedate_plugin_description" xml:space="preserve">
<value>Shows time and date values for the system time or a custom time stamp (e.g.'{0}', '{1}', '{2}', '{3}')</value>
<value>Provides 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>
</data>
<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.
// 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".
if (ExceptionHelper.IsRecoverableDwmCompositionException(e as System.Runtime.InteropServices.COMException))
if (IsDwmCompositionException(e as System.Runtime.InteropServices.COMException))
{
var logger = LogManager.GetLogger(LoggerName);
logger.Error($"From {(isNotUIThread ? "non" : string.Empty)} UI thread's exception: {ExceptionFormatter.FormatException(e)}");
@@ -91,5 +91,22 @@ 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

@@ -1,46 +0,0 @@
// 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,16 +3,13 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using ManagedCommon;
using Microsoft.Win32;
using Wox.Infrastructure.Image;
using Wox.Infrastructure.UserSettings;
using Wox.Plugin.Logger;
namespace PowerLauncher.Helper
{
@@ -23,9 +20,6 @@ namespace PowerLauncher.Helper
private readonly ThemeHelper _themeHelper = new();
private bool _disposed;
private CancellationTokenSource _themeUpdateTokenSource;
private const int MaxRetries = 5;
private const int InitialDelayMs = 2000;
public Theme CurrentTheme { get; private set; }
@@ -114,80 +108,10 @@ namespace PowerLauncher.Helper
{
Theme newTheme = _themeHelper.DetermineTheme(_settings.Theme);
// 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++)
_mainWindow.Dispatcher.Invoke(() =>
{
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;
}
}
SetSystemTheme(newTheme);
});
}
public void Dispose()
@@ -206,8 +130,6 @@ namespace PowerLauncher.Helper
if (disposing)
{
SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
_themeUpdateTokenSource?.Cancel();
_themeUpdateTokenSource?.Dispose();
}
_disposed = true;

View File

@@ -36,9 +36,9 @@ public static class AIServiceTypeRegistry
IsOnlineService = true,
LegalDescription = "AdvancedPaste_Anthropic_LegalDescription",
TermsLabel = "AdvancedPaste_Anthropic_TermsLabel",
TermsUri = new Uri("https://privacy.claude.com/en/collections/10672567-policies-terms-of-service"),
TermsUri = new Uri("https://www.anthropic.com/legal/terms-of-service"),
PrivacyLabel = "AdvancedPaste_Anthropic_PrivacyLabel",
PrivacyUri = new Uri("https://privacy.claude.com/en/"),
PrivacyUri = new Uri("https://www.anthropic.com/legal/privacy"),
},
[AIServiceType.AzureAIInference] = new AIServiceTypeMetadata
{
@@ -81,9 +81,9 @@ public static class AIServiceTypeRegistry
IsOnlineService = true,
LegalDescription = "AdvancedPaste_Google_LegalDescription",
TermsLabel = "AdvancedPaste_Google_TermsLabel",
TermsUri = new Uri("https://ai.google.dev/gemini-api/terms"),
TermsUri = new Uri("https://policies.google.com/terms"),
PrivacyLabel = "AdvancedPaste_Google_PrivacyLabel",
PrivacyUri = new Uri("https://support.google.com/gemini/answer/13594961"),
PrivacyUri = new Uri("https://policies.google.com/privacy"),
},
[AIServiceType.HuggingFace] = new AIServiceTypeMetadata
{
@@ -126,9 +126,9 @@ public static class AIServiceTypeRegistry
IsLocalModel = true,
LegalDescription = "AdvancedPaste_LocalModel_LegalDescription",
TermsLabel = "AdvancedPaste_Ollama_TermsLabel",
TermsUri = new Uri("https://ollama.org/terms"),
TermsUri = new Uri("https://ollama.com/terms"),
PrivacyLabel = "AdvancedPaste_Ollama_PrivacyLabel",
PrivacyUri = new Uri("https://ollama.org/privacy"),
PrivacyUri = new Uri("https://ollama.com/privacy"),
},
[AIServiceType.Onnx] = new AIServiceTypeMetadata
{

View File

@@ -93,28 +93,21 @@ if (-not (Test-Path $outputDir)) {
New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
}
# DSC v3 manifests go to DSCModules subfolder
$dscOutputDir = Join-Path $outputDir 'DSCModules'
if (-not (Test-Path $dscOutputDir)) {
Write-Host "Creating DSCModules subfolder at '$dscOutputDir'."
New-Item -Path $dscOutputDir -ItemType Directory -Force | Out-Null
}
Write-Host "DSC manifests will be generated to: '$outputDir'"
Write-Host "DSC manifests will be generated to: '$dscOutputDir'"
Write-Host "Cleaning previously generated DSC manifest files from '$outputDir'."
Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force
Write-Host "Cleaning previously generated DSC manifest files from '$dscOutputDir'."
Get-ChildItem -Path $dscOutputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force
$arguments = @('manifest', '--resource', 'settings', '--outputDir', $dscOutputDir)
$arguments = @('manifest', '--resource', 'settings', '--outputDir', $outputDir)
Write-Host "Invoking DSC manifest generator: '$exePath' $($arguments -join ' ')"
& $exePath @arguments
if ($LASTEXITCODE -ne 0) {
throw "PowerToys.DSC.exe exited with code $LASTEXITCODE"
}
$generatedFiles = Get-ChildItem -Path $dscOutputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
$generatedFiles = Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
if ($generatedFiles.Count -eq 0) {
throw "No DSC manifest files were generated in '$dscOutputDir'."
throw "No DSC manifest files were generated in '$outputDir'."
}
Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):"