diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 7876e37cd9..ff7a8fdf02 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -216,6 +216,7 @@ CImage
cla
CLASSDC
CLASSNOTAVAILABLE
+CLEARTYPE
clickable
clickonce
CLIENTEDGE
@@ -253,6 +254,7 @@ colorhistory
colorhistorylimit
COLORKEY
colorref
+Convs
comctl
comdlg
comexp
@@ -529,9 +531,12 @@ eyetracker
FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
FARPROC
+fdw
fdx
+FErase
fesf
FFFF
+FInc
Figma
FILEEXPLORER
fileexploreraddons
@@ -573,6 +578,7 @@ formatetc
FORPARSING
foundrylocal
FRAMECHANGED
+FRestore
frm
FROMTOUCH
fsanitize
@@ -607,6 +613,7 @@ GETSCREENSAVERRUNNING
GETSECKEY
GETSTICKYKEYS
GETTEXTLENGTH
+gfx
GHND
gitmodules
GMEM
@@ -657,6 +664,7 @@ hdwwiz
Helpline
helptext
HGFE
+hgdiobj
hglobal
hhk
HHmmssfff
@@ -704,7 +712,7 @@ hotlight
hotspot
HPAINTBUFFER
HRAWINPUT
-HREDRAW
+hredraw
hres
hresult
hrgn
@@ -882,7 +890,7 @@ LINKOVERLAY
LINQTo
listview
LIVEDRAW
-LIVEZOOM
+livezoom
LLKH
llkhf
LMEM
@@ -911,6 +919,7 @@ LPBITMAPINFOHEADER
LPCFHOOKPROC
LPCITEMIDLIST
LPCLSID
+lpch
lpcmi
LPCMINVOKECOMMANDINFO
LPCREATESTRUCT
@@ -935,6 +944,7 @@ lptpm
LPTR
LPTSTR
lpv
+LPrivate
LPW
lpwcx
lpwndpl
@@ -1349,6 +1359,7 @@ ppv
ppwsz
prc
Prefixer
+Premul
prependpath
prepopulate
prevhost
@@ -1904,7 +1915,7 @@ valuegenerator
variantassignment
VARTYPE
vcamp
-VCENTER
+vcenter
vcgtq
VCINSTALLDIR
Vcpkg
@@ -1936,7 +1947,7 @@ vorrq
VOS
vpaddlq
vqsubq
-VREDRAW
+vredraw
vreinterpretq
VSC
VSCBD
diff --git a/.gitignore b/.gitignore
index 1318abc22c..1ed1bbcbc1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -358,4 +358,4 @@ src/common/Telemetry/*.etl
/src/settings-ui/Settings.UI/Assets/Settings/search.index.json
# PowerToysInstaller Build Temp Files
-installer/*/*.wxs.bk
\ No newline at end of file
+installer/*/*.wxs.bk
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index e3ebffc20c..1b4325ba1a 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -235,6 +235,14 @@
"PowerToys.CmdPalModuleInterface.dll",
"CmdPalKeyboardService.dll",
+ "PowerToys.ModuleContracts.dll",
+ "Awake.ModuleServices.dll",
+ "ColorPicker.ModuleServices.dll",
+ "Workspaces.ModuleServices.dll",
+ "Microsoft.CommandPalette.Extensions.dll",
+ "Microsoft.CommandPalette.Extensions.Toolkit.dll",
+ "Microsoft.CmdPal.Ext.PowerToys.dll",
+ "Microsoft.CmdPal.Ext.PowerToys.exe",
"*Microsoft.CmdPal.UI_*.msix",
"PowerToys.DSC.dll",
@@ -358,9 +366,13 @@
"boost_regex-vc143-mt-x32-1_87.dll",
"boost_regex-vc143-mt-x64-1_87.dll",
+ "Microsoft.ML.OnnxRuntime.dll",
+
"UnitsNet.dll",
"UtfUnknown.dll",
- "Wpf.Ui.dll"
+ "Wpf.Ui.dll",
+ "Shmuelie.WinRTServer.dll",
+ "ToolGood.Words.Pinyin.dll"
],
"SigningInfo": {
"Operations": [
diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml
index 6a4df0c720..57fdba6397 100644
--- a/.pipelines/v2/templates/job-build-project.yml
+++ b/.pipelines/v2/templates/job-build-project.yml
@@ -624,4 +624,4 @@ jobs:
- publish: $(JobOutputDirectory)
artifact: $(JobOutputArtifactName)-failure-$(System.JobAttempt)
displayName: Publish failure logs
- condition: or(failed(), canceled())
+ condition: or(failed(), canceled())
\ No newline at end of file
diff --git a/.pipelines/versionAndSignCheck.ps1 b/.pipelines/versionAndSignCheck.ps1
index cf1f515e78..5b03250dd6 100644
--- a/.pipelines/versionAndSignCheck.ps1
+++ b/.pipelines/versionAndSignCheck.ps1
@@ -104,4 +104,4 @@ if ($totalFailure -gt 0) {
exit 1
}
-exit 0
+exit 0
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index eb04903b7e..28bd723f93 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -37,6 +37,7 @@
+
@@ -94,6 +95,7 @@
+
@@ -145,4 +147,4 @@
-
\ No newline at end of file
+
diff --git a/NOTICE.md b/NOTICE.md
index 23efb64864..4273edbb18 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1560,6 +1560,7 @@ SOFTWARE.
- ReverseMarkdown
- ScipBe.Common.Office.OneNote
- SharpCompress
+- Shmuelie.WinRTServer
- SkiaSharp.Views.WinUI
- StreamJsonRpc
- StyleCop.Analyzers
diff --git a/PowerToys.slnx b/PowerToys.slnx
index 1884b2d58b..bc481ce526 100644
--- a/PowerToys.slnx
+++ b/PowerToys.slnx
@@ -44,6 +44,10 @@
+
+
+
+
@@ -156,6 +160,10 @@
+
+
+
+
@@ -166,6 +174,10 @@
+
+
+
+
@@ -206,6 +218,11 @@
+
+
+
+
+
@@ -932,6 +949,10 @@
+
+
+
+
diff --git a/installer/PowerToysSetupVNext/BaseApplications.wxs b/installer/PowerToysSetupVNext/BaseApplications.wxs
index 1947cbf1f2..57a9c71637 100644
--- a/installer/PowerToysSetupVNext/BaseApplications.wxs
+++ b/installer/PowerToysSetupVNext/BaseApplications.wxs
@@ -7,11 +7,18 @@
+
+
+
+
+
+
+
diff --git a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
index 18d6232140..a7a9744e87 100644
--- a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
+++ b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
@@ -173,4 +173,4 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
-
\ No newline at end of file
+
diff --git a/src/PackageIdentity/AppxManifest.xml b/src/PackageIdentity/AppxManifest.xml
index 822daae8bc..502cc33ff0 100644
--- a/src/PackageIdentity/AppxManifest.xml
+++ b/src/PackageIdentity/AppxManifest.xml
@@ -10,7 +10,8 @@
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
xmlns:systemai="http://schemas.microsoft.com/appx/manifest/systemai/windows10"
- IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai">
+ xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
+ IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai com">
+
@@ -66,5 +68,42 @@
AppListEntry="none">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/common/Common.UI/SettingsDeepLink.cs b/src/common/Common.UI/SettingsDeepLink.cs
index 1891532d16..5233c0d668 100644
--- a/src/common/Common.UI/SettingsDeepLink.cs
+++ b/src/common/Common.UI/SettingsDeepLink.cs
@@ -2,8 +2,10 @@
// 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.Diagnostics;
using System.IO;
+using ManagedCommon;
namespace Common.UI
{
@@ -120,28 +122,33 @@ namespace Common.UI
}
}
- public static void OpenSettings(SettingsWindow window, bool mainExecutableIsOnTheParentFolder)
+ // What about debug build? Should also consider debug build, maybe tray window message?
+ public static void OpenSettings(SettingsWindow window)
{
try
{
- var directoryPath = System.AppContext.BaseDirectory;
- if (mainExecutableIsOnTheParentFolder)
+ var exePath = Path.Combine(
+ PowerToysPathResolver.GetPowerToysInstallPath(),
+ "PowerToys.exe");
+
+ if (exePath == null || !File.Exists(exePath))
{
- // Need to go into parent folder for PowerToys.exe. Likely a WinUI3 App SDK application.
- directoryPath = Path.Combine(directoryPath, "..");
- directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
- }
- else
- {
- // PowerToys.exe is in the same path as the application.
- directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
+ Logger.LogError($"Failed to find powertoys exe path, {exePath}");
+ return;
}
- Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
+ var args = "--open-settings=" + SettingsWindowNameToString(window);
+
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = exePath,
+ Arguments = args,
+ UseShellExecute = false,
+ });
}
- catch
+ catch (Exception ex)
{
- // TODO(stefan): Log exception once unified logging is implemented
+ Logger.LogError(ex.Message);
}
}
}
diff --git a/src/common/ManagedCommon/PowerToysPathResolver.cs b/src/common/ManagedCommon/PowerToysPathResolver.cs
new file mode 100644
index 0000000000..fc6afee818
--- /dev/null
+++ b/src/common/ManagedCommon/PowerToysPathResolver.cs
@@ -0,0 +1,168 @@
+// 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.Diagnostics;
+using System.IO;
+using System.Runtime.Versioning;
+using Microsoft.Win32;
+
+namespace ManagedCommon
+{
+ [SupportedOSPlatform("windows")]
+ public class PowerToysPathResolver
+ {
+ private const string PowerToysRegistryKey = @"Software\Classes\powertoys";
+ private const string PowerToysExe = "PowerToys.exe";
+
+ ///
+ /// Gets the PowerToys installation path by checking registry entries
+ ///
+ /// The path to PowerToys installation directory, or null if not found
+ public static string GetPowerToysInstallPath()
+ {
+#if DEBUG
+ // In debug builds, resolve directly from the running process (no installer/registry involved).
+ return GetPathFromCurrentProcess();
+#else
+ // Try to get path from Per-User installation first
+ string path = GetPathFromRegistry(RegistryHive.CurrentUser);
+ if (!string.IsNullOrEmpty(path))
+ {
+ return path;
+ }
+
+ // Fall back to Per-Machine installation
+ path = GetPathFromRegistry(RegistryHive.LocalMachine);
+ if (!string.IsNullOrEmpty(path))
+ {
+ return path;
+ }
+
+ return null;
+#endif
+ }
+
+ private static string GetPathFromRegistry(RegistryHive hive)
+ {
+ try
+ {
+ using var baseKey = RegistryKey.OpenBaseKey(hive, RegistryView.Registry64);
+
+ // First try to get path from the powertoys protocol registration
+ string path = GetPathFromProtocolRegistration(baseKey);
+ if (!string.IsNullOrEmpty(path))
+ {
+ return path;
+ }
+ }
+ catch (Exception)
+ {
+ // Ignore registry access errors
+ }
+
+ return null;
+ }
+
+ private static string GetPathFromProtocolRegistration(RegistryKey baseKey)
+ {
+ try
+ {
+ using var key = baseKey.OpenSubKey($@"{PowerToysRegistryKey}\shell\open\command");
+
+ if (key != null)
+ {
+ string command = key.GetValue(string.Empty)?.ToString();
+ if (!string.IsNullOrEmpty(command))
+ {
+ // Parse command like: "C:\Program Files\PowerToys\PowerToys.exe" "%1"
+ return ExtractPathFromCommand(command);
+ }
+ }
+ }
+ catch (Exception)
+ {
+ // Ignore registry access errors
+ }
+
+ return null;
+ }
+
+ private static string GetPathFromCurrentProcess()
+ {
+ try
+ {
+ // If we're running inside PowerToys.exe (dev/debug builds), use the executable location.
+ var processPath = Process.GetCurrentProcess().MainModule?.FileName;
+ if (!string.IsNullOrEmpty(processPath))
+ {
+ var processDir = Path.GetDirectoryName(processPath);
+ if (!string.IsNullOrEmpty(processDir) && File.Exists(Path.Combine(processDir, PowerToysExe)))
+ {
+ return processDir;
+ }
+ }
+
+ // As a fallback, walk up from AppContext.BaseDirectory to find PowerToys.exe.
+ var directory = new DirectoryInfo(AppContext.BaseDirectory);
+ while (directory != null)
+ {
+ var candidate = Path.Combine(directory.FullName, PowerToysExe);
+ if (File.Exists(candidate))
+ {
+ return directory.FullName;
+ }
+
+ directory = directory.Parent;
+ }
+ }
+ catch
+ {
+ // Ignore reflection/process permission errors; caller will see null and handle accordingly.
+ }
+
+ return null;
+ }
+
+ private static string ExtractPathFromCommand(string command)
+ {
+ if (string.IsNullOrEmpty(command))
+ {
+ return null;
+ }
+
+ try
+ {
+ // Handle quoted paths: "C:\Program Files\PowerToys\PowerToys.exe" "%1"
+ if (command.StartsWith('\"'))
+ {
+ int endQuote = command.IndexOf('\"', 1);
+ if (endQuote > 1)
+ {
+ string exePath = command.Substring(1, endQuote - 1);
+ if (File.Exists(exePath))
+ {
+ return Path.GetDirectoryName(exePath);
+ }
+ }
+ }
+ else
+ {
+ // Handle unquoted paths (less common)
+ string[] parts = command.Split(' ');
+ if (parts.Length > 0 && File.Exists(parts[0]))
+ {
+ return Path.GetDirectoryName(parts[0]);
+ }
+ }
+ }
+ catch (Exception)
+ {
+ // Ignore path parsing errors
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/common/PowerToys.ModuleContracts/IModuleService.cs b/src/common/PowerToys.ModuleContracts/IModuleService.cs
new file mode 100644
index 0000000000..845d40e656
--- /dev/null
+++ b/src/common/PowerToys.ModuleContracts/IModuleService.cs
@@ -0,0 +1,47 @@
+// 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 Common.UI;
+
+namespace PowerToys.ModuleContracts;
+
+///
+/// Base contract for PowerToys modules exposed to the Command Palette.
+///
+public interface IModuleService
+{
+ ///
+ /// Gets module identifier (e.g., Workspaces, Awake).
+ ///
+ string Key { get; }
+
+ Task LaunchAsync(CancellationToken cancellationToken = default);
+
+ Task OpenSettingsAsync(CancellationToken cancellationToken = default);
+}
+
+///
+/// Helper base to reduce duplication for simple modules.
+///
+public abstract class ModuleServiceBase : IModuleService
+{
+ public abstract string Key { get; }
+
+ protected abstract SettingsDeepLink.SettingsWindow SettingsWindow { get; }
+
+ public abstract Task LaunchAsync(CancellationToken cancellationToken = default);
+
+ public virtual Task OpenSettingsAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ SettingsDeepLink.OpenSettings(SettingsWindow);
+ return Task.FromResult(OperationResult.Ok());
+ }
+ catch (Exception ex)
+ {
+ return Task.FromResult(OperationResult.Fail($"Failed to open settings for {Key}: {ex.Message}"));
+ }
+ }
+}
diff --git a/src/common/PowerToys.ModuleContracts/OperationResult.cs b/src/common/PowerToys.ModuleContracts/OperationResult.cs
new file mode 100644
index 0000000000..a20aa26a3f
--- /dev/null
+++ b/src/common/PowerToys.ModuleContracts/OperationResult.cs
@@ -0,0 +1,30 @@
+// 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 PowerToys.ModuleContracts;
+
+///
+/// Lightweight result type for module operations.
+///
+public readonly record struct OperationResult(bool Success, string? Error = null)
+{
+ public static OperationResult Ok() => new(true, null);
+
+ public static OperationResult Fail(string error) => new(false, error);
+}
+
+///
+/// Result type with a payload.
+///
+public readonly record struct OperationResult(bool Success, T? Value, string? Error = null);
+
+///
+/// Factory helpers for creating operation results.
+///
+public static class OperationResults
+{
+ public static OperationResult Ok(T value) => new(true, value, null);
+
+ public static OperationResult Fail(string error) => new(false, default, error);
+}
diff --git a/src/common/PowerToys.ModuleContracts/PowerToys.ModuleContracts.csproj b/src/common/PowerToys.ModuleContracts/PowerToys.ModuleContracts.csproj
new file mode 100644
index 0000000000..aa80bb05fb
--- /dev/null
+++ b/src/common/PowerToys.ModuleContracts/PowerToys.ModuleContracts.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ enable
+ enable
+ false
+ false
+
+
+
+
+
+
diff --git a/src/common/interop/Constants.cpp b/src/common/interop/Constants.cpp
index 4fc9fca6f6..3e1c339233 100644
--- a/src/common/interop/Constants.cpp
+++ b/src/common/interop/Constants.cpp
@@ -75,10 +75,62 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE;
}
+ hstring Constants::AdvancedPasteShowUIEvent()
+ {
+ return CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_EVENT;
+ }
hstring Constants::AdvancedPasteTerminateAppMessage()
{
return CommonSharedConstants::ADVANCED_PASTE_TERMINATE_APP_MESSAGE;
}
+ hstring Constants::AlwaysOnTopPinEvent()
+ {
+ return CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT;
+ }
+ hstring Constants::FindMyMouseTriggerEvent()
+ {
+ return CommonSharedConstants::FIND_MY_MOUSE_TRIGGER_EVENT;
+ }
+ hstring Constants::MouseHighlighterTriggerEvent()
+ {
+ return CommonSharedConstants::MOUSE_HIGHLIGHTER_TRIGGER_EVENT;
+ }
+ hstring Constants::MouseCrosshairsTriggerEvent()
+ {
+ return CommonSharedConstants::MOUSE_CROSSHAIRS_TRIGGER_EVENT;
+ }
+ hstring Constants::CursorWrapTriggerEvent()
+ {
+ return CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT;
+ }
+ hstring Constants::LightSwitchToggleEvent()
+ {
+ return CommonSharedConstants::LIGHTSWITCH_TOGGLE_EVENT;
+ }
+ hstring Constants::ZoomItZoomEvent()
+ {
+ return CommonSharedConstants::ZOOMIT_ZOOM_EVENT;
+ }
+ hstring Constants::ZoomItDrawEvent()
+ {
+ return CommonSharedConstants::ZOOMIT_DRAW_EVENT;
+ }
+ hstring Constants::ZoomItBreakEvent()
+ {
+ return CommonSharedConstants::ZOOMIT_BREAK_EVENT;
+ }
+ hstring Constants::ZoomItLiveZoomEvent()
+ {
+ return CommonSharedConstants::ZOOMIT_LIVEZOOM_EVENT;
+ }
+ hstring Constants::ZoomItSnipEvent()
+ {
+ return CommonSharedConstants::ZOOMIT_SNIP_EVENT;
+ }
+ hstring Constants::ZoomItRecordEvent()
+ {
+ return CommonSharedConstants::ZOOMIT_RECORD_EVENT;
+ }
hstring Constants::ShowPowerOCRSharedEvent()
{
return CommonSharedConstants::SHOW_POWEROCR_SHARED_EVENT;
diff --git a/src/common/interop/Constants.h b/src/common/interop/Constants.h
index 80834442c5..8c95a09f99 100644
--- a/src/common/interop/Constants.h
+++ b/src/common/interop/Constants.h
@@ -23,6 +23,20 @@ namespace winrt::PowerToys::Interop::implementation
static hstring AdvancedPasteAdditionalActionMessage();
static hstring AdvancedPasteCustomActionMessage();
static hstring AdvancedPasteTerminateAppMessage();
+ static hstring AdvancedPasteShowUIEvent();
+ static hstring AlwaysOnTopPinEvent();
+ static hstring MeasureToolTriggerEvent();
+ static hstring FindMyMouseTriggerEvent();
+ static hstring MouseHighlighterTriggerEvent();
+ static hstring MouseCrosshairsTriggerEvent();
+ static hstring CursorWrapTriggerEvent();
+ static hstring LightSwitchToggleEvent();
+ static hstring ZoomItZoomEvent();
+ static hstring ZoomItDrawEvent();
+ static hstring ZoomItBreakEvent();
+ static hstring ZoomItLiveZoomEvent();
+ static hstring ZoomItSnipEvent();
+ static hstring ZoomItRecordEvent();
static hstring ShowPowerOCRSharedEvent();
static hstring TerminatePowerOCRSharedEvent();
static hstring MouseJumpShowPreviewEvent();
@@ -33,7 +47,6 @@ namespace winrt::PowerToys::Interop::implementation
static hstring PowerAccentExitEvent();
static hstring ShortcutGuideTriggerEvent();
static hstring RegistryPreviewTriggerEvent();
- static hstring MeasureToolTriggerEvent();
static hstring GcodePreviewResizeEvent();
static hstring BgcodePreviewResizeEvent();
static hstring QoiPreviewResizeEvent();
diff --git a/src/common/interop/Constants.idl b/src/common/interop/Constants.idl
index 6833b0d417..97be6c8b7e 100644
--- a/src/common/interop/Constants.idl
+++ b/src/common/interop/Constants.idl
@@ -20,6 +20,19 @@ namespace PowerToys
static String AdvancedPasteAdditionalActionMessage();
static String AdvancedPasteCustomActionMessage();
static String AdvancedPasteTerminateAppMessage();
+ static String AdvancedPasteShowUIEvent();
+ static String AlwaysOnTopPinEvent();
+ static String FindMyMouseTriggerEvent();
+ static String MouseHighlighterTriggerEvent();
+ static String MouseCrosshairsTriggerEvent();
+ static String CursorWrapTriggerEvent();
+ static String LightSwitchToggleEvent();
+ static String ZoomItZoomEvent();
+ static String ZoomItDrawEvent();
+ static String ZoomItBreakEvent();
+ static String ZoomItLiveZoomEvent();
+ static String ZoomItSnipEvent();
+ static String ZoomItRecordEvent();
static String ShowPowerOCRSharedEvent();
static String TerminatePowerOCRSharedEvent();
static String MouseJumpShowPreviewEvent();
@@ -51,4 +64,4 @@ namespace PowerToys
static String ShowCmdPalEvent();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h
index aa6306d7ba..73c4fb7006 100644
--- a/src/common/interop/shared_constants.h
+++ b/src/common/interop/shared_constants.h
@@ -40,6 +40,8 @@ namespace CommonSharedConstants
const wchar_t ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE[] = L"CustomAction";
const wchar_t ADVANCED_PASTE_TERMINATE_APP_MESSAGE[] = L"TerminateApp";
+
+ const wchar_t ADVANCED_PASTE_SHOW_UI_EVENT[] = L"Local\\PowerToys_AdvancedPaste_ShowUI";
// Path to the event used to show Color Picker
const wchar_t SHOW_COLOR_PICKER_SHARED_EVENT[] = L"Local\\ShowColorPickerEvent-8c46be2a-3e05-4186-b56b-4ae986ef2525";
@@ -83,12 +85,21 @@ namespace CommonSharedConstants
const wchar_t TERMINATE_MOUSE_JUMP_SHARED_EVENT[] = L"Local\\TerminateMouseJumpEvent-252fa337-317f-4c37-a61f-99464c3f9728";
+ // Paths to the events used by other Mouse Utilities
+ const wchar_t FIND_MY_MOUSE_TRIGGER_EVENT[] = L"Local\\FindMyMouseTriggerEvent-5a9dc5f4-1c74-4f2f-a66f-1b9b6a2f9b23";
+ const wchar_t MOUSE_HIGHLIGHTER_TRIGGER_EVENT[] = L"Local\\MouseHighlighterTriggerEvent-1e3c9c3d-3fdf-4f9a-9a52-31c9b3c3a8f4";
+ const wchar_t MOUSE_CROSSHAIRS_TRIGGER_EVENT[] = L"Local\\MouseCrosshairsTriggerEvent-0d4c7f92-0a5c-4f5c-b64b-8a2a2f7e0b21";
+ const wchar_t CURSOR_WRAP_TRIGGER_EVENT[] = L"Local\\CursorWrapTriggerEvent-1f8452b5-4e6e-45b3-8b09-13f14a5900c9";
+
// Path to the event used by RegistryPreview
const wchar_t REGISTRY_PREVIEW_TRIGGER_EVENT[] = L"Local\\RegistryPreviewEvent-4C559468-F75A-4E7F-BC4F-9C9688316687";
// Path to the event used by MeasureTool
const wchar_t MEASURE_TOOL_TRIGGER_EVENT[] = L"Local\\MeasureToolEvent-3d46745f-09b3-4671-a577-236be7abd199";
+ // Path to the event used by LightSwitch
+ const wchar_t LIGHTSWITCH_TOGGLE_EVENT[] = L"Local\\PowerToys-LightSwitch-ToggleEvent-d8dc2f29-8c94-4ca1-8c5f-3e2b1e3c4f5a";
+
// Path to the event used by GcodePreviewHandler
const wchar_t GCODE_PREVIEW_RESIZE_EVENT[] = L"Local\\PowerToysGcodePreviewResizeEvent-6ff1f9bd-ccbd-4b24-a79f-40a34fb0317d";
@@ -130,6 +141,12 @@ namespace CommonSharedConstants
// Path to the events used by ZoomIt
const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324";
const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220";
+ const wchar_t ZOOMIT_ZOOM_EVENT[] = L"Local\\PowerToysZoomIt-ZoomEvent-1e4190d7-94bc-4ad5-adc0-9a8fd07cb393";
+ const wchar_t ZOOMIT_DRAW_EVENT[] = L"Local\\PowerToysZoomIt-DrawEvent-56338997-404d-4549-bd9a-d132b6766975";
+ const wchar_t ZOOMIT_BREAK_EVENT[] = L"Local\\PowerToysZoomIt-BreakEvent-17f2e63c-4c56-41dd-90a0-2d12f9f50c6b";
+ const wchar_t ZOOMIT_LIVEZOOM_EVENT[] = L"Local\\PowerToysZoomIt-LiveZoomEvent-390bf0c7-616f-47dc-bafe-a2d228add20d";
+ const wchar_t ZOOMIT_SNIP_EVENT[] = L"Local\\PowerToysZoomIt-SnipEvent-2fd9c211-436d-4f17-a902-2528aaae3e30";
+ const wchar_t ZOOMIT_RECORD_EVENT[] = L"Local\\PowerToysZoomIt-RecordEvent-74539344-eaad-4711-8e83-23946e424512";
// used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
diff --git a/src/common/utils/EventWaiter.h b/src/common/utils/EventWaiter.h
index b9f420c81d..c2db880530 100644
--- a/src/common/utils/EventWaiter.h
+++ b/src/common/utils/EventWaiter.h
@@ -3,78 +3,128 @@
#include
#include
#include
+#include
#include
+///
+/// A reusable utility class that listens for a named Windows event and invokes a callback when triggered.
+/// Provides RAII-based resource management for event handles and the listener thread.
+/// The thread is properly joined on destruction to ensure clean shutdown.
+///
class EventWaiter
{
public:
- EventWaiter() {}
- EventWaiter(const std::wstring& name, std::function callback)
+ EventWaiter() = default;
+
+ EventWaiter(const EventWaiter&) = delete;
+ EventWaiter& operator=(const EventWaiter&) = delete;
+ EventWaiter(EventWaiter&&) = delete;
+ EventWaiter& operator=(EventWaiter&&) = delete;
+
+ ~EventWaiter()
{
- // Create localExitThreadEvent and localWaitingEvent for capturing. We cannot capture 'this' as we implement move constructor.
- auto localExitThreadEvent = exitThreadEvent = CreateEvent(nullptr, false, false, nullptr);
- HANDLE localWaitingEvent = waitingEvent = CreateEvent(nullptr, false, false, name.c_str());
- std::thread([=]() {
- HANDLE events[2] = { localWaitingEvent, localExitThreadEvent };
- while (true)
+ stop();
+ }
+
+ ///
+ /// Starts listening for the specified named event. When the event is signaled, the callback is invoked.
+ ///
+ /// The name of the Windows event to listen for.
+ /// The callback function to invoke when the event is triggered. Receives ERROR_SUCCESS on success.
+ /// true if listening started successfully, false otherwise.
+ bool start(const std::wstring& name, std::function callback)
+ {
+ if (m_listening)
+ {
+ return false;
+ }
+
+ m_exitThreadEvent = CreateEventW(nullptr, false, false, nullptr);
+ m_waitingEvent = CreateEventW(nullptr, false, false, name.c_str());
+
+ if (!m_exitThreadEvent || !m_waitingEvent)
+ {
+ cleanup();
+ return false;
+ }
+
+ m_listening = true;
+ m_eventThread = std::thread([this, cb = std::move(callback)]() {
+ HANDLE events[2] = { m_waitingEvent, m_exitThreadEvent };
+ while (m_listening)
{
auto waitResult = WaitForMultipleObjects(2, events, false, INFINITE);
+ if (!m_listening)
+ {
+ break;
+ }
+
if (waitResult == WAIT_OBJECT_0 + 1)
{
+ // Exit event signaled
break;
}
if (waitResult == WAIT_FAILED)
{
- callback(GetLastError());
+ cb(GetLastError());
continue;
}
if (waitResult == WAIT_OBJECT_0)
{
- callback(ERROR_SUCCESS);
+ cb(ERROR_SUCCESS);
}
}
- }).detach();
+ });
+
+ return true;
}
- EventWaiter(EventWaiter&) = delete;
- EventWaiter& operator=(EventWaiter&) = delete;
-
- EventWaiter(EventWaiter&& a) noexcept
+ ///
+ /// Stops listening for the event and cleans up resources.
+ /// Waits for the listener thread to finish before returning.
+ /// Safe to call multiple times.
+ ///
+ void stop()
{
- this->exitThreadEvent = a.exitThreadEvent;
- this->waitingEvent = a.waitingEvent;
-
- a.exitThreadEvent = nullptr;
- a.waitingEvent = nullptr;
- }
-
- EventWaiter& operator=(EventWaiter&& a) noexcept
- {
- this->exitThreadEvent = a.exitThreadEvent;
- this->waitingEvent = a.waitingEvent;
-
- a.exitThreadEvent = nullptr;
- a.waitingEvent = nullptr;
- return *this;
- }
-
- ~EventWaiter()
- {
- if (exitThreadEvent)
+ m_listening = false;
+ if (m_exitThreadEvent)
{
- SetEvent(exitThreadEvent);
- CloseHandle(exitThreadEvent);
+ SetEvent(m_exitThreadEvent);
}
-
- if (waitingEvent)
+ if (m_eventThread.joinable())
{
- CloseHandle(waitingEvent);
+ m_eventThread.join();
}
+ cleanup();
+ }
+
+ ///
+ /// Returns whether the listener is currently active.
+ ///
+ bool is_listening() const
+ {
+ return m_listening;
}
private:
- HANDLE exitThreadEvent = nullptr;
- HANDLE waitingEvent = nullptr;
+ void cleanup()
+ {
+ if (m_exitThreadEvent)
+ {
+ CloseHandle(m_exitThreadEvent);
+ m_exitThreadEvent = nullptr;
+ }
+ if (m_waitingEvent)
+ {
+ CloseHandle(m_waitingEvent);
+ m_waitingEvent = nullptr;
+ }
+ }
+
+ HANDLE m_exitThreadEvent = nullptr;
+ HANDLE m_waitingEvent = nullptr;
+ std::thread m_eventThread;
+ std::atomic_bool m_listening{ false };
};
\ No newline at end of file
diff --git a/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj b/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
index 9dc11a0a8a..a87508604f 100644
--- a/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
+++ b/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
@@ -45,6 +45,7 @@
+
\ No newline at end of file
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
index b055d46457..b474b8215a 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
@@ -661,7 +661,7 @@ namespace AdvancedPaste.ViewModels
[RelayCommand]
public void OpenSettings()
{
- SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.AdvancedPaste, true);
+ SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.AdvancedPaste);
GetMainWindow()?.Close();
}
diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
index 64caaa115f..6cf2e8d9a9 100644
--- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
+++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
@@ -15,9 +15,11 @@
#include
#include
#include
+#include
#include
#include
+#include
#include
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
@@ -101,6 +103,9 @@ private:
bool m_is_advanced_ai_enabled = false;
bool m_preview_custom_format_output = true;
+ // Event listening for external triggers (e.g., from CmdPal extension)
+ EventWaiter m_triggerEventWaiter;
+
Hotkey parse_single_hotkey(const wchar_t* keyName, const winrt::Windows::Data::Json::JsonObject& settingsObject)
{
try
@@ -779,6 +784,17 @@ public:
Trace::AdvancedPaste_Enable(true);
m_enabled = true;
m_process_manager.start();
+
+ // Start listening for external trigger event so we can invoke the same logic as the hotkey.
+ // Note: Use start() directly instead of constructor + move assignment to avoid dangling this pointer in the thread.
+ m_triggerEventWaiter.start(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_EVENT, [this](DWORD) {
+ // Same logic as hotkeyId == 1 (m_advanced_paste_ui_hotkey)
+ Logger::trace(L"AdvancedPaste ShowUI event triggered");
+ m_process_manager.start();
+ m_process_manager.bring_to_front();
+ m_process_manager.send_message(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_MESSAGE);
+ Trace::AdvancedPaste_Invoked(L"AdvancedPasteUIEvent");
+ });
};
void Disable(bool traceEvent)
@@ -787,6 +803,9 @@ public:
{
m_process_manager.stop();
+ // Stop event listening
+ m_triggerEventWaiter.stop();
+
if (traceEvent)
{
Trace::AdvancedPaste_Enable(false);
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp
index a4158d1c66..89db922ddd 100644
--- a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp
@@ -146,7 +146,7 @@ public:
}
}
- m_showEventWaiter = EventWaiter(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT, [&](int err) {
+ m_showEventWaiter.start(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT, [&](DWORD err) {
if (m_enabled && err == ERROR_SUCCESS)
{
Logger::trace(L"{} event was signaled", CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT);
@@ -164,7 +164,7 @@ public:
}
});
- m_showAdminEventWaiter = EventWaiter(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT, [&](int err) {
+ m_showAdminEventWaiter.start(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT, [&](DWORD err) {
if (m_enabled && err == ERROR_SUCCESS)
{
Logger::trace(L"{} event was signaled", CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT);
diff --git a/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs b/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs
index 8dbec70de8..0b2739ebe1 100644
--- a/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs
+++ b/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs
@@ -67,7 +67,7 @@ namespace Hosts
services.AddSingleton();
services.AddSingleton(() =>
{
- SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Hosts, true);
+ SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Hosts);
});
services.AddSingleton();
diff --git a/src/modules/Hosts/HostsModuleInterface/dllmain.cpp b/src/modules/Hosts/HostsModuleInterface/dllmain.cpp
index 993226ac2b..fb8dd40011 100644
--- a/src/modules/Hosts/HostsModuleInterface/dllmain.cpp
+++ b/src/modules/Hosts/HostsModuleInterface/dllmain.cpp
@@ -155,7 +155,7 @@ public:
}
}
- m_showEventWaiter = EventWaiter(CommonSharedConstants::SHOW_HOSTS_EVENT, [&](int err)
+ m_showEventWaiter.start(CommonSharedConstants::SHOW_HOSTS_EVENT, [&](DWORD err)
{
if (m_enabled && err == ERROR_SUCCESS)
{
@@ -174,7 +174,7 @@ public:
}
});
- m_showAdminEventWaiter = EventWaiter(CommonSharedConstants::SHOW_HOSTS_ADMIN_EVENT, [&](int err)
+ m_showAdminEventWaiter.start(CommonSharedConstants::SHOW_HOSTS_ADMIN_EVENT, [&](DWORD err)
{
if (m_enabled && err == ERROR_SUCCESS)
{
diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj b/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj
index 261cfab1e6..b86b25a4d1 100644
--- a/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj
+++ b/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj
@@ -168,9 +168,6 @@
..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)
-
- $(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib
-
@@ -222,4 +219,4 @@
-
\ No newline at end of file
+
diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp b/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp
index a5973a396f..11cfd412b0 100644
--- a/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp
+++ b/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp
@@ -8,6 +8,8 @@
#include
#include
#include "ThemeHelper.h"
+#include
+#include
extern "C" IMAGE_DOS_HEADER __ImageBase;
@@ -103,12 +105,18 @@ private:
HANDLE m_force_light_event_handle;
HANDLE m_force_dark_event_handle;
HANDLE m_manual_override_event_handle;
+ HANDLE m_toggle_event_handle{ nullptr };
+ std::thread m_toggle_thread;
+ std::atomic m_toggle_thread_running{ false };
static const constexpr int NUM_DEFAULT_HOTKEYS = 4;
Hotkey m_toggle_theme_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'D' };
void init_settings();
+ void ToggleTheme();
+ void StartToggleListener();
+ void StopToggleListener();
public:
LightSwitchInterface()
@@ -118,6 +126,7 @@ public:
m_force_light_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_LIGHT");
m_force_dark_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_DARK");
m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
+ m_toggle_event_handle = CreateDefaultEvent(L"Local\\PowerToys-LightSwitch-ToggleEvent-d8dc2f29-8c94-4ca1-8c5f-3e2b1e3c4f5a");
init_settings();
};
@@ -130,6 +139,8 @@ public:
// Destroy the powertoy and free memory
virtual void destroy() override
{
+ // Ensure worker threads/process handles are cleaned up before destruction
+ disable();
delete this;
}
@@ -444,6 +455,8 @@ public:
Logger::info(L"Light Switch process launched successfully (PID: {}).", pi.dwProcessId);
m_process = pi.hProcess;
CloseHandle(pi.hThread);
+
+ StartToggleListener();
}
// Disable the powertoy
@@ -469,6 +482,8 @@ public:
CloseHandle(m_process);
m_process = nullptr;
}
+
+ StopToggleListener();
}
// Returns if the powertoys is enabled
@@ -530,31 +545,8 @@ public:
}
else if (hotkeyId == 0)
{
- // get current will return true if in light mode; otherwise false
Logger::info(L"[Light Switch] Hotkey triggered: Toggle Theme");
- if (g_settings.m_changeSystem)
- {
- SetSystemTheme(!GetCurrentSystemTheme());
- }
- if (g_settings.m_changeApps)
- {
- SetAppsTheme(!GetCurrentAppsTheme());
- }
-
- if (!m_manual_override_event_handle)
- {
- m_manual_override_event_handle = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
- if (!m_manual_override_event_handle)
- {
- m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
- }
- }
-
- if (m_manual_override_event_handle)
- {
- SetEvent(m_manual_override_event_handle);
- Logger::debug(L"[Light Switch] Manual override event set");
- }
+ ToggleTheme();
}
return true;
@@ -567,8 +559,80 @@ public:
{
return WaitForSingleObject(m_process, 0) == WAIT_TIMEOUT;
}
+
};
+void LightSwitchInterface::ToggleTheme()
+{
+ if (g_settings.m_changeSystem)
+ {
+ SetSystemTheme(!GetCurrentSystemTheme());
+ }
+ if (g_settings.m_changeApps)
+ {
+ SetAppsTheme(!GetCurrentAppsTheme());
+ }
+
+ if (!m_manual_override_event_handle)
+ {
+ m_manual_override_event_handle = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
+ if (!m_manual_override_event_handle)
+ {
+ m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
+ }
+ }
+
+ if (m_manual_override_event_handle)
+ {
+ SetEvent(m_manual_override_event_handle);
+ Logger::debug(L"[Light Switch] Manual override event set");
+ }
+}
+
+void LightSwitchInterface::StartToggleListener()
+{
+ if (m_toggle_thread_running || !m_toggle_event_handle)
+ {
+ return;
+ }
+
+ m_toggle_thread_running = true;
+ m_toggle_thread = std::thread([this]() {
+ while (m_toggle_thread_running)
+ {
+ const DWORD wait_result = WaitForSingleObject(m_toggle_event_handle, 500);
+ if (!m_toggle_thread_running)
+ {
+ break;
+ }
+
+ if (wait_result == WAIT_OBJECT_0)
+ {
+ ToggleTheme();
+ ResetEvent(m_toggle_event_handle);
+ }
+ }
+ });
+}
+
+void LightSwitchInterface::StopToggleListener()
+{
+ if (!m_toggle_thread_running)
+ {
+ return;
+ }
+
+ m_toggle_thread_running = false;
+ if (m_toggle_event_handle)
+ {
+ SetEvent(m_toggle_event_handle);
+ }
+ if (m_toggle_thread.joinable())
+ {
+ m_toggle_thread.join();
+ }
+}
+
std::wstring utf8_to_wstring(const std::string& str)
{
if (str.empty())
@@ -646,4 +710,4 @@ void LightSwitchInterface::init_settings()
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new LightSwitchInterface();
-}
\ No newline at end of file
+}
diff --git a/src/modules/MeasureTool/MeasureToolModuleInterface/dllmain.cpp b/src/modules/MeasureTool/MeasureToolModuleInterface/dllmain.cpp
index cfac5ce640..0c31e7f9ef 100644
--- a/src/modules/MeasureTool/MeasureToolModuleInterface/dllmain.cpp
+++ b/src/modules/MeasureTool/MeasureToolModuleInterface/dllmain.cpp
@@ -149,7 +149,7 @@ public:
init_settings();
triggerEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::MEASURE_TOOL_TRIGGER_EVENT);
- triggerEventWaiter = EventWaiter(CommonSharedConstants::MEASURE_TOOL_TRIGGER_EVENT, [this](int) {
+ triggerEventWaiter.start(CommonSharedConstants::MEASURE_TOOL_TRIGGER_EVENT, [this](DWORD) {
on_hotkey(0);
});
}
diff --git a/src/modules/MouseUtils/CursorWrap/dllmain.cpp b/src/modules/MouseUtils/CursorWrap/dllmain.cpp
index 74524ed9f9..09342d3a88 100644
--- a/src/modules/MouseUtils/CursorWrap/dllmain.cpp
+++ b/src/modules/MouseUtils/CursorWrap/dllmain.cpp
@@ -6,6 +6,7 @@
#include "../../../common/utils/resources.h"
#include "../../../common/logger/logger.h"
#include "../../../common/utils/logger_helper.h"
+#include "../../../common/interop/shared_constants.h"
#include
#include
#include
@@ -108,6 +109,12 @@ private:
// Hotkey
Hotkey m_activationHotkey{};
+ // Event-driven trigger support (for CmdPal/automation)
+ HANDLE m_triggerEventHandle = nullptr;
+ HANDLE m_terminateEventHandle = nullptr;
+ std::thread m_eventThread;
+ std::atomic_bool m_listening{ false };
+
public:
// Constructor
CursorWrap()
@@ -121,7 +128,8 @@ public:
// Destroy the powertoy and free memory
virtual void destroy() override
{
- StopMouseHook();
+ // Ensure hooks/threads/handles are torn down before deletion
+ disable();
g_cursorWrapInstance = nullptr; // Clear global instance pointer
delete this;
}
@@ -195,11 +203,54 @@ public:
{
m_enabled = true;
Trace::EnableCursorWrap(true);
-
- // Always start the mouse hook when the module is enabled
- // This ensures cursor wrapping is active immediately after enabling
- StartMouseHook();
- Logger::info("CursorWrap enabled - mouse hook started");
+
+ // Start listening for external trigger event so we can invoke the same logic as the activation hotkey.
+ m_triggerEventHandle = CreateEventW(nullptr, false, false, CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT);
+ m_terminateEventHandle = CreateEventW(nullptr, false, false, nullptr);
+ if (m_triggerEventHandle && m_terminateEventHandle)
+ {
+ m_listening = true;
+ m_eventThread = std::thread([this]() {
+ HANDLE handles[2] = { m_triggerEventHandle, m_terminateEventHandle };
+
+ // WH_MOUSE_LL callbacks are delivered to the thread that installed the hook.
+ // Ensure this thread has a message queue and pumps messages while the hook is active.
+ MSG msg;
+ PeekMessage(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
+
+ StartMouseHook();
+ Logger::info("CursorWrap enabled - mouse hook started");
+
+ while (m_listening)
+ {
+ auto res = MsgWaitForMultipleObjects(2, handles, false, INFINITE, QS_ALLINPUT);
+ if (!m_listening)
+ {
+ break;
+ }
+
+ if (res == WAIT_OBJECT_0)
+ {
+ ToggleMouseHook();
+ }
+ else if (res == WAIT_OBJECT_0 + 1)
+ {
+ break;
+ }
+ else
+ {
+ while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
+ {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+ }
+ }
+
+ StopMouseHook();
+ Logger::info("CursorWrap event listener stopped");
+ });
+ }
}
// Disable the powertoy
@@ -207,8 +258,26 @@ public:
{
m_enabled = false;
Trace::EnableCursorWrap(false);
- StopMouseHook();
- Logger::info("CursorWrap disabled - mouse hook stopped");
+
+ m_listening = false;
+ if (m_terminateEventHandle)
+ {
+ SetEvent(m_terminateEventHandle);
+ }
+ if (m_eventThread.joinable())
+ {
+ m_eventThread.join();
+ }
+ if (m_triggerEventHandle)
+ {
+ CloseHandle(m_triggerEventHandle);
+ m_triggerEventHandle = nullptr;
+ }
+ if (m_terminateEventHandle)
+ {
+ CloseHandle(m_terminateEventHandle);
+ m_terminateEventHandle = nullptr;
+ }
}
// Returns if the powertoys is enabled
@@ -240,7 +309,19 @@ public:
return false;
}
- // Toggle cursor wrapping
+ // Toggle on the thread that owns the WH_MOUSE_LL hook (the event listener thread).
+ if (m_triggerEventHandle)
+ {
+ return SetEvent(m_triggerEventHandle);
+ }
+
+ return false;
+ }
+
+ private:
+ void ToggleMouseHook()
+ {
+ // Toggle cursor wrapping.
if (m_hookActive)
{
StopMouseHook();
@@ -253,11 +334,8 @@ public:
RunComprehensiveTests();
#endif
}
-
- return true;
}
-private:
// Load the settings file.
void init_settings()
{
diff --git a/src/modules/MouseUtils/FindMyMouse/dllmain.cpp b/src/modules/MouseUtils/FindMyMouse/dllmain.cpp
index b7ffb6177a..af99f45136 100644
--- a/src/modules/MouseUtils/FindMyMouse/dllmain.cpp
+++ b/src/modules/MouseUtils/FindMyMouse/dllmain.cpp
@@ -8,6 +8,8 @@
#include
#include
#include
+#include
+#include
namespace
{
@@ -69,6 +71,9 @@ private:
// Find My Mouse specific settings
FindMyMouseSettings m_findMyMouseSettings;
+ // Event-driven trigger support
+ EventWaiter m_triggerEventWaiter;
+
// Load initial settings from the persisted values.
void init_settings();
@@ -86,6 +91,8 @@ public:
// Destroy the powertoy and free memory
virtual void destroy() override
{
+ // Ensure threads/handles are cleaned up before destruction
+ disable();
delete this;
}
@@ -150,6 +157,11 @@ public:
m_enabled = true;
Trace::EnableFindMyMouse(true);
std::thread([=]() { FindMyMouseMain(m_hModule, m_findMyMouseSettings); }).detach();
+
+ // Start listening for external trigger event so we can invoke the same logic as the hotkey.
+ m_triggerEventWaiter.start(CommonSharedConstants::FIND_MY_MOUSE_TRIGGER_EVENT, [this](DWORD) {
+ OnHotkeyEx();
+ });
}
// Disable the powertoy
@@ -158,6 +170,8 @@ public:
m_enabled = false;
Trace::EnableFindMyMouse(false);
FindMyMouseDisable();
+
+ m_triggerEventWaiter.stop();
}
// Returns if the powertoys is enabled
@@ -216,7 +230,7 @@ inline static uint8_t LegacyOpacityToAlpha(int overlayOpacityPercent)
overlayOpacityPercent = 100;
}
- // Round to nearest integer (0–255)
+ // Round to nearest integer (0–255)
return static_cast((overlayOpacityPercent * 255 + 50) / 100);
}
@@ -532,4 +546,4 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new FindMyMouse();
-}
\ No newline at end of file
+}
diff --git a/src/modules/MouseUtils/MouseHighlighter/dllmain.cpp b/src/modules/MouseUtils/MouseHighlighter/dllmain.cpp
index 45c62ae9ca..83a8837409 100644
--- a/src/modules/MouseUtils/MouseHighlighter/dllmain.cpp
+++ b/src/modules/MouseUtils/MouseHighlighter/dllmain.cpp
@@ -4,6 +4,8 @@
#include "trace.h"
#include "MouseHighlighter.h"
#include "common/utils/color.h"
+#include
+#include
namespace
{
@@ -61,6 +63,9 @@ private:
// Mouse Highlighter specific settings
MouseHighlighterSettings m_highlightSettings;
+ // Event-driven trigger support
+ EventWaiter m_triggerEventWaiter;
+
public:
// Constructor
MouseHighlighter()
@@ -72,6 +77,8 @@ public:
// Destroy the powertoy and free memory
virtual void destroy() override
{
+ // Tear down threads/handles before deletion to avoid abort() on joinable threads during shutdown
+ disable();
delete this;
}
@@ -132,6 +139,11 @@ public:
m_enabled = true;
Trace::EnableMouseHighlighter(true);
std::thread([=]() { MouseHighlighterMain(m_hModule, m_highlightSettings); }).detach();
+
+ // Start listening for external trigger event so we can invoke the same logic as the hotkey.
+ m_triggerEventWaiter.start(CommonSharedConstants::MOUSE_HIGHLIGHTER_TRIGGER_EVENT, [this](DWORD) {
+ OnHotkeyEx();
+ });
}
// Disable the powertoy
@@ -140,6 +152,8 @@ public:
m_enabled = false;
Trace::EnableMouseHighlighter(false);
MouseHighlighterDisable();
+
+ m_triggerEventWaiter.stop();
}
// Returns if the powertoys is enabled
diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp
index b460e29643..5697d83d30 100644
--- a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp
+++ b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp
@@ -4,7 +4,8 @@
#include "trace.h"
#include "InclusiveCrosshairs.h"
#include "common/utils/color.h"
-#include
+#include
+#include
#include
#include
#include
@@ -124,6 +125,9 @@ private:
// Mouse Pointer Crosshairs specific settings
InclusiveCrosshairsSettings m_inclusiveCrosshairsSettings;
+ // Event-driven trigger support
+ EventWaiter m_triggerEventWaiter;
+
public:
// Constructor
MousePointerCrosshairs()
@@ -137,11 +141,9 @@ public:
// Destroy the powertoy and free memory
virtual void destroy() override
{
- UninstallKeyboardHook();
- StopXTimer();
- StopYTimer();
+ // Ensure all background threads/handles are torn down before destruction to avoid std::terminate/abort on joinable threads
+ disable();
g_instance.store(nullptr, std::memory_order_release);
- // Release shared state so worker threads (if any) exit when weak_ptr lock fails
m_state.reset();
delete this;
}
@@ -203,6 +205,11 @@ public:
m_enabled = true;
Trace::EnableMousePointerCrosshairs(true);
std::thread([=]() { InclusiveCrosshairsMain(m_hModule, m_inclusiveCrosshairsSettings); }).detach();
+
+ // Start listening for external trigger event so we can invoke the same logic as the activation hotkey.
+ m_triggerEventWaiter.start(CommonSharedConstants::MOUSE_CROSSHAIRS_TRIGGER_EVENT, [this](DWORD) {
+ on_hotkey(0); // activation hotkey
+ });
}
// Disable the powertoy
@@ -215,6 +222,8 @@ public:
StopYTimer();
m_glideState = 0;
InclusiveCrosshairsDisable();
+
+ m_triggerEventWaiter.stop();
}
// Returns if the powertoys is enabled
@@ -901,4 +910,4 @@ private:
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new MousePointerCrosshairs();
-}
\ No newline at end of file
+}
diff --git a/src/modules/PowerOCR/PowerOCR/OCROverlay.xaml.cs b/src/modules/PowerOCR/PowerOCR/OCROverlay.xaml.cs
index 2664b8f03b..88219a4110 100644
--- a/src/modules/PowerOCR/PowerOCR/OCROverlay.xaml.cs
+++ b/src/modules/PowerOCR/PowerOCR/OCROverlay.xaml.cs
@@ -426,7 +426,7 @@ public partial class OCROverlay : Window
private void SettingsMenuItem_Click(object sender, RoutedEventArgs e)
{
WindowUtilities.CloseAllOCROverlays();
- SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerOCR, false);
+ SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerOCR);
}
private static bool CheckIfCheckingOrUnchecking(object? sender)
diff --git a/src/modules/ShortcutGuide/ShortcutGuide/main.cpp b/src/modules/ShortcutGuide/ShortcutGuide/main.cpp
index 713446403b..57a4491d41 100644
--- a/src/modules/ShortcutGuide/ShortcutGuide/main.cpp
+++ b/src/modules/ShortcutGuide/ShortcutGuide/main.cpp
@@ -121,7 +121,7 @@ int WINAPI wWinMain(_In_ HINSTANCE /*hInstance*/, _In_opt_ HINSTANCE /*hPrevInst
else
{
auto mainThreadId = GetCurrentThreadId();
- exitEventWaiter = EventWaiter(CommonSharedConstants::SHORTCUT_GUIDE_EXIT_EVENT, [mainThreadId, &window](int err) {
+ exitEventWaiter.start(CommonSharedConstants::SHORTCUT_GUIDE_EXIT_EVENT, [mainThreadId, &window](DWORD err) {
if (err != ERROR_SUCCESS)
{
Logger::error(L"Failed to wait for {} event. {}", CommonSharedConstants::SHORTCUT_GUIDE_EXIT_EVENT, get_last_error_or_default(err));
diff --git a/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/dllmain.cpp b/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/dllmain.cpp
index 5e8fe9aa1b..a870fb9ad8 100644
--- a/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/dllmain.cpp
+++ b/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/dllmain.cpp
@@ -37,7 +37,7 @@ public:
}
triggerEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT);
- triggerEventWaiter = EventWaiter(CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT, [this](int) {
+ triggerEventWaiter.start(CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT, [this](DWORD) {
OnHotkeyEx();
});
diff --git a/src/modules/Workspaces/Workspaces.ModuleServices/IWorkspaceService.cs b/src/modules/Workspaces/Workspaces.ModuleServices/IWorkspaceService.cs
new file mode 100644
index 0000000000..00300fea9f
--- /dev/null
+++ b/src/modules/Workspaces/Workspaces.ModuleServices/IWorkspaceService.cs
@@ -0,0 +1,22 @@
+// 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 PowerToys.ModuleContracts;
+using WorkspacesCsharpLibrary.Data;
+
+namespace Workspaces.ModuleServices;
+
+///
+/// Workspaces-specific operations.
+///
+public interface IWorkspaceService : IModuleService
+{
+ Task LaunchWorkspaceAsync(string workspaceId, CancellationToken cancellationToken = default);
+
+ Task LaunchEditorAsync(CancellationToken cancellationToken = default);
+
+ Task SnapshotAsync(string? targetPath = null, CancellationToken cancellationToken = default);
+
+ Task>> GetWorkspacesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/modules/Workspaces/Workspaces.ModuleServices/WorkspaceService.cs b/src/modules/Workspaces/Workspaces.ModuleServices/WorkspaceService.cs
new file mode 100644
index 0000000000..eb916e24df
--- /dev/null
+++ b/src/modules/Workspaces/Workspaces.ModuleServices/WorkspaceService.cs
@@ -0,0 +1,96 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.IO;
+using Common.UI;
+using ManagedCommon;
+using PowerToys.Interop;
+using PowerToys.ModuleContracts;
+using WorkspacesCsharpLibrary.Data;
+
+namespace Workspaces.ModuleServices;
+
+///
+/// Implementation of workspace actions for reuse across hosts.
+///
+public sealed class WorkspaceService : ModuleServiceBase, IWorkspaceService
+{
+ public static WorkspaceService Instance { get; } = new();
+
+ public override string Key => SettingsDeepLink.SettingsWindow.Workspaces.ToString();
+
+ protected override SettingsDeepLink.SettingsWindow SettingsWindow => SettingsDeepLink.SettingsWindow.Workspaces;
+
+ public override Task LaunchAsync(CancellationToken cancellationToken = default)
+ {
+ // Treat launch as invoking the Workspaces editor.
+ return LaunchEditorAsync(cancellationToken);
+ }
+
+ public Task LaunchEditorAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.WorkspacesLaunchEditorEvent());
+ eventHandle.Set();
+ return Task.FromResult(OperationResult.Ok());
+ }
+ catch (Exception ex)
+ {
+ return Task.FromResult(OperationResult.Fail($"Failed to launch Workspaces editor: {ex.Message}"));
+ }
+ }
+
+ public Task LaunchWorkspaceAsync(string workspaceId, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(workspaceId))
+ {
+ return Task.FromResult(OperationResult.Fail("Workspace id is required."));
+ }
+
+ try
+ {
+ var powertoysBaseDir = PowerToysPathResolver.GetPowerToysInstallPath();
+ if (string.IsNullOrEmpty(powertoysBaseDir))
+ {
+ return Task.FromResult(OperationResult.Fail("PowerToys installation path not found."));
+ }
+
+ var launcherPath = Path.Combine(powertoysBaseDir, "PowerToys.WorkspacesLauncher.exe");
+ var startInfo = new ProcessStartInfo(launcherPath)
+ {
+ Arguments = workspaceId,
+ UseShellExecute = true,
+ };
+
+ Process.Start(startInfo);
+ return Task.FromResult(OperationResult.Ok());
+ }
+ catch (Exception ex)
+ {
+ return Task.FromResult(OperationResult.Fail($"Failed to launch workspace: {ex.Message}"));
+ }
+ }
+
+ public Task SnapshotAsync(string? targetPath = null, CancellationToken cancellationToken = default)
+ {
+ // Snapshot orchestration is not yet exposed via events; provide a clear failure for now.
+ return Task.FromResult(OperationResult.Fail("Snapshot is not implemented for Workspaces."));
+ }
+
+ public Task>> GetWorkspacesAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var items = WorkspacesStorage.Load();
+
+ return Task.FromResult(OperationResults.Ok>(items));
+ }
+ catch (Exception ex)
+ {
+ return Task.FromResult(OperationResults.Fail>($"Failed to read workspaces: {ex.Message}"));
+ }
+ }
+}
diff --git a/src/modules/Workspaces/Workspaces.ModuleServices/Workspaces.ModuleServices.csproj b/src/modules/Workspaces/Workspaces.ModuleServices/Workspaces.ModuleServices.csproj
new file mode 100644
index 0000000000..f835138a27
--- /dev/null
+++ b/src/modules/Workspaces/Workspaces.ModuleServices/Workspaces.ModuleServices.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ enable
+ enable
+ false
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ApplicationWrapper.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ApplicationWrapper.cs
new file mode 100644
index 0000000000..5bf07b9fc1
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ApplicationWrapper.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json.Serialization;
+
+namespace WorkspacesCsharpLibrary.Data;
+
+public struct ApplicationWrapper
+{
+ public struct WindowPositionWrapper
+ {
+ [JsonPropertyName("x")]
+ public int X { get; set; }
+
+ [JsonPropertyName("y")]
+ public int Y { get; set; }
+
+ [JsonPropertyName("width")]
+ public int Width { get; set; }
+
+ [JsonPropertyName("height")]
+ public int Height { get; set; }
+ }
+
+ [JsonPropertyName("id")]
+ public string Id { get; set; }
+
+ [JsonPropertyName("application")]
+ public string Application { get; set; }
+
+ [JsonPropertyName("application-path")]
+ public string ApplicationPath { get; set; }
+
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ [JsonPropertyName("package-full-name")]
+ public string PackageFullName { get; set; }
+
+ [JsonPropertyName("app-user-model-id")]
+ public string AppUserModelId { get; set; }
+
+ [JsonPropertyName("pwa-app-id")]
+ public string PwaAppId { get; set; }
+
+ [JsonPropertyName("command-line-arguments")]
+ public string CommandLineArguments { get; set; }
+
+ [JsonPropertyName("is-elevated")]
+ public bool IsElevated { get; set; }
+
+ [JsonPropertyName("can-launch-elevated")]
+ public bool CanLaunchElevated { get; set; }
+
+ [JsonPropertyName("minimized")]
+ public bool Minimized { get; set; }
+
+ [JsonPropertyName("maximized")]
+ public bool Maximized { get; set; }
+
+ [JsonPropertyName("position")]
+ public WindowPositionWrapper Position { get; set; }
+
+ [JsonPropertyName("monitor")]
+ public int Monitor { get; set; }
+
+ [JsonPropertyName("version")]
+ public string Version { get; set; }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/InvokePoint.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/InvokePoint.cs
similarity index 51%
rename from src/modules/Workspaces/WorkspacesEditor/Data/InvokePoint.cs
rename to src/modules/Workspaces/WorkspacesCsharpLibrary/Data/InvokePoint.cs
index fe41a65bd7..3f24d51f28 100644
--- a/src/modules/Workspaces/WorkspacesEditor/Data/InvokePoint.cs
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/InvokePoint.cs
@@ -2,13 +2,12 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-namespace WorkspacesEditor.Data
+namespace WorkspacesCsharpLibrary.Data;
+
+public enum InvokePoint
{
- /* sync with workspaces-common */
- public enum InvokePoint
- {
- EditorButton = 0,
- Shortcut,
- LaunchAndEdit,
- }
+ EditorButton = 0,
+ Shortcut,
+ LaunchAndEdit,
+ CommandPaletteExtension,
}
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/MonitorConfigurationWrapper.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/MonitorConfigurationWrapper.cs
new file mode 100644
index 0000000000..1c48dee1ab
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/MonitorConfigurationWrapper.cs
@@ -0,0 +1,43 @@
+// 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;
+
+namespace WorkspacesCsharpLibrary.Data;
+
+public struct MonitorConfigurationWrapper
+{
+ public struct MonitorRectWrapper
+ {
+ [JsonPropertyName("top")]
+ public int Top { get; set; }
+
+ [JsonPropertyName("left")]
+ public int Left { get; set; }
+
+ [JsonPropertyName("width")]
+ public int Width { get; set; }
+
+ [JsonPropertyName("height")]
+ public int Height { get; set; }
+ }
+
+ [JsonPropertyName("id")]
+ public string Id { get; set; }
+
+ [JsonPropertyName("instance-id")]
+ public string InstanceId { get; set; }
+
+ [JsonPropertyName("monitor-number")]
+ public int MonitorNumber { get; set; }
+
+ [JsonPropertyName("dpi")]
+ public int Dpi { get; set; }
+
+ [JsonPropertyName("monitor-rect-dpi-aware")]
+ public MonitorRectWrapper MonitorRectDpiAware { get; set; }
+
+ [JsonPropertyName("monitor-rect-dpi-unaware")]
+ public MonitorRectWrapper MonitorRectDpiUnaware { get; set; }
+}
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectData.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectData.cs
new file mode 100644
index 0000000000..04006cb2c5
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectData.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using WorkspacesCsharpLibrary.Data;
+
+namespace WorkspacesCsharpLibrary.Data;
+
+public class ProjectData : WorkspacesEditorData
+{
+}
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectWrapper.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectWrapper.cs
new file mode 100644
index 0000000000..3f0f4dbc58
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectWrapper.cs
@@ -0,0 +1,26 @@
+// 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;
+
+namespace WorkspacesCsharpLibrary.Data;
+
+public struct ProjectWrapper
+{
+ public string Id { get; set; }
+
+ public string Name { get; set; }
+
+ public long CreationTime { get; set; }
+
+ public long LastLaunchedTime { get; set; }
+
+ public bool IsShortcutNeeded { get; set; }
+
+ public bool MoveExistingWindows { get; set; }
+
+ public List MonitorConfiguration { get; set; }
+
+ public List Applications { get; set; }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/TempProjectData.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/TempProjectData.cs
similarity index 82%
rename from src/modules/Workspaces/WorkspacesEditor/Data/TempProjectData.cs
rename to src/modules/Workspaces/WorkspacesCsharpLibrary/Data/TempProjectData.cs
index a1600885b9..c5e4a3ce25 100644
--- a/src/modules/Workspaces/WorkspacesEditor/Data/TempProjectData.cs
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/TempProjectData.cs
@@ -2,9 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using WorkspacesEditor.Utils;
+using WorkspacesCsharpLibrary.Data;
+using WorkspacesCsharpLibrary.Utils;
-namespace WorkspacesEditor.Data
+namespace WorkspacesCsharpLibrary.Data
{
public class TempProjectData : ProjectData
{
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesData.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesData.cs
new file mode 100644
index 0000000000..6395bffdba
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesData.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using WorkspacesCsharpLibrary.Utils;
+using static WorkspacesCsharpLibrary.Data.WorkspacesData;
+
+namespace WorkspacesCsharpLibrary.Data;
+
+public class WorkspacesData : WorkspacesEditorData
+{
+ public string File => FolderUtils.DataFolder() + "\\workspaces.json";
+
+ public struct WorkspacesListWrapper
+ {
+ public List Workspaces { get; set; }
+ }
+
+ public enum OrderBy
+ {
+ LastViewed = 0,
+ Created = 1,
+ Name = 2,
+ Unknown = 3,
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesEditorData`1.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesEditorData`1.cs
new file mode 100644
index 0000000000..eed73af224
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesEditorData`1.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using WorkspacesCsharpLibrary.Utils;
+
+namespace WorkspacesCsharpLibrary.Data;
+
+///
+/// Shared JSON serializer helper for Workspaces payloads.
+///
+public class WorkspacesEditorData
+{
+ [RequiresUnreferencedCode("JSON serialization uses reflection-based serializer.")]
+ [RequiresDynamicCode("JSON serialization uses reflection-based serializer.")]
+ public T Read(string file)
+ {
+ IOUtils ioUtils = new();
+ string data = ioUtils.ReadFile(file);
+ return JsonSerializer.Deserialize(data, WorkspacesJsonOptions.EditorOptions)!;
+ }
+
+ [RequiresUnreferencedCode("JSON serialization uses reflection-based serializer.")]
+ [RequiresDynamicCode("JSON serialization uses reflection-based serializer.")]
+ public string Serialize(T data)
+ {
+ return JsonSerializer.Serialize(data, WorkspacesJsonOptions.EditorOptions);
+ }
+
+ [RequiresUnreferencedCode("JSON serialization uses reflection-based serializer.")]
+ [RequiresDynamicCode("JSON serialization uses reflection-based serializer.")]
+ public T Deserialize(string json)
+ {
+ return JsonSerializer.Deserialize(json, WorkspacesJsonOptions.EditorOptions)!;
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesJsonOptions.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesJsonOptions.cs
new file mode 100644
index 0000000000..d9d00152b3
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesJsonOptions.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json;
+using WorkspacesCsharpLibrary.Utils;
+
+namespace WorkspacesCsharpLibrary.Data;
+
+internal static class WorkspacesJsonOptions
+{
+ internal static readonly JsonSerializerOptions EditorOptions = new()
+ {
+ PropertyNamingPolicy = new DashCaseNamingPolicy(),
+ WriteIndented = true,
+ };
+}
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorage.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorage.cs
new file mode 100644
index 0000000000..ea33884577
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorage.cs
@@ -0,0 +1,96 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace WorkspacesCsharpLibrary.Data;
+
+///
+/// Lightweight reader for persisted workspaces.
+///
+public static class WorkspacesStorage
+{
+ public static IReadOnlyList Load()
+ {
+ var filePath = GetDefaultFilePath();
+ if (!File.Exists(filePath))
+ {
+ return [];
+ }
+
+ try
+ {
+ var json = File.ReadAllText(filePath);
+ var data = JsonSerializer.Deserialize(json, WorkspacesStorageJsonContext.Default.WorkspacesFile);
+
+ if (data?.Workspaces == null)
+ {
+ return [];
+ }
+
+ return data.Workspaces
+ .Where(ws => !string.IsNullOrWhiteSpace(ws.Id) && !string.IsNullOrWhiteSpace(ws.Name))
+ .Select(ws => new ProjectWrapper
+ {
+ Id = ws.Id!,
+ Name = ws.Name!,
+ Applications = ws.Applications ?? new List(),
+ CreationTime = ws.CreationTime,
+ LastLaunchedTime = ws.LastLaunchedTime,
+ IsShortcutNeeded = ws.IsShortcutNeeded,
+ MoveExistingWindows = ws.MoveExistingWindows,
+ MonitorConfiguration = ws.MonitorConfiguration ?? new List(),
+ })
+ .ToList()
+ .AsReadOnly();
+ }
+ catch
+ {
+ return Array.Empty();
+ }
+ }
+
+ public static string GetDefaultFilePath()
+ {
+ var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ return Path.Combine(localAppData, "Microsoft", "PowerToys", "Workspaces", "workspaces.json");
+ }
+
+ internal sealed class WorkspacesFile
+ {
+ public List Workspaces { get; set; } = new();
+ }
+
+ internal sealed class WorkspaceProject
+ {
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ [JsonPropertyName("applications")]
+ public List Applications { get; set; } = new();
+
+ [JsonPropertyName("monitor-configuration")]
+ public List MonitorConfiguration { get; set; } = new();
+
+ [JsonPropertyName("creation-time")]
+ public long CreationTime { get; set; }
+
+ [JsonPropertyName("last-launched-time")]
+ public long LastLaunchedTime { get; set; }
+
+ [JsonPropertyName("is-shortcut-needed")]
+ public bool IsShortcutNeeded { get; set; }
+
+ [JsonPropertyName("move-existing-windows")]
+ public bool MoveExistingWindows { get; set; }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorageJsonContext.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorageJsonContext.cs
new file mode 100644
index 0000000000..45ba31a03d
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorageJsonContext.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json.Serialization;
+
+namespace WorkspacesCsharpLibrary.Data;
+
+[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
+[JsonSerializable(typeof(WorkspacesStorage.WorkspacesFile))]
+[JsonSerializable(typeof(WorkspacesStorage.WorkspaceProject))]
+[JsonSerializable(typeof(ApplicationWrapper))]
+[JsonSerializable(typeof(ApplicationWrapper.WindowPositionWrapper))]
+[JsonSerializable(typeof(MonitorConfigurationWrapper))]
+[JsonSerializable(typeof(MonitorConfigurationWrapper.MonitorRectWrapper))]
+internal sealed partial class WorkspacesStorageJsonContext : JsonSerializerContext
+{
+}
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Models/BaseApplication.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Models/BaseApplication.cs
index e3c0bff508..897bd97de5 100644
--- a/src/modules/Workspaces/WorkspacesCsharpLibrary/Models/BaseApplication.cs
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Models/BaseApplication.cs
@@ -16,7 +16,7 @@ using Windows.Management.Deployment;
namespace WorkspacesCsharpLibrary.Models
{
- public class BaseApplication : INotifyPropertyChanged, IDisposable
+ public partial class BaseApplication : INotifyPropertyChanged, IDisposable
{
public event PropertyChangedEventHandler PropertyChanged;
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/DashCaseNamingPolicy.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/DashCaseNamingPolicy.cs
new file mode 100644
index 0000000000..cb1a0f1377
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/DashCaseNamingPolicy.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json;
+using WorkspacesCsharpLibrary.Utils;
+
+namespace WorkspacesCsharpLibrary.Utils;
+
+public class DashCaseNamingPolicy : JsonNamingPolicy
+{
+ public static DashCaseNamingPolicy Instance { get; } = new DashCaseNamingPolicy();
+
+ public override string ConvertName(string name)
+ {
+ return name.UpperCamelCaseToDashCase();
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/FolderUtils.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/FolderUtils.cs
new file mode 100644
index 0000000000..cef2aae957
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/FolderUtils.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+
+namespace WorkspacesCsharpLibrary.Utils;
+
+public class FolderUtils
+{
+ public static string Desktop()
+ {
+ return Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
+ }
+
+ public static string Temp()
+ {
+ return Path.GetTempPath();
+ }
+
+ // Note: the same path should be used in SnapshotTool and Launcher
+ public static string DataFolder()
+ {
+ return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Microsoft\\PowerToys\\Workspaces";
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/IOUtils.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/IOUtils.cs
new file mode 100644
index 0000000000..8ceb703cc4
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/IOUtils.cs
@@ -0,0 +1,47 @@
+// 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;
+using System.IO.Abstractions;
+using System.Threading.Tasks;
+
+namespace WorkspacesCsharpLibrary.Utils;
+
+public class IOUtils
+{
+ private readonly IFileSystem _fileSystem = new FileSystem();
+
+ public void WriteFile(string fileName, string data)
+ {
+ _fileSystem.File.WriteAllText(fileName, data);
+ }
+
+ public string ReadFile(string fileName)
+ {
+ if (_fileSystem.File.Exists(fileName))
+ {
+ int attempts = 0;
+ while (attempts < 10)
+ {
+ try
+ {
+ using FileSystemStream inputStream = _fileSystem.File.Open(fileName, FileMode.Open);
+ using StreamReader reader = new(inputStream);
+ string data = reader.ReadToEnd();
+ inputStream.Close();
+ return data;
+ }
+ catch (Exception)
+ {
+ Task.Delay(10).Wait();
+ }
+
+ attempts++;
+ }
+ }
+
+ return string.Empty;
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/StringUtils.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/StringUtils.cs
new file mode 100644
index 0000000000..0b85911fa8
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/StringUtils.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Linq;
+
+namespace WorkspacesCsharpLibrary.Utils;
+
+public static class StringUtils
+{
+ public static string UpperCamelCaseToDashCase(this string str)
+ {
+ // If it's a single letter variable, leave it as it is
+ return str.Length == 1
+ ? str
+ : string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "-" + x : x.ToString())).ToLowerInvariant();
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/WorkspacesCsharpLibrary.csproj b/src/modules/Workspaces/WorkspacesCsharpLibrary/WorkspacesCsharpLibrary.csproj
index eea9001b12..501f6b4f0c 100644
--- a/src/modules/Workspaces/WorkspacesCsharpLibrary/WorkspacesCsharpLibrary.csproj
+++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/WorkspacesCsharpLibrary.csproj
@@ -1,11 +1,13 @@

+
PowerToys.WorkspacesCsharpLibrary
PowerToys Workspaces Csharp Library
PowerToys Workspaces Csharp Library
+ enable
true
true
false
@@ -15,4 +17,8 @@
PowerToys.WorkspacesCsharpLibrary
-
\ No newline at end of file
+
+
+
+
+
diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/ProjectData.cs b/src/modules/Workspaces/WorkspacesEditor/Data/ProjectData.cs
deleted file mode 100644
index 27c4290909..0000000000
--- a/src/modules/Workspaces/WorkspacesEditor/Data/ProjectData.cs
+++ /dev/null
@@ -1,101 +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.Collections.Generic;
-using static WorkspacesEditor.Data.ProjectData;
-
-namespace WorkspacesEditor.Data
-{
- public class ProjectData : WorkspacesEditorData
- {
- public struct ApplicationWrapper
- {
- public struct WindowPositionWrapper
- {
- public int X { get; set; }
-
- public int Y { get; set; }
-
- public int Width { get; set; }
-
- public int Height { get; set; }
- }
-
- public string Id { get; set; }
-
- public string Application { get; set; }
-
- public string ApplicationPath { get; set; }
-
- public string Title { get; set; }
-
- public string PackageFullName { get; set; }
-
- public string AppUserModelId { get; set; }
-
- public string PwaAppId { get; set; }
-
- public string CommandLineArguments { get; set; }
-
- public bool IsElevated { get; set; }
-
- public bool CanLaunchElevated { get; set; }
-
- public bool Minimized { get; set; }
-
- public bool Maximized { get; set; }
-
- public WindowPositionWrapper Position { get; set; }
-
- public int Monitor { get; set; }
-
- public string Version { get; set; }
- }
-
- public struct MonitorConfigurationWrapper
- {
- public struct MonitorRectWrapper
- {
- public int Top { get; set; }
-
- public int Left { get; set; }
-
- public int Width { get; set; }
-
- public int Height { get; set; }
- }
-
- public string Id { get; set; }
-
- public string InstanceId { get; set; }
-
- public int MonitorNumber { get; set; }
-
- public int Dpi { get; set; }
-
- public MonitorRectWrapper MonitorRectDpiAware { get; set; }
-
- public MonitorRectWrapper MonitorRectDpiUnaware { get; set; }
- }
-
- public struct ProjectWrapper
- {
- public string Id { get; set; }
-
- public string Name { get; set; }
-
- public long CreationTime { get; set; }
-
- public long LastLaunchedTime { get; set; }
-
- public bool IsShortcutNeeded { get; set; }
-
- public bool MoveExistingWindows { get; set; }
-
- public List MonitorConfiguration { get; set; }
-
- public List Applications { get; set; }
- }
- }
-}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesData.cs b/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesData.cs
deleted file mode 100644
index 6e0d015905..0000000000
--- a/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesData.cs
+++ /dev/null
@@ -1,30 +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.Collections.Generic;
-using WorkspacesEditor.Utils;
-
-using static WorkspacesEditor.Data.ProjectData;
-using static WorkspacesEditor.Data.WorkspacesData;
-
-namespace WorkspacesEditor.Data
-{
- public class WorkspacesData : WorkspacesEditorData
- {
- public string File => FolderUtils.DataFolder() + "\\workspaces.json";
-
- public struct WorkspacesListWrapper
- {
- public List Workspaces { get; set; }
- }
-
- public enum OrderBy
- {
- LastViewed = 0,
- Created = 1,
- Name = 2,
- Unknown = 3,
- }
- }
-}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesEditorData`1.cs b/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesEditorData`1.cs
deleted file mode 100644
index c2ad0a70a4..0000000000
--- a/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesEditorData`1.cs
+++ /dev/null
@@ -1,33 +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.Text.Json;
-using WorkspacesEditor.Utils;
-
-namespace WorkspacesEditor.Data
-{
- public class WorkspacesEditorData
- {
- protected JsonSerializerOptions JsonOptions
- {
- get => new()
- {
- PropertyNamingPolicy = new DashCaseNamingPolicy(),
- WriteIndented = true,
- };
- }
-
- public T Read(string file)
- {
- IOUtils ioUtils = new();
- string data = ioUtils.ReadFile(file);
- return JsonSerializer.Deserialize(data, JsonOptions);
- }
-
- public string Serialize(T data)
- {
- return JsonSerializer.Serialize(data, JsonOptions);
- }
- }
-}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Models/Project.cs b/src/modules/Workspaces/WorkspacesEditor/Models/Project.cs
index a3b82355b7..bb5fd1c93e 100644
--- a/src/modules/Workspaces/WorkspacesEditor/Models/Project.cs
+++ b/src/modules/Workspaces/WorkspacesEditor/Models/Project.cs
@@ -13,7 +13,7 @@ using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using ManagedCommon;
-using WorkspacesEditor.Data;
+using WorkspacesCsharpLibrary.Data;
using WorkspacesEditor.Utils;
namespace WorkspacesEditor.Models
@@ -226,7 +226,7 @@ namespace WorkspacesEditor.Models
}
}
- public Project(ProjectData.ProjectWrapper project)
+ public Project(ProjectWrapper project)
{
Id = project.Id;
Name = project.Name;
@@ -237,7 +237,7 @@ namespace WorkspacesEditor.Models
Monitors = [];
Applications = [];
- foreach (ProjectData.ApplicationWrapper app in project.Applications)
+ foreach (ApplicationWrapper app in project.Applications)
{
Models.Application newApp = new()
{
@@ -269,7 +269,7 @@ namespace WorkspacesEditor.Models
Applications.Add(newApp);
}
- foreach (ProjectData.MonitorConfigurationWrapper monitor in project.MonitorConfiguration)
+ foreach (MonitorConfigurationWrapper monitor in project.MonitorConfiguration)
{
System.Windows.Rect dpiAware = new(monitor.MonitorRectDpiAware.Left, monitor.MonitorRectDpiAware.Top, monitor.MonitorRectDpiAware.Width, monitor.MonitorRectDpiAware.Height);
System.Windows.Rect dpiUnaware = new(monitor.MonitorRectDpiUnaware.Left, monitor.MonitorRectDpiUnaware.Top, monitor.MonitorRectDpiUnaware.Width, monitor.MonitorRectDpiUnaware.Height);
diff --git a/src/modules/Workspaces/WorkspacesEditor/Utils/DashCaseNamingPolicy.cs b/src/modules/Workspaces/WorkspacesEditor/Utils/DashCaseNamingPolicy.cs
deleted file mode 100644
index 3e8857a076..0000000000
--- a/src/modules/Workspaces/WorkspacesEditor/Utils/DashCaseNamingPolicy.cs
+++ /dev/null
@@ -1,18 +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.Text.Json;
-
-namespace WorkspacesEditor.Utils
-{
- public class DashCaseNamingPolicy : JsonNamingPolicy
- {
- public static DashCaseNamingPolicy Instance { get; } = new DashCaseNamingPolicy();
-
- public override string ConvertName(string name)
- {
- return name.UpperCamelCaseToDashCase();
- }
- }
-}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Utils/FolderUtils.cs b/src/modules/Workspaces/WorkspacesEditor/Utils/FolderUtils.cs
deleted file mode 100644
index fc12593e09..0000000000
--- a/src/modules/Workspaces/WorkspacesEditor/Utils/FolderUtils.cs
+++ /dev/null
@@ -1,28 +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.IO;
-
-namespace WorkspacesEditor.Utils
-{
- public class FolderUtils
- {
- public static string Desktop()
- {
- return Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
- }
-
- public static string Temp()
- {
- return Path.GetTempPath();
- }
-
- // Note: the same path should be used in SnapshotTool and Launcher
- public static string DataFolder()
- {
- return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Microsoft\\PowerToys\\Workspaces";
- }
- }
-}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Utils/IOUtils.cs b/src/modules/Workspaces/WorkspacesEditor/Utils/IOUtils.cs
deleted file mode 100644
index fe69777593..0000000000
--- a/src/modules/Workspaces/WorkspacesEditor/Utils/IOUtils.cs
+++ /dev/null
@@ -1,52 +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.IO;
-using System.IO.Abstractions;
-using System.Threading.Tasks;
-
-namespace WorkspacesEditor.Utils
-{
- public class IOUtils
- {
- private readonly IFileSystem _fileSystem = new FileSystem();
-
- public IOUtils()
- {
- }
-
- public void WriteFile(string fileName, string data)
- {
- _fileSystem.File.WriteAllText(fileName, data);
- }
-
- public string ReadFile(string fileName)
- {
- if (_fileSystem.File.Exists(fileName))
- {
- int attempts = 0;
- while (attempts < 10)
- {
- try
- {
- using FileSystemStream inputStream = _fileSystem.File.Open(fileName, FileMode.Open);
- using StreamReader reader = new(inputStream);
- string data = reader.ReadToEnd();
- inputStream.Close();
- return data;
- }
- catch (Exception)
- {
- Task.Delay(10).Wait();
- }
-
- attempts++;
- }
- }
-
- return string.Empty;
- }
- }
-}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Utils/WorkspacesEditorIO.cs b/src/modules/Workspaces/WorkspacesEditor/Utils/WorkspacesEditorIO.cs
index a66145b484..b0b3dc9a50 100644
--- a/src/modules/Workspaces/WorkspacesEditor/Utils/WorkspacesEditorIO.cs
+++ b/src/modules/Workspaces/WorkspacesEditor/Utils/WorkspacesEditorIO.cs
@@ -6,9 +6,9 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-
using ManagedCommon;
-using WorkspacesEditor.Data;
+using WorkspacesCsharpLibrary.Data;
+using WorkspacesCsharpLibrary.Utils;
using WorkspacesEditor.Models;
using WorkspacesEditor.ViewModels;
@@ -81,7 +81,7 @@ namespace WorkspacesEditor.Utils
foreach (Project project in workspaces)
{
- ProjectData.ProjectWrapper wrapper = new()
+ ProjectWrapper wrapper = new()
{
Id = project.Id,
Name = project.Name,
@@ -95,7 +95,7 @@ namespace WorkspacesEditor.Utils
foreach (Application app in project.Applications.Where(x => x.IsIncluded))
{
- wrapper.Applications.Add(new ProjectData.ApplicationWrapper
+ wrapper.Applications.Add(new ApplicationWrapper
{
Id = app.Id,
Application = app.AppName,
@@ -110,7 +110,7 @@ namespace WorkspacesEditor.Utils
Version = app.Version,
Maximized = app.Maximized,
Minimized = app.Minimized,
- Position = new ProjectData.ApplicationWrapper.WindowPositionWrapper
+ Position = new ApplicationWrapper.WindowPositionWrapper
{
X = app.Position.X,
Y = app.Position.Y,
@@ -123,20 +123,20 @@ namespace WorkspacesEditor.Utils
foreach (MonitorSetup monitor in project.Monitors)
{
- wrapper.MonitorConfiguration.Add(new ProjectData.MonitorConfigurationWrapper
+ wrapper.MonitorConfiguration.Add(new MonitorConfigurationWrapper
{
Id = monitor.MonitorName,
InstanceId = monitor.MonitorInstanceId,
MonitorNumber = monitor.MonitorNumber,
Dpi = monitor.Dpi,
- MonitorRectDpiAware = new ProjectData.MonitorConfigurationWrapper.MonitorRectWrapper
+ MonitorRectDpiAware = new MonitorConfigurationWrapper.MonitorRectWrapper
{
Left = (int)monitor.MonitorDpiAwareBounds.Left,
Top = (int)monitor.MonitorDpiAwareBounds.Top,
Width = (int)monitor.MonitorDpiAwareBounds.Width,
Height = (int)monitor.MonitorDpiAwareBounds.Height,
},
- MonitorRectDpiUnaware = new ProjectData.MonitorConfigurationWrapper.MonitorRectWrapper
+ MonitorRectDpiUnaware = new MonitorConfigurationWrapper.MonitorRectWrapper
{
Left = (int)monitor.MonitorDpiUnawareBounds.Left,
Top = (int)monitor.MonitorDpiUnawareBounds.Top,
@@ -163,7 +163,7 @@ namespace WorkspacesEditor.Utils
private bool AddWorkspaces(MainViewModel mainViewModel, WorkspacesData.WorkspacesListWrapper workspaces)
{
- foreach (ProjectData.ProjectWrapper project in workspaces.Workspaces)
+ foreach (ProjectWrapper project in workspaces.Workspaces)
{
mainViewModel.Workspaces.Add(new Project(project));
}
diff --git a/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs b/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs
index 30b28f4cd8..9c76c26fa0 100644
--- a/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs
+++ b/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs
@@ -18,12 +18,12 @@ using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using WorkspacesCsharpLibrary;
-using WorkspacesEditor.Data;
+using WorkspacesCsharpLibrary.Data;
+using WorkspacesCsharpLibrary.Utils;
using WorkspacesEditor.Models;
using WorkspacesEditor.Telemetry;
using WorkspacesEditor.Utils;
-
-using static WorkspacesEditor.Data.WorkspacesData;
+using static WorkspacesCsharpLibrary.Data.WorkspacesData;
namespace WorkspacesEditor.ViewModels
{
diff --git a/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditor.csproj b/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditor.csproj
index 3f7d153e56..71e91979bd 100644
--- a/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditor.csproj
+++ b/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditor.csproj
@@ -78,6 +78,15 @@
+
+
+
+
+
+
+
+
+
True
@@ -96,4 +105,4 @@
Settings.Designer.cs
-
\ No newline at end of file
+
diff --git a/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditorPage.xaml.cs b/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditorPage.xaml.cs
index 1dde4d1114..4a573584ad 100644
--- a/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditorPage.xaml.cs
+++ b/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditorPage.xaml.cs
@@ -9,7 +9,7 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
-using WorkspacesEditor.Data;
+using WorkspacesCsharpLibrary.Data;
using WorkspacesEditor.Models;
using WorkspacesEditor.ViewModels;
diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs
index 6e9ad24379..1e9bf665c5 100644
--- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs
+++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs
@@ -3,15 +3,12 @@
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
-
-using Workspaces.Data;
-
using static WorkspacesLauncherUI.Data.AppLaunchData;
using static WorkspacesLauncherUI.Data.AppLaunchInfosData;
namespace WorkspacesLauncherUI.Data
{
- public class AppLaunchData : WorkspacesUIData
+ public class AppLaunchData : WorkspacesCsharpLibrary.Data.WorkspacesEditorData
{
public struct AppLaunchDataWrapper
{
diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs
index c01ffaba8c..5a19ccde15 100644
--- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs
+++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs
@@ -3,14 +3,11 @@
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
-
-using Workspaces.Data;
-
using static WorkspacesLauncherUI.Data.AppLaunchInfoData;
namespace WorkspacesLauncherUI.Data
{
- public class AppLaunchInfoData : WorkspacesUIData
+ public class AppLaunchInfoData : WorkspacesCsharpLibrary.Data.WorkspacesEditorData
{
public struct AppLaunchInfoWrapper
{
diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs
index cb00cb4478..a656712d9a 100644
--- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs
+++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs
@@ -4,15 +4,12 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
-
-using Workspaces.Data;
-
using static WorkspacesLauncherUI.Data.AppLaunchInfoData;
using static WorkspacesLauncherUI.Data.AppLaunchInfosData;
namespace WorkspacesLauncherUI.Data
{
- public class AppLaunchInfosData : WorkspacesUIData
+ public class AppLaunchInfosData : WorkspacesCsharpLibrary.Data.WorkspacesEditorData
{
public struct AppLaunchInfoListWrapper
{
diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs
deleted file mode 100644
index 5e9b88a728..0000000000
--- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs
+++ /dev/null
@@ -1,35 +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.Text.Json;
-
-using WorkspacesLauncherUI.Utils;
-
-namespace Workspaces.Data
-{
- public class WorkspacesUIData
- {
- protected JsonSerializerOptions JsonOptions
- {
- get
- {
- return new JsonSerializerOptions
- {
- PropertyNamingPolicy = new DashCaseNamingPolicy(),
- WriteIndented = true,
- };
- }
- }
-
- public T Deserialize(string data)
- {
- return JsonSerializer.Deserialize(data, JsonOptions);
- }
-
- public string Serialize(T data)
- {
- return JsonSerializer.Serialize(data, JsonOptions);
- }
- }
-}
diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs b/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs
index aa029d7ea2..5b358686de 100644
--- a/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs
+++ b/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs
@@ -6,13 +6,11 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
-using System.Diagnostics;
using ManagedCommon;
using WorkspacesCsharpLibrary;
using WorkspacesLauncherUI.Data;
using WorkspacesLauncherUI.Models;
-using WorkspacesLauncherUI.Utils;
namespace WorkspacesLauncherUI.ViewModels
{
diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/WorkspacesLauncherUI.csproj b/src/modules/Workspaces/WorkspacesLauncherUI/WorkspacesLauncherUI.csproj
index 839c08f90d..f55d30205f 100644
--- a/src/modules/Workspaces/WorkspacesLauncherUI/WorkspacesLauncherUI.csproj
+++ b/src/modules/Workspaces/WorkspacesLauncherUI/WorkspacesLauncherUI.csproj
@@ -1,102 +1,102 @@

-
-
-
-
-
- PowerToys.WorkspacesLauncherUI
- PowerToys Workspaces Editor
- PowerToys Workspaces Editor
- WinExe
- true
- true
- false
- false
- true
- ..\..\..\..\$(Platform)\$(Configuration)
-
-
-
- {9C53CC25-0623-4569-95BC-B05410675EE3}
- {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- true
-
+
+
+
-
- ..\Assets\Workspaces\Workspaces.ico
-
-
- app.manifest
- PowerToys.WorkspacesLauncherUI
-
-
-
-
- Assets\Workspaces\%(Filename)%(Extension)
- PreserveNewest
-
-
+
+ PowerToys.WorkspacesLauncherUI
+ PowerToys Workspaces Launcher UI
+ PowerToys Workspaces Launcher UI
+ WinExe
+ true
+ true
+ false
+ false
+ true
+ ..\..\..\..\$(Platform)\$(Configuration)
+
-
-
- tlbimp
- 0
- 1
- f935dc20-1cf0-11d0-adb9-00c04fd58a0b
- 0
- false
- true
-
-
- tlbimp
- 0
- 1
- 50a7e9b0-70ef-11d1-b75a-00a0c90564fe
- 0
- false
- true
-
-
-
-
-
- PublicResXFileCodeGenerator
- Resources.Designer.cs
-
-
-
+
+ {9C53CC25-0623-4569-95BC-B05410675EE3}
+ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ true
+
-
-
-
-
-
+
+ ..\Assets\Workspaces\Workspaces.ico
+
+
+ app.manifest
+ PowerToys.WorkspacesLauncherUI
+
-
-
-
-
-
-
-
-
-
-
-
- True
- True
- Resources.resx
-
-
- True
- True
- Settings.settings
-
-
-
-
- SettingsSingleFileGenerator
- Settings.Designer.cs
-
-
+
+
+ Assets\Workspaces\%(Filename)%(Extension)
+ PreserveNewest
+
+
+
+
+
+ tlbimp
+ 0
+ 1
+ f935dc20-1cf0-11d0-adb9-00c04fd58a0b
+ 0
+ false
+ true
+
+
+ tlbimp
+ 0
+ 1
+ 50a7e9b0-70ef-11d1-b75a-00a0c90564fe
+ 0
+ false
+ true
+
+
+
+
+
+ PublicResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ True
+ Settings.settings
+
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
\ No newline at end of file
diff --git a/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp b/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp
index 90c898fc5f..a4caf01649 100644
--- a/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp
+++ b/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp
@@ -201,7 +201,7 @@ public:
Logger::error(message.value());
}
}
- m_toggleEditorEventWaiter = EventWaiter(CommonSharedConstants::WORKSPACES_LAUNCH_EDITOR_EVENT, [&](int err) {
+ m_toggleEditorEventWaiter.start(CommonSharedConstants::WORKSPACES_LAUNCH_EDITOR_EVENT, [&](DWORD err) {
if (err == ERROR_SUCCESS)
{
Logger::trace(L"{} event was signaled", CommonSharedConstants::WORKSPACES_LAUNCH_EDITOR_EVENT);
diff --git a/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj b/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj
index b1f91a5228..77c299f303 100644
--- a/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj
+++ b/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj
@@ -369,4 +369,4 @@
-
\ No newline at end of file
+
diff --git a/src/modules/ZoomIt/ZoomIt/Zoomit.cpp b/src/modules/ZoomIt/ZoomIt/Zoomit.cpp
index 71f09fc578..e38ca07f66 100644
--- a/src/modules/ZoomIt/ZoomIt/Zoomit.cpp
+++ b/src/modules/ZoomIt/ZoomIt/Zoomit.cpp
@@ -28,6 +28,20 @@
#include
#include
#include
+#include
+#include
+#endif // __ZOOMIT_POWERTOYS__
+
+#ifdef __ZOOMIT_POWERTOYS__
+enum class ZoomItCommand
+{
+ Zoom,
+ Draw,
+ Break,
+ LiveZoom,
+ Snip,
+ Record,
+};
#endif // __ZOOMIT_POWERTOYS__
namespace winrt
@@ -172,7 +186,6 @@ std::wstring g_RecordingSaveLocationGIF;
winrt::IDirect3DDevice g_RecordDevice{ nullptr };
std::shared_ptr g_RecordingSession = nullptr;
std::shared_ptr g_GifRecordingSession = nullptr;
-
type_pGetMonitorInfo pGetMonitorInfo;
type_MonitorFromPoint pMonitorFromPoint;
type_pSHAutoComplete pSHAutoComplete;
@@ -7712,6 +7725,53 @@ HWND InitInstance( HINSTANCE hInstance, int nCmdShow )
}
+// Dispatch commands coming from the PowerToys IPC channel.
+#ifdef __ZOOMIT_POWERTOYS__
+void ZoomIt_DispatchCommand(ZoomItCommand cmd)
+{
+ auto post_hotkey = [](WPARAM id)
+ {
+ if (g_hWndMain != nullptr)
+ {
+ PostMessage(g_hWndMain, WM_HOTKEY, id, 0);
+ }
+ };
+
+ switch (cmd)
+ {
+ case ZoomItCommand::Zoom:
+ if (g_hWndMain != nullptr)
+ {
+ PostMessage(g_hWndMain, WM_COMMAND, IDC_ZOOM, 0);
+ }
+ Trace::ZoomItActivateZoom();
+ break;
+ case ZoomItCommand::Draw:
+ post_hotkey(DRAW_HOTKEY);
+ Trace::ZoomItActivateDraw();
+ break;
+ case ZoomItCommand::Break:
+ post_hotkey(BREAK_HOTKEY);
+ Trace::ZoomItActivateBreak();
+ break;
+ case ZoomItCommand::LiveZoom:
+ post_hotkey(LIVE_HOTKEY);
+ Trace::ZoomItActivateLiveZoom();
+ break;
+ case ZoomItCommand::Snip:
+ post_hotkey(SNIP_HOTKEY);
+ Trace::ZoomItActivateSnip();
+ break;
+ case ZoomItCommand::Record:
+ post_hotkey(RECORD_HOTKEY);
+ Trace::ZoomItActivateRecord();
+ break;
+ default:
+ break;
+ }
+}
+#endif
+
//----------------------------------------------------------------------------
//
// WinMain
@@ -7746,7 +7806,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
// Initialize logger
LoggerHelpers::init_logger(L"ZoomIt", L"", LogSettings::zoomItLoggerName);
-
ProcessWaiter::OnProcessTerminate(pid, [mainThreadId](int err) {
if (err != ERROR_SUCCESS)
{
@@ -7905,27 +7964,63 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
#ifdef __ZOOMIT_POWERTOYS__
HANDLE m_reload_settings_event_handle = NULL;
HANDLE m_exit_event_handle = NULL;
+ HANDLE m_zoom_event_handle = NULL;
+ HANDLE m_draw_event_handle = NULL;
+ HANDLE m_break_event_handle = NULL;
+ HANDLE m_live_zoom_event_handle = NULL;
+ HANDLE m_snip_event_handle = NULL;
+ HANDLE m_record_event_handle = NULL;
std::thread m_event_triggers_thread;
if( g_StartedByPowerToys ) {
// Start a thread to listen to PowerToys Events.
m_reload_settings_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_REFRESH_SETTINGS_EVENT);
m_exit_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_EXIT_EVENT);
- if (!m_reload_settings_event_handle || !m_exit_event_handle)
+ m_zoom_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_ZOOM_EVENT);
+ m_draw_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_DRAW_EVENT);
+ m_break_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_BREAK_EVENT);
+ m_live_zoom_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_LIVEZOOM_EVENT);
+ m_snip_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_SNIP_EVENT);
+ m_record_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_RECORD_EVENT);
+ if (!m_reload_settings_event_handle || !m_exit_event_handle || !m_zoom_event_handle || !m_draw_event_handle || !m_break_event_handle || !m_live_zoom_event_handle || !m_snip_event_handle || !m_record_event_handle)
{
Logger::warn(L"Failed to create events. {}", get_last_error_or_default(GetLastError()));
return 1;
}
- m_event_triggers_thread = std::thread([&]() {
+ const std::array event_handles{
+ m_reload_settings_event_handle,
+ m_exit_event_handle,
+ m_zoom_event_handle,
+ m_draw_event_handle,
+ m_break_event_handle,
+ m_live_zoom_event_handle,
+ m_snip_event_handle,
+ m_record_event_handle,
+ };
+ const DWORD handle_count = static_cast(event_handles.size());
+ m_event_triggers_thread = std::thread([event_handles, handle_count]() {
MSG msg;
- HANDLE event_handles[2] = {m_reload_settings_event_handle, m_exit_event_handle};
while (g_running)
{
- DWORD dwEvt = MsgWaitForMultipleObjects(2, event_handles, false, INFINITE, QS_ALLINPUT);
+ DWORD dwEvt = MsgWaitForMultipleObjects(handle_count, event_handles.data(), false, INFINITE, QS_ALLINPUT);
+ if (dwEvt == WAIT_FAILED)
+ {
+ Logger::error(L"ZoomIt event wait failed. {}", get_last_error_or_default(GetLastError()));
+ break;
+ }
if (!g_running)
{
break;
}
+ if (dwEvt == WAIT_OBJECT_0 + handle_count)
+ {
+ if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
+ {
+ TranslateMessage(&msg);
+ DispatchMessageW(&msg);
+ }
+ continue;
+ }
switch (dwEvt)
{
case WAIT_OBJECT_0:
@@ -7938,19 +8033,28 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
case WAIT_OBJECT_0 + 1:
{
// Exit Event
- Logger::trace(L"Received an exit event.");
PostMessage(g_hWndMain, WM_QUIT, 0, 0);
break;
}
case WAIT_OBJECT_0 + 2:
- if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
- {
- TranslateMessage(&msg);
- DispatchMessageW(&msg);
- }
+ ZoomIt_DispatchCommand(ZoomItCommand::Zoom);
break;
- default:
+ case WAIT_OBJECT_0 + 3:
+ ZoomIt_DispatchCommand(ZoomItCommand::Draw);
break;
+ case WAIT_OBJECT_0 + 4:
+ ZoomIt_DispatchCommand(ZoomItCommand::Break);
+ break;
+ case WAIT_OBJECT_0 + 5:
+ ZoomIt_DispatchCommand(ZoomItCommand::LiveZoom);
+ break;
+ case WAIT_OBJECT_0 + 6:
+ ZoomIt_DispatchCommand(ZoomItCommand::Snip);
+ break;
+ case WAIT_OBJECT_0 + 7:
+ ZoomIt_DispatchCommand(ZoomItCommand::Record);
+ break;
+ default: break;
}
}
});
@@ -7980,6 +8084,12 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
SetEvent(m_reload_settings_event_handle);
CloseHandle(m_reload_settings_event_handle);
CloseHandle(m_exit_event_handle);
+ CloseHandle(m_zoom_event_handle);
+ CloseHandle(m_draw_event_handle);
+ CloseHandle(m_break_event_handle);
+ CloseHandle(m_live_zoom_event_handle);
+ CloseHandle(m_snip_event_handle);
+ CloseHandle(m_record_event_handle);
m_event_triggers_thread.join();
}
#endif // __ZOOMIT_POWERTOYS__
diff --git a/src/modules/ZoomIt/ZoomItModuleInterface/dllmain.cpp b/src/modules/ZoomIt/ZoomItModuleInterface/dllmain.cpp
index eea809a0a2..40158ed68f 100644
--- a/src/modules/ZoomIt/ZoomItModuleInterface/dllmain.cpp
+++ b/src/modules/ZoomIt/ZoomItModuleInterface/dllmain.cpp
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include
#include
diff --git a/src/modules/awake/Awake.ModuleServices/Awake.ModuleServices.csproj b/src/modules/awake/Awake.ModuleServices/Awake.ModuleServices.csproj
new file mode 100644
index 0000000000..52588938e4
--- /dev/null
+++ b/src/modules/awake/Awake.ModuleServices/Awake.ModuleServices.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ enable
+ enable
+ false
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/awake/Awake.ModuleServices/AwakeService.cs b/src/modules/awake/Awake.ModuleServices/AwakeService.cs
new file mode 100644
index 0000000000..f783cdc3db
--- /dev/null
+++ b/src/modules/awake/Awake.ModuleServices/AwakeService.cs
@@ -0,0 +1,151 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.Text.Json;
+using Common.UI;
+using Microsoft.PowerToys.Settings.UI.Library;
+using PowerToys.ModuleContracts;
+
+namespace Awake.ModuleServices;
+
+///
+/// Provides CLI-based Awake control for reuse across hosts.
+///
+public sealed class AwakeService : ModuleServiceBase, IAwakeService
+{
+ public static AwakeService Instance { get; } = new();
+
+ public override string Key => SettingsDeepLink.SettingsWindow.Awake.ToString();
+
+ protected override SettingsDeepLink.SettingsWindow SettingsWindow => SettingsDeepLink.SettingsWindow.Awake;
+
+ public override Task LaunchAsync(CancellationToken cancellationToken = default)
+ {
+ // Default launch -> indefinite, honoring Awake's own settings for display behavior.
+ return SetIndefiniteAsync(cancellationToken);
+ }
+
+ public AwakeState GetCurrentState()
+ {
+ var isRunning = IsAwakeProcessRunning();
+ var settings = ReadSettings();
+
+ if (settings is null)
+ {
+ return new AwakeState(isRunning, AwakeStateMode.Passive, false, null, null);
+ }
+
+ var mode = settings.Properties.Mode switch
+ {
+ AwakeMode.PASSIVE => AwakeStateMode.Passive,
+ AwakeMode.INDEFINITE => AwakeStateMode.Indefinite,
+ AwakeMode.TIMED => AwakeStateMode.Timed,
+ AwakeMode.EXPIRABLE => AwakeStateMode.Expirable,
+ _ => AwakeStateMode.Passive,
+ };
+
+ TimeSpan? duration = null;
+ DateTimeOffset? expiration = null;
+
+ switch (mode)
+ {
+ case AwakeStateMode.Timed:
+ duration = TimeSpan.FromHours(settings.Properties.IntervalHours) + TimeSpan.FromMinutes(settings.Properties.IntervalMinutes);
+ break;
+ case AwakeStateMode.Expirable:
+ expiration = settings.Properties.ExpirationDateTime;
+ break;
+ }
+
+ return new AwakeState(isRunning, mode, settings.Properties.KeepDisplayOn, duration, expiration);
+ }
+
+ public Task SetIndefiniteAsync(CancellationToken cancellationToken = default)
+ {
+ return UpdateSettingsAsync(
+ settings =>
+ {
+ settings.Properties.Mode = AwakeMode.INDEFINITE;
+ },
+ cancellationToken);
+ }
+
+ public Task SetTimedAsync(int minutes, CancellationToken cancellationToken = default)
+ {
+ if (minutes <= 0)
+ {
+ return Task.FromResult(OperationResult.Fail("Minutes must be greater than zero."));
+ }
+
+ return UpdateSettingsAsync(
+ settings =>
+ {
+ var totalMinutes = Math.Min(minutes, int.MaxValue);
+ settings.Properties.Mode = AwakeMode.TIMED;
+ settings.Properties.IntervalHours = (uint)(totalMinutes / 60);
+ settings.Properties.IntervalMinutes = (uint)(totalMinutes % 60);
+ },
+ cancellationToken);
+ }
+
+ public Task SetOffAsync(CancellationToken cancellationToken = default)
+ {
+ return UpdateSettingsAsync(
+ settings =>
+ {
+ settings.Properties.Mode = AwakeMode.PASSIVE;
+ },
+ cancellationToken);
+ }
+
+ private static Task UpdateSettingsAsync(Action mutateSettings, CancellationToken cancellationToken)
+ {
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var settingsUtils = SettingsUtils.Default;
+ var settings = settingsUtils.GetSettingsOrDefault(AwakeSettings.ModuleName);
+
+ mutateSettings(settings);
+
+ settingsUtils.SaveSettings(JsonSerializer.Serialize(settings, AwakeServiceJsonContext.Default.AwakeSettings), AwakeSettings.ModuleName);
+ return Task.FromResult(OperationResult.Ok());
+ }
+ catch (OperationCanceledException)
+ {
+ return Task.FromResult(OperationResult.Fail("Awake update was cancelled."));
+ }
+ catch (Exception ex)
+ {
+ return Task.FromResult(OperationResult.Fail($"Failed to update Awake settings: {ex.Message}"));
+ }
+ }
+
+ private static bool IsAwakeProcessRunning()
+ {
+ try
+ {
+ return Process.GetProcessesByName("PowerToys.Awake").Length > 0;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static AwakeSettings? ReadSettings()
+ {
+ try
+ {
+ var settingsUtils = SettingsUtils.Default;
+ return settingsUtils.GetSettingsOrDefault(AwakeSettings.ModuleName);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/modules/awake/Awake.ModuleServices/AwakeServiceJsonContext.cs b/src/modules/awake/Awake.ModuleServices/AwakeServiceJsonContext.cs
new file mode 100644
index 0000000000..cfaeff5bed
--- /dev/null
+++ b/src/modules/awake/Awake.ModuleServices/AwakeServiceJsonContext.cs
@@ -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.
+
+using System.Text.Json.Serialization;
+using Microsoft.PowerToys.Settings.UI.Library;
+
+namespace Awake.ModuleServices;
+
+[JsonSerializable(typeof(AwakeSettings))]
+internal sealed partial class AwakeServiceJsonContext : JsonSerializerContext
+{
+}
diff --git a/src/modules/awake/Awake.ModuleServices/AwakeState.cs b/src/modules/awake/Awake.ModuleServices/AwakeState.cs
new file mode 100644
index 0000000000..4a59291732
--- /dev/null
+++ b/src/modules/awake/Awake.ModuleServices/AwakeState.cs
@@ -0,0 +1,26 @@
+// 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 Awake.ModuleServices;
+
+///
+/// Represents the current state of the Awake module.
+///
+/// Whether the Awake process is currently running.
+/// The current Awake mode (Passive, Indefinite, Timed, Expirable).
+/// Whether the display is kept on.
+/// For timed mode, the configured duration.
+/// For expirable mode, the expiration date/time.
+public readonly record struct AwakeState(bool IsRunning, AwakeStateMode Mode, bool KeepDisplayOn, TimeSpan? Duration, DateTimeOffset? Expiration);
+
+///
+/// The mode of the Awake module.
+///
+public enum AwakeStateMode
+{
+ Passive = 0,
+ Indefinite = 1,
+ Timed = 2,
+ Expirable = 3,
+}
diff --git a/src/modules/awake/Awake.ModuleServices/IAwakeService.cs b/src/modules/awake/Awake.ModuleServices/IAwakeService.cs
new file mode 100644
index 0000000000..1e600f52fe
--- /dev/null
+++ b/src/modules/awake/Awake.ModuleServices/IAwakeService.cs
@@ -0,0 +1,21 @@
+// 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 PowerToys.ModuleContracts;
+
+namespace Awake.ModuleServices;
+
+public interface IAwakeService : IModuleService
+{
+ Task SetIndefiniteAsync(CancellationToken cancellationToken = default);
+
+ Task SetTimedAsync(int minutes, CancellationToken cancellationToken = default);
+
+ Task SetOffAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the current state of the Awake module.
+ ///
+ AwakeState GetCurrentState();
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml
index 882c64e3e7..a7bece87c1 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml
@@ -12,4 +12,4 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
true
False
-
\ No newline at end of file
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml
index c686bf808b..73aa1ac98f 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml
@@ -12,4 +12,4 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
true
False
-
\ No newline at end of file
+
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/AdvancedPaste/OpenAdvancedPasteCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/AdvancedPaste/OpenAdvancedPasteCommand.cs
new file mode 100644
index 0000000000..4e738a7342
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/AdvancedPaste/OpenAdvancedPasteCommand.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Opens Advanced Paste UI by signaling the module's show event.
+/// The DLL interface handles starting the process if it's not running.
+///
+internal sealed partial class OpenAdvancedPasteCommand : InvokableCommand
+{
+ public OpenAdvancedPasteCommand()
+ {
+ Name = "Open Advanced Paste";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var showEvent = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.AdvancedPasteShowUIEvent());
+ showEvent.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to open Advanced Paste: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/RefreshAwakeStatusCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/RefreshAwakeStatusCommand.cs
new file mode 100644
index 0000000000..7327090fd3
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/RefreshAwakeStatusCommand.cs
@@ -0,0 +1,26 @@
+// 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 Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace PowerToysExtension.Commands;
+
+internal sealed partial class RefreshAwakeStatusCommand : InvokableCommand
+{
+ private readonly Action _refreshAction;
+
+ internal RefreshAwakeStatusCommand(Action refreshAction)
+ {
+ ArgumentNullException.ThrowIfNull(refreshAction);
+ _refreshAction = refreshAction;
+ Name = "Refresh Awake status";
+ }
+
+ public override CommandResult Invoke()
+ {
+ _refreshAction();
+ return CommandResult.KeepOpen();
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StartAwakeCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StartAwakeCommand.cs
new file mode 100644
index 0000000000..d4695535ff
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StartAwakeCommand.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.ModuleContracts;
+
+namespace PowerToysExtension.Commands;
+
+internal sealed partial class StartAwakeCommand : InvokableCommand
+{
+ private readonly Func> _action;
+ private readonly string _successToast;
+ private readonly Action? _onSuccess;
+
+ internal StartAwakeCommand(string title, Func> action, string successToast = "", Action? onSuccess = null)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+ ArgumentException.ThrowIfNullOrWhiteSpace(title);
+
+ _action = action;
+ _successToast = successToast ?? string.Empty;
+ _onSuccess = onSuccess;
+ Name = title;
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ var result = _action().GetAwaiter().GetResult();
+ if (!result.Success)
+ {
+ return ShowToastKeepOpen(result.Error ?? "Failed to start Awake.");
+ }
+
+ _onSuccess?.Invoke();
+
+ return string.IsNullOrWhiteSpace(_successToast)
+ ? CommandResult.KeepOpen()
+ : ShowToastKeepOpen(_successToast);
+ }
+ catch (Exception ex)
+ {
+ return ShowToastKeepOpen($"Launching Awake failed: {ex.Message}");
+ }
+ }
+
+ private static CommandResult ShowToastKeepOpen(string message)
+ {
+ return CommandResult.ShowToast(new ToastArgs()
+ {
+ Message = message,
+ Result = CommandResult.KeepOpen(),
+ });
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StopAwakeCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StopAwakeCommand.cs
new file mode 100644
index 0000000000..426c039437
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StopAwakeCommand.cs
@@ -0,0 +1,48 @@
+// 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 Awake.ModuleServices;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace PowerToysExtension.Commands;
+
+internal sealed partial class StopAwakeCommand : InvokableCommand
+{
+ private readonly Action? _onSuccess;
+
+ internal StopAwakeCommand(Action? onSuccess = null)
+ {
+ _onSuccess = onSuccess;
+ Name = "Set Awake to Off";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ var result = AwakeService.Instance.SetOffAsync().GetAwaiter().GetResult();
+ if (result.Success)
+ {
+ _onSuccess?.Invoke();
+ return ShowToastKeepOpen("Awake switched to Off.");
+ }
+
+ return ShowToastKeepOpen(result.Error ?? "Awake does not appear to be running.");
+ }
+ catch (Exception ex)
+ {
+ return ShowToastKeepOpen($"Failed to switch Awake off: {ex.Message}");
+ }
+ }
+
+ private static CommandResult ShowToastKeepOpen(string message)
+ {
+ return CommandResult.ShowToast(new ToastArgs()
+ {
+ Message = message,
+ Result = CommandResult.KeepOpen(),
+ });
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/CopySavedColorCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/CopySavedColorCommand.cs
new file mode 100644
index 0000000000..96b43a9a17
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/CopySavedColorCommand.cs
@@ -0,0 +1,39 @@
+// 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 ColorPicker.ModuleServices;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Copies a saved color in a chosen format.
+///
+internal sealed partial class CopySavedColorCommand : InvokableCommand
+{
+ private readonly SavedColor _color;
+ private readonly string _copyValue;
+
+ public CopySavedColorCommand(SavedColor color, string copyValue)
+ {
+ _color = color;
+ _copyValue = copyValue;
+ Name = $"Copy {_color.Hex}";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ ClipboardHelper.SetText(_copyValue);
+
+ return CommandResult.ShowToast($"Copied {_copyValue}");
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to copy color: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/OpenColorPickerCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/OpenColorPickerCommand.cs
new file mode 100644
index 0000000000..6982c5dffe
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/OpenColorPickerCommand.cs
@@ -0,0 +1,38 @@
+// 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 ColorPicker.ModuleServices;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Opens the Color Picker picker session via shared event.
+///
+internal sealed partial class OpenColorPickerCommand : InvokableCommand
+{
+ public OpenColorPickerCommand()
+ {
+ Name = "Open Color Picker";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ var result = ColorPickerService.Instance.OpenPickerAsync().GetAwaiter().GetResult();
+ if (!result.Success)
+ {
+ return CommandResult.ShowToast(result.Error ?? "Failed to open Color Picker.");
+ }
+
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to open Color Picker: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockReparentCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockReparentCommand.cs
new file mode 100644
index 0000000000..417ab34a5d
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockReparentCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Triggers Crop and Lock reparent mode via the shared event.
+///
+internal sealed partial class CropAndLockReparentCommand : InvokableCommand
+{
+ public CropAndLockReparentCommand()
+ {
+ Name = "Crop and Lock (Reparent)";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockReparentEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to start Crop and Lock (Reparent): {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockThumbnailCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockThumbnailCommand.cs
new file mode 100644
index 0000000000..b9996f7835
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockThumbnailCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Triggers Crop and Lock thumbnail mode via the shared event.
+///
+internal sealed partial class CropAndLockThumbnailCommand : InvokableCommand
+{
+ public CropAndLockThumbnailCommand()
+ {
+ Name = "Crop and Lock (Thumbnail)";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockThumbnailEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to start Crop and Lock (Thumbnail): {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesAdminCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesAdminCommand.cs
new file mode 100644
index 0000000000..6961783325
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesAdminCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Launches Environment Variables (admin) via the shared event.
+///
+internal sealed partial class OpenEnvironmentVariablesAdminCommand : InvokableCommand
+{
+ public OpenEnvironmentVariablesAdminCommand()
+ {
+ Name = "Open Environment Variables (Admin)";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowEnvironmentVariablesAdminSharedEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to open Environment Variables (Admin): {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesCommand.cs
new file mode 100644
index 0000000000..71bb81068d
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Launches Environment Variables (user) via the shared event.
+///
+internal sealed partial class OpenEnvironmentVariablesCommand : InvokableCommand
+{
+ public OpenEnvironmentVariablesCommand()
+ {
+ Name = "Open Environment Variables";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowEnvironmentVariablesSharedEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to open Environment Variables: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/ApplyFancyZonesLayoutCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/ApplyFancyZonesLayoutCommand.cs
new file mode 100644
index 0000000000..b4ef1e55c9
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/ApplyFancyZonesLayoutCommand.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension.Commands;
+
+internal sealed partial class ApplyFancyZonesLayoutCommand : InvokableCommand
+{
+ private readonly FancyZonesLayoutDescriptor _layout;
+ private readonly FancyZonesMonitorDescriptor? _targetMonitor;
+
+ public ApplyFancyZonesLayoutCommand(FancyZonesLayoutDescriptor layout, FancyZonesMonitorDescriptor? monitor)
+ {
+ _layout = layout;
+ _targetMonitor = monitor;
+
+ Name = monitor is null ? "Apply to all monitors" : $"Apply to Monitor {monitor.Value.Title}";
+
+ Icon = new IconInfo("\uF78C");
+ }
+
+ public override CommandResult Invoke()
+ {
+ var monitor = _targetMonitor;
+ var (success, message) = monitor is null
+ ? FancyZonesDataService.ApplyLayoutToAllMonitors(_layout)
+ : FancyZonesDataService.ApplyLayoutToMonitor(_layout, monitor.Value);
+
+ return success
+ ? CommandResult.Dismiss()
+ : CommandResult.ShowToast(message);
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesLayoutListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesLayoutListItem.cs
new file mode 100644
index 0000000000..b2e7077fee
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesLayoutListItem.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using ManagedCommon;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension.Pages;
+
+internal sealed partial class FancyZonesLayoutListItem : ListItem
+{
+ private readonly Lazy> _iconLoadTask;
+ private readonly string _layoutId;
+ private readonly string _layoutTitle;
+
+ private int _isLoadingIcon;
+
+ public override IIconInfo? Icon
+ {
+ get
+ {
+ if (Interlocked.Exchange(ref _isLoadingIcon, 1) == 0)
+ {
+ _ = LoadIconAsync();
+ }
+
+ return base.Icon;
+ }
+ set => base.Icon = value;
+ }
+
+ public FancyZonesLayoutListItem(ICommand command, FancyZonesLayoutDescriptor layout, IconInfo fallbackIcon)
+ : base(command)
+ {
+ Title = layout.Title;
+ Subtitle = layout.Subtitle;
+ Icon = fallbackIcon;
+ _layoutId = layout.Id;
+ _layoutTitle = layout.Title;
+
+ _iconLoadTask = new Lazy>(async () => await FancyZonesThumbnailRenderer.RenderLayoutIconAsync(layout));
+ }
+
+ private async Task LoadIconAsync()
+ {
+ try
+ {
+ Logger.LogDebug($"FancyZones layout icon load starting. LayoutId={_layoutId} Title=\"{_layoutTitle}\"");
+ var icon = await _iconLoadTask.Value;
+ if (icon is not null)
+ {
+ Icon = icon;
+ Logger.LogDebug($"FancyZones layout icon load succeeded. LayoutId={_layoutId} Title=\"{_layoutTitle}\"");
+ }
+ else
+ {
+ Logger.LogDebug($"FancyZones layout icon load returned null. LayoutId={_layoutId} Title=\"{_layoutTitle}\"");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"FancyZones layout icon load failed. LayoutId={_layoutId} Title=\"{_layoutTitle}\" Exception={ex}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesMonitorListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesMonitorListItem.cs
new file mode 100644
index 0000000000..c65db779df
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesMonitorListItem.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Globalization;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension.Pages;
+
+internal sealed partial class FancyZonesMonitorListItem : ListItem
+{
+ public FancyZonesMonitorListItem(FancyZonesMonitorDescriptor monitor, string subtitle, IconInfo icon)
+ : base(new IdentifyFancyZonesMonitorCommand(monitor))
+ {
+ Title = monitor.Title;
+ Subtitle = subtitle;
+ Icon = icon;
+
+ Details = BuildMonitorDetails(monitor);
+
+ var pickerPage = new FancyZonesMonitorLayoutPickerPage(monitor)
+ {
+ Name = "Set active layout",
+ };
+
+ MoreCommands =
+ [
+ new CommandContextItem(pickerPage)
+ {
+ Title = "Set active layout",
+ Subtitle = "Pick a layout for this monitor",
+ },
+ ];
+ }
+
+ public static Details BuildMonitorDetails(FancyZonesMonitorDescriptor monitor)
+ {
+ var currentVirtualDesktop = FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString();
+ var tags = new List
+ {
+ DetailTag("Monitor", monitor.Data.Monitor),
+ DetailTag("Instance", monitor.Data.MonitorInstanceId),
+ DetailTag("Serial", monitor.Data.MonitorSerialNumber),
+ DetailTag("Number", monitor.Data.MonitorNumber.ToString(CultureInfo.InvariantCulture)),
+ DetailTag("Virtual desktop", currentVirtualDesktop),
+ DetailTag("Work area", $"{monitor.Data.LeftCoordinate},{monitor.Data.TopCoordinate} {monitor.Data.WorkAreaWidth}\u00D7{monitor.Data.WorkAreaHeight}"),
+ DetailTag("Resolution", $"{monitor.Data.MonitorWidth}\u00D7{monitor.Data.MonitorHeight}"),
+ DetailTag("DPI", monitor.Data.Dpi.ToString(CultureInfo.InvariantCulture)),
+ };
+
+ return new Details
+ {
+ Title = monitor.Title,
+ HeroImage = FancyZonesMonitorPreviewRenderer.TryRenderMonitorHeroImage(monitor) ??
+ PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"),
+ Metadata = tags.ToArray(),
+ };
+ }
+
+ private static DetailsElement DetailTag(string key, string? value)
+ {
+ return new DetailsElement
+ {
+ Key = key,
+ Data = new DetailsTags
+ {
+ Tags = [new Tag(string.IsNullOrWhiteSpace(value) ? "n/a" : value)],
+ },
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/IdentifyFancyZonesMonitorCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/IdentifyFancyZonesMonitorCommand.cs
new file mode 100644
index 0000000000..6e4b8b45c5
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/IdentifyFancyZonesMonitorCommand.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Linq;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension.Commands;
+
+internal sealed partial class IdentifyFancyZonesMonitorCommand : InvokableCommand
+{
+ private readonly FancyZonesMonitorDescriptor _monitor;
+
+ public IdentifyFancyZonesMonitorCommand(FancyZonesMonitorDescriptor monitor)
+ {
+ _monitor = monitor;
+ Name = $"Identify {_monitor.Title}";
+ Icon = new IconInfo("\uE773");
+ }
+
+ public override CommandResult Invoke()
+ {
+ if (!FancyZonesDataService.TryGetMonitors(out var monitors, out var error))
+ {
+ return CommandResult.ShowToast(error);
+ }
+
+ var monitor = monitors.FirstOrDefault(m => m.Data.MonitorInstanceId == _monitor.Data.MonitorInstanceId);
+
+ if (monitor == null)
+ {
+ return CommandResult.ShowToast("Monitor not found.");
+ }
+
+ FancyZonesMonitorIdentifier.Show(
+ monitor.Data.LeftCoordinate,
+ monitor.Data.TopCoordinate,
+ monitor.Data.WorkAreaWidth,
+ monitor.Data.WorkAreaHeight,
+ _monitor.Title,
+ durationMs: 1200);
+
+ return CommandResult.KeepOpen();
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/OpenFancyZonesEditorCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/OpenFancyZonesEditorCommand.cs
new file mode 100644
index 0000000000..9376fba709
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/OpenFancyZonesEditorCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Launches the FancyZones layout editor via the shared event.
+///
+internal sealed partial class OpenFancyZonesEditorCommand : InvokableCommand
+{
+ public OpenFancyZonesEditorCommand()
+ {
+ Name = "Open FancyZones Editor";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.FZEToggleEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to open FancyZones editor: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorAdminCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorAdminCommand.cs
new file mode 100644
index 0000000000..63bd74d62a
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorAdminCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Launches Hosts File Editor (Admin) via the shared event.
+///
+internal sealed partial class OpenHostsEditorAdminCommand : InvokableCommand
+{
+ public OpenHostsEditorAdminCommand()
+ {
+ Name = "Open Hosts File Editor (Admin)";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowHostsAdminSharedEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to open Hosts File Editor (Admin): {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorCommand.cs
new file mode 100644
index 0000000000..fdf5c807d0
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Launches Hosts File Editor via the shared event.
+///
+internal sealed partial class OpenHostsEditorCommand : InvokableCommand
+{
+ public OpenHostsEditorCommand()
+ {
+ Name = "Open Hosts File Editor";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowHostsSharedEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to open Hosts File Editor: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LaunchModuleCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LaunchModuleCommand.cs
new file mode 100644
index 0000000000..3101a73030
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LaunchModuleCommand.cs
@@ -0,0 +1,121 @@
+// 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.Diagnostics;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Launches a PowerToys module either by raising its shared event or starting the backing executable.
+///
+internal sealed partial class LaunchModuleCommand : InvokableCommand
+{
+ private readonly string _moduleName;
+ private readonly string _eventName;
+ private readonly string _executableName;
+ private readonly string _arguments;
+
+ internal LaunchModuleCommand(
+ string moduleName,
+ string eventName = "",
+ string executableName = "",
+ string arguments = "",
+ string displayName = "")
+ {
+ if (string.IsNullOrWhiteSpace(moduleName))
+ {
+ throw new ArgumentException("Module name is required", nameof(moduleName));
+ }
+
+ _moduleName = moduleName;
+ _eventName = eventName ?? string.Empty;
+ _executableName = executableName ?? string.Empty;
+ _arguments = arguments ?? string.Empty;
+ Name = string.IsNullOrWhiteSpace(displayName) ? $"Launch {moduleName}" : displayName;
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ if (TrySignalEvent())
+ {
+ return CommandResult.Hide();
+ }
+
+ if (TryLaunchExecutable())
+ {
+ return CommandResult.Hide();
+ }
+
+ return CommandResult.ShowToast($"Unable to launch {_moduleName}.");
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Launching {_moduleName} failed: {ex.Message}");
+ }
+ }
+
+ private bool TrySignalEvent()
+ {
+ if (string.IsNullOrEmpty(_eventName))
+ {
+ return false;
+ }
+
+ try
+ {
+ using var existingHandle = EventWaitHandle.OpenExisting(_eventName);
+ return existingHandle.Set();
+ }
+ catch (WaitHandleCannotBeOpenedException)
+ {
+ try
+ {
+ using var newHandle = new EventWaitHandle(false, EventResetMode.AutoReset, _eventName, out _);
+ return newHandle.Set();
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private bool TryLaunchExecutable()
+ {
+ if (string.IsNullOrEmpty(_executableName))
+ {
+ return false;
+ }
+
+ var executablePath = PowerToysPathResolver.TryResolveExecutable(_executableName);
+ if (string.IsNullOrEmpty(executablePath))
+ {
+ return false;
+ }
+
+ var startInfo = new ProcessStartInfo(executablePath)
+ {
+ UseShellExecute = true,
+ };
+
+ if (!string.IsNullOrWhiteSpace(_arguments))
+ {
+ startInfo.Arguments = _arguments;
+ startInfo.UseShellExecute = false;
+ }
+
+ Process.Start(startInfo);
+ return true;
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LightSwitch/ToggleLightSwitchCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LightSwitch/ToggleLightSwitchCommand.cs
new file mode 100644
index 0000000000..8702d7630a
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LightSwitch/ToggleLightSwitchCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Toggles Light Switch via the shared event.
+///
+internal sealed partial class ToggleLightSwitchCommand : InvokableCommand
+{
+ public ToggleLightSwitchCommand()
+ {
+ Name = "Toggle Light Switch";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.LightSwitchToggleEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to toggle Light Switch: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ShowMouseJumpPreviewCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ShowMouseJumpPreviewCommand.cs
new file mode 100644
index 0000000000..f0c3b9af32
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ShowMouseJumpPreviewCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Shows Mouse Jump preview via the shared event.
+///
+internal sealed partial class ShowMouseJumpPreviewCommand : InvokableCommand
+{
+ public ShowMouseJumpPreviewCommand()
+ {
+ Name = "Show Mouse Jump Preview";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MouseJumpShowPreviewEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to show Mouse Jump preview: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleCursorWrapCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleCursorWrapCommand.cs
new file mode 100644
index 0000000000..9e6d9e6817
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleCursorWrapCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Toggles Cursor Wrap via the shared trigger event.
+///
+internal sealed partial class ToggleCursorWrapCommand : InvokableCommand
+{
+ public ToggleCursorWrapCommand()
+ {
+ Name = "Toggle Cursor Wrap";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CursorWrapTriggerEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to toggle Cursor Wrap: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleFindMyMouseCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleFindMyMouseCommand.cs
new file mode 100644
index 0000000000..f8bb115789
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleFindMyMouseCommand.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Triggers Find My Mouse via the shared event.
+///
+internal sealed partial class ToggleFindMyMouseCommand : InvokableCommand
+{
+ public ToggleFindMyMouseCommand()
+ {
+ Name = "Trigger Find My Mouse";
+ }
+
+ public override CommandResult Invoke()
+ {
+ // Delay the trigger so the Command Palette dismisses first
+ _ = Task.Run(async () =>
+ {
+ await Task.Delay(200).ConfigureAwait(false);
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.FindMyMouseTriggerEvent());
+ evt.Set();
+ }
+ catch
+ {
+ // Ignore errors in background task
+ }
+ });
+
+ return CommandResult.Dismiss();
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseCrosshairsCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseCrosshairsCommand.cs
new file mode 100644
index 0000000000..2209a60d58
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseCrosshairsCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Toggles Mouse Pointer Crosshairs via the shared event.
+///
+internal sealed partial class ToggleMouseCrosshairsCommand : InvokableCommand
+{
+ public ToggleMouseCrosshairsCommand()
+ {
+ Name = "Toggle Mouse Crosshairs";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MouseCrosshairsTriggerEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to toggle Mouse Crosshairs: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseHighlighterCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseHighlighterCommand.cs
new file mode 100644
index 0000000000..1485885723
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseHighlighterCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Toggles Mouse Highlighter via the shared event.
+///
+internal sealed partial class ToggleMouseHighlighterCommand : InvokableCommand
+{
+ public ToggleMouseHighlighterCommand()
+ {
+ Name = "Toggle Mouse Highlighter";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MouseHighlighterTriggerEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to toggle Mouse Highlighter: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenInSettingsCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenInSettingsCommand.cs
new file mode 100644
index 0000000000..65860c249a
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenInSettingsCommand.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Common.UI;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Opens the PowerToys settings page for the given module via SettingsDeepLink.
+///
+internal sealed partial class OpenInSettingsCommand : InvokableCommand
+{
+ private readonly SettingsDeepLink.SettingsWindow _module;
+
+ internal OpenInSettingsCommand(SettingsDeepLink.SettingsWindow module, string title = "")
+ {
+ _module = module;
+ Name = string.IsNullOrWhiteSpace(title) ? $"Open {_module} settings" : title;
+ }
+
+ public override CommandResult Invoke()
+ {
+ SettingsDeepLink.OpenSettings(_module);
+ return CommandResult.Hide();
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenPowerToysSettingsCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenPowerToysSettingsCommand.cs
new file mode 100644
index 0000000000..bd865fcab1
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenPowerToysSettingsCommand.cs
@@ -0,0 +1,61 @@
+// 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.Diagnostics;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Opens the PowerToys settings application deep linked to a specific module.
+///
+internal sealed partial class OpenPowerToysSettingsCommand : InvokableCommand
+{
+ private readonly string _moduleName;
+ private readonly string _settingsKey;
+
+ internal OpenPowerToysSettingsCommand(string moduleName, string settingsKey)
+ {
+ if (string.IsNullOrWhiteSpace(moduleName))
+ {
+ throw new ArgumentException("Module name is required", nameof(moduleName));
+ }
+
+ if (string.IsNullOrWhiteSpace(settingsKey))
+ {
+ throw new ArgumentException("Settings key is required", nameof(settingsKey));
+ }
+
+ _moduleName = moduleName;
+ _settingsKey = settingsKey;
+ Name = $"Open {_moduleName} settings";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ var powerToysPath = PowerToysPathResolver.TryResolveExecutable("PowerToys.exe");
+ if (string.IsNullOrEmpty(powerToysPath))
+ {
+ return CommandResult.ShowToast("Unable to locate PowerToys.");
+ }
+
+ var startInfo = new ProcessStartInfo(powerToysPath)
+ {
+ Arguments = $"--open-settings={_settingsKey}",
+ UseShellExecute = false,
+ };
+
+ Process.Start(startInfo);
+ return CommandResult.Hide();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Opening {_moduleName} settings failed: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/RegistryPreview/OpenRegistryPreviewCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/RegistryPreview/OpenRegistryPreviewCommand.cs
new file mode 100644
index 0000000000..6df382256f
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/RegistryPreview/OpenRegistryPreviewCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Launches Registry Preview via the shared event.
+///
+internal sealed partial class OpenRegistryPreviewCommand : InvokableCommand
+{
+ public OpenRegistryPreviewCommand()
+ {
+ Name = "Open Registry Preview";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.RegistryPreviewTriggerEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to open Registry Preview: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ScreenRuler/ToggleScreenRulerCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ScreenRuler/ToggleScreenRulerCommand.cs
new file mode 100644
index 0000000000..889cb5d7b9
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ScreenRuler/ToggleScreenRulerCommand.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+internal sealed partial class ToggleScreenRulerCommand : InvokableCommand
+{
+ public ToggleScreenRulerCommand()
+ {
+ Name = "Toggle Screen Ruler";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MeasureToolTriggerEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to toggle Screen Ruler: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ShortcutGuide/ToggleShortcutGuideCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ShortcutGuide/ToggleShortcutGuideCommand.cs
new file mode 100644
index 0000000000..4c6d056eaf
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ShortcutGuide/ToggleShortcutGuideCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Toggles the Shortcut Guide UI via the shared trigger event.
+///
+internal sealed partial class ToggleShortcutGuideCommand : InvokableCommand
+{
+ public ToggleShortcutGuideCommand()
+ {
+ Name = "Toggle Shortcut Guide";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShortcutGuideTriggerEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to toggle Shortcut Guide: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/TextExtractor/ToggleTextExtractorCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/TextExtractor/ToggleTextExtractorCommand.cs
new file mode 100644
index 0000000000..615fb0e395
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/TextExtractor/ToggleTextExtractorCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+///
+/// Triggers the Text Extractor UI via the existing show event.
+///
+internal sealed partial class ToggleTextExtractorCommand : InvokableCommand
+{
+ public ToggleTextExtractorCommand()
+ {
+ Name = "Toggle Text Extractor";
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowPowerOCRSharedEvent());
+ evt.Set();
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to toggle Text Extractor: {ex.Message}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/LaunchWorkspaceCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/LaunchWorkspaceCommand.cs
new file mode 100644
index 0000000000..4372e5b7ff
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/LaunchWorkspaceCommand.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using Workspaces.ModuleServices;
+
+namespace PowerToysExtension.Commands;
+
+internal sealed partial class LaunchWorkspaceCommand : InvokableCommand
+{
+ private readonly string _workspaceId;
+
+ internal LaunchWorkspaceCommand(string workspaceId)
+ {
+ _workspaceId = workspaceId;
+ Name = "Launch workspace";
+ }
+
+ public override CommandResult Invoke()
+ {
+ if (string.IsNullOrEmpty(_workspaceId))
+ {
+ return CommandResult.KeepOpen();
+ }
+
+ var result = WorkspaceService.Instance.LaunchWorkspaceAsync(_workspaceId).GetAwaiter().GetResult();
+ if (!result.Success)
+ {
+ return CommandResult.ShowToast(result.Error ?? "Launching workspace failed.");
+ }
+
+ return CommandResult.Hide();
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/OpenWorkspaceEditorCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/OpenWorkspaceEditorCommand.cs
new file mode 100644
index 0000000000..63150902a6
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/OpenWorkspaceEditorCommand.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using Workspaces.ModuleServices;
+
+namespace PowerToysExtension.Commands;
+
+internal sealed partial class OpenWorkspaceEditorCommand : InvokableCommand
+{
+ public override CommandResult Invoke()
+ {
+ var result = WorkspaceService.Instance.LaunchEditorAsync().GetAwaiter().GetResult();
+ if (!result.Success)
+ {
+ return CommandResult.ShowToast(result.Error ?? "Unable to launch the Workspaces editor.");
+ }
+
+ return CommandResult.Hide();
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/WorkspaceListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/WorkspaceListItem.cs
new file mode 100644
index 0000000000..1b2024e759
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/WorkspaceListItem.cs
@@ -0,0 +1,117 @@
+// 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 Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Helpers;
+using WorkspacesCsharpLibrary.Data;
+
+namespace PowerToysExtension.Commands;
+
+internal sealed partial class WorkspaceListItem : ListItem
+{
+ public WorkspaceListItem(ProjectWrapper workspace, IconInfo icon)
+ : base(new LaunchWorkspaceCommand(workspace.Id))
+ {
+ Title = workspace.Name;
+ Subtitle = BuildSubtitle(workspace);
+ Icon = icon;
+ Details = BuildDetails(workspace, icon);
+ }
+
+ private static string BuildSubtitle(ProjectWrapper workspace)
+ {
+ var appCount = workspace.Applications?.Count ?? 0;
+ var appsText = appCount switch
+ {
+ 0 => "No applications",
+ _ => string.Format(CultureInfo.CurrentCulture, "{0} applications", appCount),
+ };
+
+ var lastLaunched = workspace.LastLaunchedTime > 0
+ ? $"Last launched {FormatRelativeTime(workspace.LastLaunchedTime)}"
+ : "Never launched";
+
+ return $"{appsText} \u2022 {lastLaunched}";
+ }
+
+ private static Details BuildDetails(ProjectWrapper workspace, IconInfo icon)
+ {
+ var appCount = workspace.Applications?.Count ?? 0;
+ var body = appCount switch
+ {
+ 0 => "No applications in this workspace",
+ 1 => "1 application",
+ _ => $"{appCount} applications",
+ };
+
+ return new Details
+ {
+ HeroImage = icon,
+ Title = workspace.Name ?? "Workspace",
+ Body = body,
+ Metadata = BuildAppMetadata(workspace),
+ };
+ }
+
+ private static IDetailsElement[] BuildAppMetadata(ProjectWrapper workspace)
+ {
+ if (workspace.Applications is null || workspace.Applications.Count == 0)
+ {
+ return Array.Empty();
+ }
+
+ var elements = new List();
+ foreach (var app in workspace.Applications)
+ {
+ var appName = string.IsNullOrWhiteSpace(app.Application) ? "App" : app.Application;
+ var title = string.IsNullOrWhiteSpace(app.Title) ? appName : app.Title;
+
+ var tags = new List();
+
+ if (!string.IsNullOrWhiteSpace(app.ApplicationPath))
+ {
+ tags.Add(new Tag(app.ApplicationPath));
+ }
+ else
+ {
+ tags.Add(new Tag(appName));
+ }
+
+ elements.Add(new DetailsElement
+ {
+ Key = title,
+ Data = new DetailsTags { Tags = tags.ToArray() },
+ });
+ }
+
+ return elements.ToArray();
+ }
+
+ private static string FormatRelativeTime(long unixSeconds)
+ {
+ var lastLaunch = DateTimeOffset.FromUnixTimeSeconds(unixSeconds).UtcDateTime;
+ var delta = DateTime.UtcNow - lastLaunch;
+
+ if (delta.TotalMinutes < 1)
+ {
+ return "just now";
+ }
+
+ if (delta.TotalMinutes < 60)
+ {
+ return string.Format(CultureInfo.CurrentCulture, "{0} min ago", (int)delta.TotalMinutes);
+ }
+
+ if (delta.TotalHours < 24)
+ {
+ return string.Format(CultureInfo.CurrentCulture, "{0} hr ago", (int)delta.TotalHours);
+ }
+
+ return string.Format(CultureInfo.CurrentCulture, "{0} days ago", (int)delta.TotalDays);
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ZoomIt/ZoomItActionCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ZoomIt/ZoomItActionCommand.cs
new file mode 100644
index 0000000000..44b5ea447a
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ZoomIt/ZoomItActionCommand.cs
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToys.Interop;
+
+namespace PowerToysExtension.Commands;
+
+internal sealed partial class ZoomItActionCommand : InvokableCommand
+{
+ private readonly string _action;
+ private readonly string _title;
+
+ public ZoomItActionCommand(string action, string title)
+ {
+ _action = action;
+ _title = title;
+ Name = title;
+ }
+
+ public override CommandResult Invoke()
+ {
+ try
+ {
+ if (!TryGetEventName(_action, out var eventName))
+ {
+ return CommandResult.ShowToast($"Unknown ZoomIt action: {_action}.");
+ }
+
+ var evt = EventWaitHandle.OpenExisting(eventName);
+ _ = Task.Run(async () =>
+ {
+ using (evt)
+ {
+ // Hide CmdPal first, then signal shortly after so UI like snip/zoom won't capture it.
+ await Task.Delay(50).ConfigureAwait(false);
+ evt.Set();
+ }
+ });
+
+ return CommandResult.Hide();
+ }
+ catch (WaitHandleCannotBeOpenedException)
+ {
+ return CommandResult.ShowToast("ZoomIt is not running. Please start it from PowerToys and try again.");
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast($"Failed to invoke ZoomIt ({_title}): {ex.Message}");
+ }
+ }
+
+ private static bool TryGetEventName(string action, out string eventName)
+ {
+ switch (action.ToLowerInvariant())
+ {
+ case "zoom":
+ eventName = Constants.ZoomItZoomEvent();
+ return true;
+ case "draw":
+ eventName = Constants.ZoomItDrawEvent();
+ return true;
+ case "break":
+ eventName = Constants.ZoomItBreakEvent();
+ return true;
+ case "livezoom":
+ eventName = Constants.ZoomItLiveZoomEvent();
+ return true;
+ case "snip":
+ eventName = Constants.ZoomItSnipEvent();
+ return true;
+ case "record":
+ eventName = Constants.ZoomItRecordEvent();
+ return true;
+ default:
+ eventName = string.Empty;
+ return false;
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/AwakeStatusService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/AwakeStatusService.cs
new file mode 100644
index 0000000000..102c9e90fc
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/AwakeStatusService.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using Awake.ModuleServices;
+using Common.UI;
+
+namespace PowerToysExtension.Helpers;
+
+internal static class AwakeStatusService
+{
+ internal static string GetStatusSubtitle()
+ {
+ var state = AwakeService.Instance.GetCurrentState();
+ if (!state.IsRunning)
+ {
+ return "Awake is idle";
+ }
+
+ if (state.Mode == AwakeStateMode.Passive)
+ {
+ // When the PowerToys Awake module is enabled, the Awake process stays resident
+ // even in passive mode. In that case "idle" is correct. If the module is disabled,
+ // a running process implies a standalone/session keep-awake, so report as active.
+ return ModuleEnablementService.IsModuleEnabled(SettingsDeepLink.SettingsWindow.Awake)
+ ? "Awake is idle"
+ : "Active - session running";
+ }
+
+ return state.Mode switch
+ {
+ AwakeStateMode.Indefinite => "Active - indefinite",
+ AwakeStateMode.Timed => state.Duration is { } span
+ ? $"Active - timer {FormatDuration(span)}"
+ : "Active - timer",
+ AwakeStateMode.Expirable => state.Expiration is { } expiry
+ ? $"Active - until {expiry.ToLocalTime():t}"
+ : "Active - scheduled",
+ _ => "Awake is running",
+ };
+ }
+
+ private static string FormatDuration(TimeSpan span)
+ {
+ if (span.TotalHours >= 1)
+ {
+ var hours = (int)Math.Floor(span.TotalHours);
+ var minutes = span.Minutes;
+ return minutes > 0 ? $"{hours}h {minutes}m" : $"{hours}h";
+ }
+
+ if (span.TotalMinutes >= 1)
+ {
+ return $"{(int)Math.Round(span.TotalMinutes)}m";
+ }
+
+ return span.TotalSeconds >= 1 ? $"{(int)Math.Round(span.TotalSeconds)}s" : "\u2014";
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ColorSwatchIconFactory.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ColorSwatchIconFactory.cs
new file mode 100644
index 0000000000..3fde50882f
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ColorSwatchIconFactory.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Runtime.InteropServices.WindowsRuntime;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using Windows.Storage.Streams;
+
+namespace PowerToysExtension.Helpers;
+
+internal static class ColorSwatchIconFactory
+{
+ public static IconInfo Create(byte r, byte g, byte b, byte a)
+ {
+ try
+ {
+ using var bmp = new Bitmap(32, 32, PixelFormat.Format32bppArgb);
+ using var gfx = Graphics.FromImage(bmp);
+ gfx.SmoothingMode = SmoothingMode.AntiAlias;
+ gfx.Clear(Color.Transparent);
+
+ using var brush = new SolidBrush(Color.FromArgb(a, r, g, b));
+ const int padding = 4;
+ gfx.FillEllipse(brush, padding, padding, bmp.Width - (padding * 2), bmp.Height - (padding * 2));
+
+ using var ms = new MemoryStream();
+ bmp.Save(ms, ImageFormat.Png);
+ var ras = new InMemoryRandomAccessStream();
+ var writer = new DataWriter(ras);
+ writer.WriteBytes(ms.ToArray());
+ writer.StoreAsync().GetResults();
+ ras.Seek(0);
+ return IconInfo.FromStream(ras);
+ }
+ catch
+ {
+ // Fallback to a simple colored glyph when drawing fails.
+ return new IconInfo("\u25CF"); // Black circle glyph
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesDataService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesDataService.cs
new file mode 100644
index 0000000000..0a55859409
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesDataService.cs
@@ -0,0 +1,517 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+
+using FancyZonesEditorCommon.Data;
+using FancyZonesEditorCommon.Utils;
+using ManagedCommon;
+
+using FZPaths = FancyZonesEditorCommon.Data.FancyZonesPaths;
+
+namespace PowerToysExtension.Helpers;
+
+internal static class FancyZonesDataService
+{
+ private const string ZeroUuid = "{00000000-0000-0000-0000-000000000000}";
+
+ public static bool TryGetMonitors(out IReadOnlyList monitors, out string error)
+ {
+ monitors = Array.Empty();
+ error = string.Empty;
+
+ Logger.LogInfo($"TryGetMonitors: Starting. EditorParametersPath={FZPaths.EditorParameters}");
+
+ try
+ {
+ if (!File.Exists(FZPaths.EditorParameters))
+ {
+ error = "FancyZones monitor data not found. Open FancyZones Editor once to initialize.";
+ Logger.LogWarning($"TryGetMonitors: File not found. Path={FZPaths.EditorParameters}");
+ return false;
+ }
+
+ Logger.LogInfo($"TryGetMonitors: File exists, reading...");
+ var editorParams = FancyZonesDataIO.ReadEditorParameters();
+ Logger.LogInfo($"TryGetMonitors: ReadEditorParameters returned. Monitors={editorParams.Monitors?.Count ?? -1}");
+
+ var editorMonitors = editorParams.Monitors;
+ if (editorMonitors is null || editorMonitors.Count == 0)
+ {
+ error = "No FancyZones monitors found.";
+ Logger.LogWarning($"TryGetMonitors: No monitors in file.");
+ return false;
+ }
+
+ monitors = editorMonitors
+ .Select((monitor, i) => new FancyZonesMonitorDescriptor(i + 1, monitor))
+ .ToArray();
+ Logger.LogInfo($"TryGetMonitors: Succeeded. MonitorCount={monitors.Count}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ error = $"Failed to read FancyZones monitor data: {ex.Message}";
+ Logger.LogError($"TryGetMonitors: Exception. Message={ex.Message} Stack={ex.StackTrace}");
+ return false;
+ }
+ }
+
+ public static IReadOnlyList GetLayouts()
+ {
+ Logger.LogInfo($"GetLayouts: Starting. LayoutTemplatesPath={FZPaths.LayoutTemplates} CustomLayoutsPath={FZPaths.CustomLayouts}");
+ var layouts = new List();
+ try
+ {
+ var templates = GetTemplateLayouts().ToArray();
+ Logger.LogInfo($"GetLayouts: GetTemplateLayouts returned {templates.Length} layouts");
+ layouts.AddRange(templates);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"GetLayouts: GetTemplateLayouts failed. Message={ex.Message} Stack={ex.StackTrace}");
+ }
+
+ try
+ {
+ var customLayouts = GetCustomLayouts().ToArray();
+ Logger.LogInfo($"GetLayouts: GetCustomLayouts returned {customLayouts.Length} layouts");
+ layouts.AddRange(customLayouts);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"GetLayouts: GetCustomLayouts failed. Message={ex.Message} Stack={ex.StackTrace}");
+ }
+
+ Logger.LogInfo($"GetLayouts: Total layouts={layouts.Count}");
+ return layouts;
+ }
+
+ public static bool TryGetAppliedLayoutForMonitor(EditorParameters.NativeMonitorDataWrapper monitor, out AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper? appliedLayout)
+ => TryGetAppliedLayoutForMonitor(monitor, FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString(), out appliedLayout);
+
+ public static bool TryGetAppliedLayoutForMonitor(EditorParameters.NativeMonitorDataWrapper monitor, string virtualDesktopId, out AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper? appliedLayout)
+ {
+ appliedLayout = null;
+
+ if (!TryReadAppliedLayouts(out var file))
+ {
+ return false;
+ }
+
+ var match = FindAppliedLayoutEntry(file, monitor, virtualDesktopId);
+ if (match is not null)
+ {
+ appliedLayout = match.Value.AppliedLayout;
+ return true;
+ }
+
+ return false;
+ }
+
+ public static (bool Success, string Message) ApplyLayoutToAllMonitors(FancyZonesLayoutDescriptor layout)
+ {
+ if (!TryGetMonitors(out var monitors, out var error))
+ {
+ return (false, error);
+ }
+
+ return ApplyLayoutToMonitors(layout, monitors.Select(m => m.Data));
+ }
+
+ public static (bool Success, string Message) ApplyLayoutToMonitor(FancyZonesLayoutDescriptor layout, FancyZonesMonitorDescriptor monitor)
+ {
+ if (!TryGetMonitors(out var monitors, out var error))
+ {
+ return (false, error);
+ }
+
+ EditorParameters.NativeMonitorDataWrapper? monitorData = null;
+ foreach (var candidate in monitors)
+ {
+ if (candidate.Data.MonitorInstanceId == monitor.Data.MonitorInstanceId)
+ {
+ monitorData = candidate.Data;
+ break;
+ }
+ }
+
+ if (monitorData is null)
+ {
+ return (false, "Monitor not found.");
+ }
+
+ return ApplyLayoutToMonitors(layout, [monitorData.Value]);
+ }
+
+ private static (bool Success, string Message) ApplyLayoutToMonitors(FancyZonesLayoutDescriptor layout, IEnumerable monitors)
+ {
+ AppliedLayouts.AppliedLayoutsListWrapper appliedFile;
+ if (!TryReadAppliedLayouts(out var existingFile))
+ {
+ appliedFile = new AppliedLayouts.AppliedLayoutsListWrapper { AppliedLayouts = new List() };
+ }
+ else
+ {
+ appliedFile = existingFile;
+ }
+
+ appliedFile.AppliedLayouts ??= new List();
+
+ var currentVirtualDesktop = FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString();
+
+ foreach (var monitor in monitors)
+ {
+ var existingEntry = FindAppliedLayoutEntry(appliedFile, monitor, currentVirtualDesktop);
+ if (existingEntry is not null)
+ {
+ // Remove the existing entry so we can add a new one
+ appliedFile.AppliedLayouts.Remove(existingEntry.Value);
+ }
+
+ var newEntry = new AppliedLayouts.AppliedLayoutWrapper
+ {
+ Device = new AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper
+ {
+ Monitor = monitor.Monitor,
+ MonitorInstance = monitor.MonitorInstanceId ?? string.Empty,
+ SerialNumber = monitor.MonitorSerialNumber ?? string.Empty,
+ MonitorNumber = monitor.MonitorNumber,
+ VirtualDesktop = currentVirtualDesktop,
+ },
+ AppliedLayout = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper
+ {
+ Uuid = layout.ApplyLayout.Uuid,
+ Type = layout.ApplyLayout.Type,
+ ZoneCount = layout.ApplyLayout.ZoneCount,
+ ShowSpacing = layout.ApplyLayout.ShowSpacing,
+ Spacing = layout.ApplyLayout.Spacing,
+ SensitivityRadius = layout.ApplyLayout.SensitivityRadius,
+ },
+ };
+
+ appliedFile.AppliedLayouts.Add(newEntry);
+ }
+
+ try
+ {
+ FancyZonesDataIO.WriteAppliedLayouts(appliedFile);
+ }
+ catch (Exception ex)
+ {
+ return (false, $"Failed to write applied layouts: {ex.Message}");
+ }
+
+ try
+ {
+ FancyZonesNotifier.NotifyAppliedLayoutsChanged();
+ }
+ catch (Exception ex)
+ {
+ return (true, $"Layout applied, but FancyZones could not be notified: {ex.Message}");
+ }
+
+ return (true, "Layout applied.");
+ }
+
+ private static AppliedLayouts.AppliedLayoutWrapper? FindAppliedLayoutEntry(AppliedLayouts.AppliedLayoutsListWrapper file, EditorParameters.NativeMonitorDataWrapper monitor, string virtualDesktopId)
+ {
+ if (file.AppliedLayouts is null)
+ {
+ return null;
+ }
+
+ return file.AppliedLayouts.FirstOrDefault(e =>
+ string.Equals(e.Device.Monitor, monitor.Monitor, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(e.Device.MonitorInstance ?? string.Empty, monitor.MonitorInstanceId ?? string.Empty, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(e.Device.SerialNumber ?? string.Empty, monitor.MonitorSerialNumber ?? string.Empty, StringComparison.OrdinalIgnoreCase) &&
+ e.Device.MonitorNumber == monitor.MonitorNumber &&
+ string.Equals(e.Device.VirtualDesktop, virtualDesktopId, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private static bool TryReadAppliedLayouts(out AppliedLayouts.AppliedLayoutsListWrapper file)
+ {
+ file = default;
+ try
+ {
+ if (!File.Exists(FZPaths.AppliedLayouts))
+ {
+ return false;
+ }
+
+ file = FancyZonesDataIO.ReadAppliedLayouts();
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static IEnumerable GetTemplateLayouts()
+ {
+ Logger.LogInfo($"GetTemplateLayouts: Starting. Path={FZPaths.LayoutTemplates} Exists={File.Exists(FZPaths.LayoutTemplates)}");
+
+ LayoutTemplates.TemplateLayoutsListWrapper templates;
+ try
+ {
+ if (!File.Exists(FZPaths.LayoutTemplates))
+ {
+ Logger.LogWarning($"GetTemplateLayouts: File not found.");
+ yield break;
+ }
+
+ templates = FancyZonesDataIO.ReadLayoutTemplates();
+ Logger.LogInfo($"GetTemplateLayouts: ReadLayoutTemplates succeeded. Count={templates.LayoutTemplates?.Count ?? -1}");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"GetTemplateLayouts: ReadLayoutTemplates failed. Message={ex.Message} Stack={ex.StackTrace}");
+ yield break;
+ }
+
+ var templateLayouts = templates.LayoutTemplates;
+ if (templateLayouts is null)
+ {
+ Logger.LogWarning($"GetTemplateLayouts: LayoutTemplates is null.");
+ yield break;
+ }
+
+ foreach (var template in templateLayouts)
+ {
+ if (string.IsNullOrWhiteSpace(template.Type))
+ {
+ continue;
+ }
+
+ var type = template.Type.Trim();
+ var zoneCount = type.Equals("blank", StringComparison.OrdinalIgnoreCase)
+ ? 0
+ : template.ZoneCount > 0 ? template.ZoneCount : 3;
+ var title = $"Template: {type}";
+ var subtitle = $"{zoneCount} zones";
+
+ yield return new FancyZonesLayoutDescriptor
+ {
+ Id = $"template:{type.ToLowerInvariant()}",
+ Source = FancyZonesLayoutSource.Template,
+ Title = title,
+ Subtitle = subtitle,
+ Template = template,
+ ApplyLayout = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper
+ {
+ Type = type.ToLowerInvariant(),
+ Uuid = ZeroUuid,
+ ZoneCount = zoneCount,
+ ShowSpacing = template.ShowSpacing,
+ Spacing = template.Spacing,
+ SensitivityRadius = template.SensitivityRadius,
+ },
+ };
+ }
+ }
+
+ private static IEnumerable GetCustomLayouts()
+ {
+ CustomLayouts.CustomLayoutListWrapper customLayouts;
+ try
+ {
+ if (!File.Exists(FZPaths.CustomLayouts))
+ {
+ yield break;
+ }
+
+ customLayouts = FancyZonesDataIO.ReadCustomLayouts();
+ }
+ catch
+ {
+ yield break;
+ }
+
+ var layouts = customLayouts.CustomLayouts;
+ if (layouts is null)
+ {
+ yield break;
+ }
+
+ foreach (var custom in layouts)
+ {
+ if (string.IsNullOrWhiteSpace(custom.Uuid) || string.IsNullOrWhiteSpace(custom.Name))
+ {
+ continue;
+ }
+
+ var uuid = custom.Uuid.Trim();
+ var customType = custom.Type?.Trim().ToLowerInvariant() ?? string.Empty;
+
+ if (!TryBuildAppliedLayoutForCustom(custom, out var applied))
+ {
+ continue;
+ }
+
+ var title = custom.Name.Trim();
+ var subtitle = customType switch
+ {
+ "grid" => $"Custom grid \u2022 {applied.ZoneCount} zones",
+ "canvas" => $"Custom canvas \u2022 {applied.ZoneCount} zones",
+ _ => $"Custom \u2022 {applied.ZoneCount} zones",
+ };
+
+ yield return new FancyZonesLayoutDescriptor
+ {
+ Id = $"custom:{uuid}",
+ Source = FancyZonesLayoutSource.Custom,
+ Title = title,
+ Subtitle = subtitle,
+ Custom = custom,
+ ApplyLayout = applied,
+ };
+ }
+ }
+
+ private static bool TryBuildAppliedLayoutForCustom(CustomLayouts.CustomLayoutWrapper custom, out AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper applied)
+ {
+ applied = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper
+ {
+ Type = "custom",
+ Uuid = custom.Uuid.Trim(),
+ ShowSpacing = false,
+ Spacing = 0,
+ ZoneCount = 0,
+ SensitivityRadius = 20,
+ };
+
+ if (custom.Info.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
+ {
+ return false;
+ }
+
+ var customType = custom.Type?.Trim().ToLowerInvariant() ?? string.Empty;
+ if (customType == "grid")
+ {
+ if (!TryParseCustomGridInfo(custom.Info, out var zoneCount, out var showSpacing, out var spacing, out var sensitivity))
+ {
+ return false;
+ }
+
+ applied.ZoneCount = zoneCount;
+ applied.ShowSpacing = showSpacing;
+ applied.Spacing = spacing;
+ applied.SensitivityRadius = sensitivity;
+ return true;
+ }
+
+ if (customType == "canvas")
+ {
+ if (!TryParseCustomCanvasInfo(custom.Info, out var zoneCount, out var sensitivity))
+ {
+ return false;
+ }
+
+ applied.ZoneCount = zoneCount;
+ applied.SensitivityRadius = sensitivity;
+ applied.ShowSpacing = false;
+ applied.Spacing = 0;
+ return true;
+ }
+
+ return false;
+ }
+
+ internal static bool TryParseCustomGridInfo(JsonElement info, out int zoneCount, out bool showSpacing, out int spacing, out int sensitivityRadius)
+ {
+ zoneCount = 0;
+ showSpacing = false;
+ spacing = 0;
+ sensitivityRadius = 20;
+
+ if (!info.TryGetProperty("rows", out var rowsProp) ||
+ !info.TryGetProperty("columns", out var columnsProp) ||
+ rowsProp.ValueKind != JsonValueKind.Number ||
+ columnsProp.ValueKind != JsonValueKind.Number)
+ {
+ return false;
+ }
+
+ var rows = rowsProp.GetInt32();
+ var columns = columnsProp.GetInt32();
+ if (rows <= 0 || columns <= 0)
+ {
+ return false;
+ }
+
+ if (info.TryGetProperty("cell-child-map", out var cellMap) && cellMap.ValueKind == JsonValueKind.Array)
+ {
+ var max = -1;
+ foreach (var row in cellMap.EnumerateArray())
+ {
+ if (row.ValueKind != JsonValueKind.Array)
+ {
+ continue;
+ }
+
+ foreach (var cell in row.EnumerateArray())
+ {
+ if (cell.ValueKind == JsonValueKind.Number && cell.TryGetInt32(out var value))
+ {
+ max = Math.Max(max, value);
+ }
+ }
+ }
+
+ zoneCount = max + 1;
+ }
+ else
+ {
+ zoneCount = rows * columns;
+ }
+
+ if (zoneCount <= 0)
+ {
+ return false;
+ }
+
+ if (info.TryGetProperty("show-spacing", out var showSpacingProp) &&
+ (showSpacingProp.ValueKind == JsonValueKind.True || showSpacingProp.ValueKind == JsonValueKind.False))
+ {
+ showSpacing = showSpacingProp.GetBoolean();
+ }
+
+ if (info.TryGetProperty("spacing", out var spacingProp) && spacingProp.ValueKind == JsonValueKind.Number)
+ {
+ spacing = spacingProp.GetInt32();
+ }
+
+ if (info.TryGetProperty("sensitivity-radius", out var sensitivityProp) && sensitivityProp.ValueKind == JsonValueKind.Number)
+ {
+ sensitivityRadius = sensitivityProp.GetInt32();
+ }
+
+ return true;
+ }
+
+ internal static bool TryParseCustomCanvasInfo(JsonElement info, out int zoneCount, out int sensitivityRadius)
+ {
+ zoneCount = 0;
+ sensitivityRadius = 20;
+
+ if (!info.TryGetProperty("zones", out var zones) || zones.ValueKind != JsonValueKind.Array)
+ {
+ return false;
+ }
+
+ zoneCount = zones.GetArrayLength();
+
+ if (info.TryGetProperty("sensitivity-radius", out var sensitivityProp) && sensitivityProp.ValueKind == JsonValueKind.Number)
+ {
+ sensitivityRadius = sensitivityProp.GetInt32();
+ }
+
+ return zoneCount >= 0;
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor.cs
new file mode 100644
index 0000000000..0c99dcc8f4
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using FancyZonesEditorCommon.Data;
+
+namespace PowerToysExtension.Helpers;
+
+internal sealed class FancyZonesLayoutDescriptor
+{
+ public required string Id { get; init; } // "template:" or "custom:"
+
+ public required FancyZonesLayoutSource Source { get; init; }
+
+ public required string Title { get; init; }
+
+ public required string Subtitle { get; init; }
+
+ public required AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper ApplyLayout { get; init; }
+
+ public LayoutTemplates.TemplateLayoutWrapper? Template { get; init; }
+
+ public CustomLayouts.CustomLayoutWrapper? Custom { get; init; }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor1.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor1.cs
new file mode 100644
index 0000000000..194a9a206c
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor1.cs
@@ -0,0 +1,5 @@
+// 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.
+
+// Intentionally empty: this file was an accidental duplicate of FancyZonesLayoutDescriptor.cs.
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutSource.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutSource.cs
new file mode 100644
index 0000000000..d75e3faccd
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutSource.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace PowerToysExtension.Helpers;
+
+internal enum FancyZonesLayoutSource
+{
+ Template,
+ Custom,
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorDescriptor.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorDescriptor.cs
new file mode 100644
index 0000000000..4e3f7092a4
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorDescriptor.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Globalization;
+
+using FancyZonesEditorCommon.Data;
+
+namespace PowerToysExtension.Helpers;
+
+internal readonly record struct FancyZonesMonitorDescriptor(
+ int Index,
+ EditorParameters.NativeMonitorDataWrapper Data)
+{
+ public string Title => Data.Monitor;
+
+ public string Subtitle
+ {
+ get
+ {
+ var size = $"{Data.MonitorWidth}×{Data.MonitorHeight}";
+ var scaling = Data.Dpi > 0 ? string.Format(CultureInfo.InvariantCulture, "{0}%", (int)Math.Round(Data.Dpi * 100 / 96.0)) : "n/a";
+ return $"{size} \u2022 {scaling}";
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorIdentifier.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorIdentifier.cs
new file mode 100644
index 0000000000..afa908e08b
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorIdentifier.cs
@@ -0,0 +1,461 @@
+// 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.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace PowerToysExtension.Helpers;
+
+internal static class FancyZonesMonitorIdentifier
+{
+ private const string WindowClassName = "PowerToys_FancyZones_MonitorIdentify";
+
+ private const uint WsExToolWindow = 0x00000080;
+ private const uint WsExTopmost = 0x00000008;
+ private const uint WsExTransparent = 0x00000020;
+ private const uint WsPopup = 0x80000000;
+
+ private const uint WmDestroy = 0x0002;
+ private const uint WmPaint = 0x000F;
+ private const uint WmTimer = 0x0113;
+
+ private const uint CsVRedraw = 0x0001;
+ private const uint CsHRedraw = 0x0002;
+
+ private const int SwShowNoActivate = 4;
+
+ private const int Transparent = 1;
+
+ private const int BaseFontHeightPx = 52;
+ private const int BaseDpi = 96;
+
+ private const uint DtCenter = 0x00000001;
+ private const uint DtVCenter = 0x00000004;
+ private const uint DtSingleLine = 0x00000020;
+
+ private const uint MonitorDefaultToNearest = 2;
+
+ private static readonly nint DpiAwarenessContextUnaware = new(-1);
+
+ private static readonly object Sync = new();
+ private static bool classRegistered;
+
+ private static GCHandle? currentPinnedTextHandle;
+
+ public static void Show(int left, int top, int width, int height, string text, int durationMs = 1200)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ text = "Monitor";
+ }
+
+ _ = Task.Run(() => RunWindow(left, top, width, height, text, durationMs))
+ .ContinueWith(static t => _ = t.Exception, TaskContinuationOptions.OnlyOnFaulted);
+ }
+
+ private static unsafe void RunWindow(int left, int top, int width, int height, string text, int durationMs)
+ {
+ EnsureClassRegistered();
+
+ var workArea = TryGetWorkAreaFromFancyZonesCoordinates(left, top, width, height, out var resolvedWorkArea)
+ ? resolvedWorkArea
+ : new RECT
+ {
+ Left = left,
+ Top = top,
+ Right = left + width,
+ Bottom = top + height,
+ };
+
+ var workAreaWidth = Math.Max(0, workArea.Right - workArea.Left);
+ var workAreaHeight = Math.Max(0, workArea.Bottom - workArea.Top);
+
+ var overlayWidth = Math.Clamp(workAreaWidth / 4, 220, 420);
+ var overlayHeight = Math.Clamp(workAreaHeight / 6, 120, 240);
+
+ var x = workArea.Left + ((workAreaWidth - overlayWidth) / 2);
+ var y = workArea.Top + ((workAreaHeight - overlayHeight) / 2);
+
+ lock (Sync)
+ {
+ currentPinnedTextHandle?.Free();
+ currentPinnedTextHandle = GCHandle.Alloc(text, GCHandleType.Pinned);
+ }
+
+ var hwnd = CreateWindowExW(
+ WsExToolWindow | WsExTopmost | WsExTransparent,
+ WindowClassName,
+ "MonitorIdentify",
+ WsPopup,
+ x,
+ y,
+ overlayWidth,
+ overlayHeight,
+ nint.Zero,
+ nint.Zero,
+ GetModuleHandleW(null),
+ nint.Zero);
+
+ if (hwnd == nint.Zero)
+ {
+ return;
+ }
+
+ _ = ShowWindow(hwnd, SwShowNoActivate);
+ _ = UpdateWindow(hwnd);
+
+ _ = SetTimer(hwnd, 1, (uint)durationMs, nint.Zero);
+
+ MSG msg;
+ while (GetMessageW(out msg, nint.Zero, 0, 0) != 0)
+ {
+ _ = TranslateMessage(in msg);
+ _ = DispatchMessageW(in msg);
+ }
+
+ lock (Sync)
+ {
+ currentPinnedTextHandle?.Free();
+ currentPinnedTextHandle = null;
+ }
+ }
+
+ private static unsafe void EnsureClassRegistered()
+ {
+ lock (Sync)
+ {
+ if (classRegistered)
+ {
+ return;
+ }
+
+ fixed (char* className = WindowClassName ?? string.Empty)
+ {
+ var wc = new WNDCLASSEXW
+ {
+ CbSize = (uint)sizeof(WNDCLASSEXW),
+ Style = CsHRedraw | CsVRedraw,
+ LpfnWndProc = &WndProc,
+ HInstance = GetModuleHandleW(null),
+ HCursor = LoadCursorW(nint.Zero, new IntPtr(32512)), // IDC_ARROW
+ LpszClassName = className,
+ };
+
+ _ = RegisterClassExW(in wc);
+ classRegistered = true;
+ }
+ }
+ }
+
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
+ private static unsafe nint WndProc(nint hwnd, uint msg, nuint wParam, nint lParam)
+ {
+ switch (msg)
+ {
+ case WmTimer:
+ _ = KillTimer(hwnd, 1);
+ _ = DestroyWindow(hwnd);
+ return nint.Zero;
+
+ case WmDestroy:
+ PostQuitMessage(0);
+ return nint.Zero;
+
+ case WmPaint:
+ {
+ var hdc = BeginPaint(hwnd, out var ps);
+
+ _ = GetClientRect(hwnd, out var rect);
+
+ var bgBrush = CreateSolidBrush(0x202020);
+ _ = FillRect(hdc, in rect, bgBrush);
+
+ _ = SetBkMode(hdc, Transparent);
+ _ = SetTextColor(hdc, 0xFFFFFF);
+
+ var dpi = GetDpiForWindow(hwnd);
+ var fontHeight = -MulDiv(BaseFontHeightPx, (int)dpi, BaseDpi);
+ var font = CreateFontW(
+ fontHeight,
+ 0,
+ 0,
+ 0,
+ 700,
+ 0,
+ 0,
+ 0,
+ 1, // DEFAULT_CHARSET
+ 0, // OUT_DEFAULT_PRECIS
+ 0, // CLIP_DEFAULT_PRECIS
+ 5, // CLEARTYPE_QUALITY
+ 0x20, // FF_SWISS
+ "Segoe UI");
+
+ var oldFont = SelectObject(hdc, font);
+
+ var textPtr = GetPinnedTextPointer();
+ if (textPtr is not null)
+ {
+ var textNint = (nint)textPtr;
+ _ = DrawTextW(hdc, textNint, -1, ref rect, DtCenter | DtVCenter | DtSingleLine);
+ }
+
+ _ = SelectObject(hdc, oldFont);
+ _ = DeleteObject(font);
+ _ = DeleteObject(bgBrush);
+
+ _ = EndPaint(hwnd, ref ps);
+ return nint.Zero;
+ }
+ }
+
+ return DefWindowProcW(hwnd, msg, wParam, lParam);
+ }
+
+ private static unsafe char* GetPinnedTextPointer()
+ {
+ lock (Sync)
+ {
+ if (!currentPinnedTextHandle.HasValue || !currentPinnedTextHandle.Value.IsAllocated)
+ {
+ return null;
+ }
+
+ return (char*)currentPinnedTextHandle.Value.AddrOfPinnedObject();
+ }
+ }
+
+ private static bool TryGetWorkAreaFromFancyZonesCoordinates(int left, int top, int width, int height, out RECT workArea)
+ {
+ workArea = default;
+
+ if (width <= 0 || height <= 0)
+ {
+ return false;
+ }
+
+ var logicalRect = new RECT
+ {
+ Left = left,
+ Top = top,
+ Right = left + width,
+ Bottom = top + height,
+ };
+
+ var previousContext = SetThreadDpiAwarenessContext(DpiAwarenessContextUnaware);
+ nint monitor;
+ try
+ {
+ monitor = MonitorFromRect(ref logicalRect, MonitorDefaultToNearest);
+ }
+ finally
+ {
+ _ = SetThreadDpiAwarenessContext(previousContext);
+ }
+
+ if (monitor == nint.Zero)
+ {
+ return false;
+ }
+
+ var mi = new MONITORINFOEXW
+ {
+ CbSize = (uint)Marshal.SizeOf(),
+ };
+
+ if (!GetMonitorInfoW(monitor, ref mi))
+ {
+ return false;
+ }
+
+ workArea = mi.RcWork;
+ return true;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private unsafe struct WNDCLASSEXW
+ {
+ public uint CbSize;
+ public uint Style;
+ public delegate* unmanaged[Stdcall] LpfnWndProc;
+ public int CbClsExtra;
+ public int CbWndExtra;
+ public nint HInstance;
+ public nint HIcon;
+ public nint HCursor;
+ public nint HbrBackground;
+ public char* LpszMenuName;
+ public char* LpszClassName;
+ public nint HIconSm;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct POINT
+ {
+ public int X;
+ public int Y;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct MSG
+ {
+ public nint Hwnd;
+ public uint Message;
+ public nuint WParam;
+ public nint LParam;
+ public uint Time;
+ public POINT Pt;
+ public uint LPrivate;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct RECT
+ {
+ public int Left;
+ public int Top;
+ public int Right;
+ public int Bottom;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ private struct MONITORINFOEXW
+ {
+ public uint CbSize;
+ public RECT RcMonitor;
+ public RECT RcWork;
+ public uint DwFlags;
+
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
+ public string SzDevice;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private unsafe struct PAINTSTRUCT
+ {
+ public nint Hdc;
+ public int FErase;
+ public RECT RcPaint;
+ public int FRestore;
+ public int FIncUpdate;
+ public fixed byte RgbReserved[32];
+ }
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern nint GetModuleHandleW(string? lpModuleName);
+
+ [DllImport("kernel32.dll")]
+ private static extern int MulDiv(int nNumber, int nNumerator, int nDenominator);
+
+ [DllImport("user32.dll")]
+ private static extern uint GetDpiForWindow(nint hwnd);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern bool UpdateWindow(nint hWnd);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern bool ShowWindow(nint hWnd, int nCmdShow);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern bool DestroyWindow(nint hWnd);
+
+ [DllImport("user32.dll")]
+ private static extern void PostQuitMessage(int nExitCode);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern bool KillTimer(nint hWnd, nuint uIDEvent);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern nuint SetTimer(nint hWnd, nuint nIDEvent, uint uElapse, nint lpTimerFunc);
+
+ [DllImport("user32.dll")]
+ private static extern int GetMessageW(out MSG lpMsg, nint hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
+
+ [DllImport("user32.dll")]
+ private static extern bool TranslateMessage(in MSG lpMsg);
+
+ [DllImport("user32.dll")]
+ private static extern nint DispatchMessageW(in MSG lpMsg);
+
+ [DllImport("user32.dll")]
+ private static extern nint SetThreadDpiAwarenessContext(nint dpiContext);
+
+ [DllImport("user32.dll")]
+ private static extern nint MonitorFromRect(ref RECT lprc, uint dwFlags);
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
+ private static extern bool GetMonitorInfoW(nint hMonitor, ref MONITORINFOEXW lpmi);
+
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern ushort RegisterClassExW(in WNDCLASSEXW lpwcx);
+
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern nint CreateWindowExW(
+ uint dwExStyle,
+ string lpClassName,
+ string lpWindowName,
+ uint dwStyle,
+ int x,
+ int y,
+ int nWidth,
+ int nHeight,
+ nint hWndParent,
+ nint hMenu,
+ nint hInstance,
+ nint lpParam);
+
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern nint LoadCursorW(nint hInstance, nint lpCursorName);
+
+ [DllImport("user32.dll")]
+ private static extern nint DefWindowProcW(nint hWnd, uint msg, nuint wParam, nint lParam);
+
+ [DllImport("user32.dll")]
+ private static extern nint BeginPaint(nint hWnd, out PAINTSTRUCT lpPaint);
+
+ [DllImport("user32.dll")]
+ private static extern bool EndPaint(nint hWnd, ref PAINTSTRUCT lpPaint);
+
+ [DllImport("user32.dll")]
+ private static extern bool GetClientRect(nint hWnd, out RECT lpRect);
+
+ [DllImport("user32.dll")]
+ private static extern int FillRect(nint hDC, in RECT lprc, nint hbr);
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
+ private static extern int DrawTextW(nint hdc, nint lpchText, int cchText, ref RECT lprc, uint format);
+
+ [DllImport("gdi32.dll")]
+ private static extern nint CreateSolidBrush(uint colorRef);
+
+ [DllImport("gdi32.dll")]
+ private static extern int SetBkMode(nint hdc, int mode);
+
+ [DllImport("gdi32.dll")]
+ private static extern uint SetTextColor(nint hdc, uint colorRef);
+
+ [DllImport("gdi32.dll", CharSet = CharSet.Unicode)]
+ private static extern nint CreateFontW(
+ int nHeight,
+ int nWidth,
+ int nEscapement,
+ int nOrientation,
+ int fnWeight,
+ uint fdwItalic,
+ uint fdwUnderline,
+ uint fdwStrikeOut,
+ uint fdwCharSet,
+ uint fdwOutputPrecision,
+ uint fdwClipPrecision,
+ uint fdwQuality,
+ uint fdwPitchAndFamily,
+ string lpszFace);
+
+ [DllImport("gdi32.dll")]
+ private static extern nint SelectObject(nint hdc, nint hgdiobj);
+
+ [DllImport("gdi32.dll")]
+ private static extern bool DeleteObject(nint hObject);
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorPreviewRenderer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorPreviewRenderer.cs
new file mode 100644
index 0000000000..fc22b1a0cd
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorPreviewRenderer.cs
@@ -0,0 +1,399 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+
+using FancyZonesEditorCommon.Data;
+
+using ManagedCommon;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using Windows.Graphics.Imaging;
+using Windows.Storage.Streams;
+
+using InteropConstants = PowerToys.Interop.Constants;
+
+namespace PowerToysExtension.Helpers;
+
+internal static class FancyZonesMonitorPreviewRenderer
+{
+ public static IconInfo? TryRenderMonitorHeroImage(FancyZonesMonitorDescriptor monitor)
+ {
+ try
+ {
+ var cached = TryGetCachedIcon(monitor);
+ if (cached is not null)
+ {
+ return cached;
+ }
+
+ var icon = RenderMonitorHeroImageAsync(monitor).GetAwaiter().GetResult();
+ return icon;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"FancyZones monitor hero render failed. Monitor={monitor.Data.Monitor} Index={monitor.Index} Exception={ex}");
+ return null;
+ }
+ }
+
+ private static IconInfo? TryGetCachedIcon(FancyZonesMonitorDescriptor monitor)
+ {
+ var cachePath = GetCachePath(monitor);
+ if (string.IsNullOrEmpty(cachePath))
+ {
+ return null;
+ }
+
+ try
+ {
+ if (File.Exists(cachePath))
+ {
+ return new IconInfo(cachePath);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"FancyZones monitor hero cache check failed. Path=\"{cachePath}\" Monitor={monitor.Data.Monitor} Index={monitor.Index} Exception={ex}");
+ }
+
+ return null;
+ }
+
+ private static async Task RenderMonitorHeroImageAsync(FancyZonesMonitorDescriptor monitor)
+ {
+ var cachePath = GetCachePath(monitor);
+ if (string.IsNullOrEmpty(cachePath))
+ {
+ return null;
+ }
+
+ var (widthPx, heightPx) = ComputeCanvasSize(monitor.Data);
+ Logger.LogDebug($"FancyZones monitor hero render starting. Monitor={monitor.Data.Monitor} Index={monitor.Index} Size={widthPx}x{heightPx}");
+
+ var (layoutRectangles, spacing) = GetLayoutRectangles(monitor.Data);
+ var pixelBytes = RenderMonitorPreviewBgra(widthPx, heightPx, layoutRectangles, spacing);
+
+ var stream = new InMemoryRandomAccessStream();
+ var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
+ encoder.SetPixelData(
+ BitmapPixelFormat.Bgra8,
+ BitmapAlphaMode.Premultiplied,
+ (uint)widthPx,
+ (uint)heightPx,
+ 96,
+ 96,
+ pixelBytes);
+ await encoder.FlushAsync();
+ stream.Seek(0);
+
+ try
+ {
+ var tempPath = FormattableString.Invariant($"{cachePath}.{Guid.NewGuid():N}.tmp");
+ Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
+ await WriteStreamToFileAsync(stream, tempPath);
+ File.Move(tempPath, cachePath, overwrite: true);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"FancyZones monitor hero cache write failed. Path=\"{cachePath}\" Monitor={monitor.Data.Monitor} Index={monitor.Index} Exception={ex}");
+ return null;
+ }
+
+ Logger.LogDebug($"FancyZones monitor hero render succeeded. Monitor={monitor.Data.Monitor} Index={monitor.Index} Path=\"{cachePath}\"");
+ return new IconInfo(cachePath);
+ }
+
+ private static (int WidthPx, int HeightPx) ComputeCanvasSize(EditorParameters.NativeMonitorDataWrapper monitor)
+ {
+ const int maxDim = 320;
+ var w = monitor.WorkAreaWidth > 0 ? monitor.WorkAreaWidth : monitor.MonitorWidth;
+ var h = monitor.WorkAreaHeight > 0 ? monitor.WorkAreaHeight : monitor.MonitorHeight;
+
+ if (w <= 0 || h <= 0)
+ {
+ return (maxDim, 180);
+ }
+
+ var aspect = (float)w / h;
+ if (aspect >= 1)
+ {
+ var height = (int)Math.Clamp(Math.Round(maxDim / aspect), 90, maxDim);
+ return (maxDim, height);
+ }
+ else
+ {
+ var width = (int)Math.Clamp(Math.Round(maxDim * aspect), 90, maxDim);
+ return (width, maxDim);
+ }
+ }
+
+ private static (List Rects, int Spacing) GetLayoutRectangles(EditorParameters.NativeMonitorDataWrapper monitor)
+ {
+ if (!FancyZonesDataService.TryGetAppliedLayoutForMonitor(monitor, out var applied) || applied is null)
+ {
+ return ([], 0);
+ }
+
+ var layout = FindLayoutDescriptor(applied.Value);
+ if (layout is null)
+ {
+ return ([], 0);
+ }
+
+ var rects = FancyZonesThumbnailRenderer.GetNormalizedRectsForLayout(layout);
+ var spacing = layout.ApplyLayout.ShowSpacing && layout.ApplyLayout.Spacing > 0 ? layout.ApplyLayout.Spacing : 0;
+ return (rects, spacing);
+ }
+
+ private static FancyZonesLayoutDescriptor? FindLayoutDescriptor(AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper applied)
+ {
+ try
+ {
+ var layouts = FancyZonesDataService.GetLayouts();
+
+ if (!string.IsNullOrWhiteSpace(applied.Uuid) &&
+ !applied.Uuid.Equals("{00000000-0000-0000-0000-000000000000}", StringComparison.OrdinalIgnoreCase))
+ {
+ return layouts.FirstOrDefault(l => l.Source == FancyZonesLayoutSource.Custom &&
+ l.Custom is not null &&
+ string.Equals(l.Custom.Value.Uuid?.Trim(), applied.Uuid.Trim(), StringComparison.OrdinalIgnoreCase));
+ }
+
+ var type = applied.Type?.Trim().ToLowerInvariant() ?? string.Empty;
+ var zoneCount = applied.ZoneCount;
+ return layouts.FirstOrDefault(l =>
+ l.Source == FancyZonesLayoutSource.Template &&
+ string.Equals(l.ApplyLayout.Type, type, StringComparison.OrdinalIgnoreCase) &&
+ l.ApplyLayout.ZoneCount == zoneCount &&
+ l.ApplyLayout.ShowSpacing == applied.ShowSpacing &&
+ l.ApplyLayout.Spacing == applied.Spacing);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static string? GetCachePath(FancyZonesMonitorDescriptor monitor)
+ {
+ try
+ {
+ var basePath = InteropConstants.AppDataPath();
+ if (string.IsNullOrWhiteSpace(basePath))
+ {
+ return null;
+ }
+
+ var cacheFolder = Path.Combine(basePath, "CmdPal", "PowerToysExtension", "Cache", "FancyZones", "MonitorPreviews");
+ var fileName = ComputeMonitorHash(monitor) + ".png";
+ return Path.Combine(cacheFolder, fileName);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static string ComputeMonitorHash(FancyZonesMonitorDescriptor monitor)
+ {
+ var currentVirtualDesktop = FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString();
+ var appliedFingerprint = string.Empty;
+ if (FancyZonesDataService.TryGetAppliedLayoutForMonitor(monitor.Data, out var applied) && applied is not null)
+ {
+ appliedFingerprint = FormattableString.Invariant($"{applied.Value.Type}|{applied.Value.Uuid}|{applied.Value.ZoneCount}|{applied.Value.ShowSpacing}|{applied.Value.Spacing}");
+ }
+
+ var identity = FormattableString.Invariant(
+ $"{monitor.Data.Monitor}|{monitor.Data.MonitorInstanceId}|{monitor.Data.MonitorSerialNumber}|{monitor.Data.MonitorNumber}|{currentVirtualDesktop}|{monitor.Data.WorkAreaWidth}x{monitor.Data.WorkAreaHeight}|{monitor.Data.MonitorWidth}x{monitor.Data.MonitorHeight}|{appliedFingerprint}");
+
+ var bytes = Encoding.UTF8.GetBytes(identity);
+ var hash = SHA256.HashData(bytes);
+ return Convert.ToHexString(hash).ToLowerInvariant();
+ }
+
+ private static byte[] RenderMonitorPreviewBgra(
+ int widthPx,
+ int heightPx,
+ IReadOnlyList rects,
+ int spacing)
+ {
+ var pixels = new byte[widthPx * heightPx * 4];
+
+ var frame = Premultiply(new BgraColor(0x80, 0x80, 0x80, 0xFF));
+ var bezelFill = Premultiply(new BgraColor(0x20, 0x20, 0x20, 0x18));
+ var screenFill = Premultiply(new BgraColor(0x00, 0x00, 0x00, 0x00));
+ var border = Premultiply(new BgraColor(0xFF, 0xD8, 0x8C, 0xFF));
+ var fill = Premultiply(new BgraColor(0xFF, 0xD8, 0x8C, 0xC0));
+ var background = Premultiply(new BgraColor(0x00, 0x00, 0x00, 0x00));
+
+ for (var i = 0; i < pixels.Length; i += 4)
+ {
+ pixels[i + 0] = background.B;
+ pixels[i + 1] = background.G;
+ pixels[i + 2] = background.R;
+ pixels[i + 3] = background.A;
+ }
+
+ DrawRectBorder(pixels, widthPx, heightPx, 0, 0, widthPx, heightPx, frame);
+
+ const int bezel = 3;
+ FillRect(pixels, widthPx, heightPx, 1, 1, widthPx - 1, heightPx - 1, bezelFill);
+ FillRect(pixels, widthPx, heightPx, 1 + bezel, 1 + bezel, widthPx - 1 - bezel, heightPx - 1 - bezel, screenFill);
+
+ var innerLeft = 1 + bezel;
+ var innerTop = 1 + bezel;
+ var innerRight = widthPx - 1 - bezel;
+ var innerBottom = heightPx - 1 - bezel;
+ var innerWidth = Math.Max(1, innerRight - innerLeft);
+ var innerHeight = Math.Max(1, innerBottom - innerTop);
+
+ var gapPx = spacing > 0 ? Math.Clamp(spacing / 8, 1, 3) : 0;
+ foreach (var rect in rects)
+ {
+ var (x1, y1, x2, y2) = ToPixelBounds(rect, innerLeft, innerTop, innerWidth, innerHeight, gapPx);
+ if (x2 <= x1 || y2 <= y1)
+ {
+ continue;
+ }
+
+ FillRect(pixels, widthPx, heightPx, x1, y1, x2, y2, fill);
+ DrawRectBorder(pixels, widthPx, heightPx, x1, y1, x2, y2, border);
+ }
+
+ return pixels;
+ }
+
+ private static (int X1, int Y1, int X2, int Y2) ToPixelBounds(
+ FancyZonesThumbnailRenderer.NormalizedRect rect,
+ int originX,
+ int originY,
+ int widthPx,
+ int heightPx,
+ int gapPx)
+ {
+ var x1 = originX + (int)MathF.Round(rect.X * widthPx);
+ var y1 = originY + (int)MathF.Round(rect.Y * heightPx);
+ var x2 = originX + (int)MathF.Round((rect.X + rect.Width) * widthPx);
+ var y2 = originY + (int)MathF.Round((rect.Y + rect.Height) * heightPx);
+
+ x1 = Math.Clamp(x1 + gapPx, originX, originX + widthPx - 1);
+ y1 = Math.Clamp(y1 + gapPx, originY, originY + heightPx - 1);
+ x2 = Math.Clamp(x2 - gapPx, originX + 1, originX + widthPx);
+ y2 = Math.Clamp(y2 - gapPx, originY + 1, originY + heightPx);
+
+ if (x2 <= x1 + 1)
+ {
+ x2 = Math.Min(originX + widthPx, x1 + 2);
+ }
+
+ if (y2 <= y1 + 1)
+ {
+ y2 = Math.Min(originY + heightPx, y1 + 2);
+ }
+
+ return (x1, y1, x2, y2);
+ }
+
+ private static void FillRect(byte[] pixels, int widthPx, int heightPx, int x1, int y1, int x2, int y2, BgraColor color)
+ {
+ for (var y = y1; y < y2; y++)
+ {
+ if ((uint)y >= (uint)heightPx)
+ {
+ continue;
+ }
+
+ var rowStart = y * widthPx * 4;
+ for (var x = x1; x < x2; x++)
+ {
+ if ((uint)x >= (uint)widthPx)
+ {
+ continue;
+ }
+
+ var i = rowStart + (x * 4);
+ pixels[i + 0] = color.B;
+ pixels[i + 1] = color.G;
+ pixels[i + 2] = color.R;
+ pixels[i + 3] = color.A;
+ }
+ }
+ }
+
+ private static void DrawRectBorder(byte[] pixels, int widthPx, int heightPx, int x1, int y1, int x2, int y2, BgraColor color)
+ {
+ var left = x1;
+ var right = x2 - 1;
+ var top = y1;
+ var bottom = y2 - 1;
+
+ for (var x = left; x <= right; x++)
+ {
+ SetPixel(pixels, widthPx, heightPx, x, top, color);
+ SetPixel(pixels, widthPx, heightPx, x, bottom, color);
+ }
+
+ for (var y = top; y <= bottom; y++)
+ {
+ SetPixel(pixels, widthPx, heightPx, left, y, color);
+ SetPixel(pixels, widthPx, heightPx, right, y, color);
+ }
+ }
+
+ private static void SetPixel(byte[] pixels, int widthPx, int heightPx, int x, int y, BgraColor color)
+ {
+ if ((uint)x >= (uint)widthPx || (uint)y >= (uint)heightPx)
+ {
+ return;
+ }
+
+ var i = ((y * widthPx) + x) * 4;
+ pixels[i + 0] = color.B;
+ pixels[i + 1] = color.G;
+ pixels[i + 2] = color.R;
+ pixels[i + 3] = color.A;
+ }
+
+ private static BgraColor Premultiply(BgraColor color)
+ {
+ if (color.A == 0 || color.A == 255)
+ {
+ return color;
+ }
+
+ byte Premul(byte c) => (byte)(((c * color.A) + 127) / 255);
+ return new BgraColor(Premul(color.B), Premul(color.G), Premul(color.R), color.A);
+ }
+
+ private readonly record struct BgraColor(byte B, byte G, byte R, byte A);
+
+ private static async Task WriteStreamToFileAsync(IRandomAccessStream stream, string filePath)
+ {
+ stream.Seek(0);
+ var size = stream.Size;
+ if (size == 0)
+ {
+ File.WriteAllBytes(filePath, Array.Empty());
+ return;
+ }
+
+ if (size > int.MaxValue)
+ {
+ throw new InvalidOperationException("Icon stream too large.");
+ }
+
+ using var input = stream.GetInputStreamAt(0);
+ using var reader = new DataReader(input);
+ await reader.LoadAsync((uint)size);
+ var bytes = new byte[(int)size];
+ reader.ReadBytes(bytes);
+ File.WriteAllBytes(filePath, bytes);
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesNotifier.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesNotifier.cs
new file mode 100644
index 0000000000..ada8ee7b17
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesNotifier.cs
@@ -0,0 +1,25 @@
+// 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 PowerToysExtension.Helpers;
+
+internal static class FancyZonesNotifier
+{
+ private const string AppliedLayoutsFileUpdateMessage = "{2ef2c8a7-e0d5-4f31-9ede-52aade2d284d}";
+ private static readonly uint WmPrivAppliedLayoutsFileUpdate = RegisterWindowMessageW(AppliedLayoutsFileUpdateMessage);
+
+ public static void NotifyAppliedLayoutsChanged()
+ {
+ _ = PostMessageW(new IntPtr(0xFFFF), WmPrivAppliedLayoutsFileUpdate, UIntPtr.Zero, IntPtr.Zero);
+ }
+
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern uint RegisterWindowMessageW(string lpString);
+
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern bool PostMessageW(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam);
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesThumbnailRenderer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesThumbnailRenderer.cs
new file mode 100644
index 0000000000..579b4e2d0e
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesThumbnailRenderer.cs
@@ -0,0 +1,716 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using FancyZonesEditorCommon.Data;
+using ManagedCommon;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using Windows.Graphics.Imaging;
+using Windows.Storage.Streams;
+
+using InteropConstants = PowerToys.Interop.Constants;
+
+namespace PowerToysExtension.Helpers;
+
+internal static class FancyZonesThumbnailRenderer
+{
+ internal readonly record struct NormalizedRect(float X, float Y, float Width, float Height);
+
+ private readonly record struct BgraColor(byte B, byte G, byte R, byte A);
+
+ public static async Task RenderLayoutIconAsync(FancyZonesLayoutDescriptor layout, int sizePx = 72)
+ {
+ try
+ {
+ Logger.LogDebug($"FancyZones thumbnail render starting. LayoutId={layout.Id} Type={layout.ApplyLayout.Type} ZoneCount={layout.ApplyLayout.ZoneCount} Source={layout.Source}");
+ if (sizePx < 16)
+ {
+ sizePx = 16;
+ }
+
+ var cachedIcon = TryGetCachedIcon(layout);
+ if (cachedIcon is not null)
+ {
+ Logger.LogDebug($"FancyZones thumbnail cache hit. LayoutId={layout.Id}");
+ return cachedIcon;
+ }
+
+ var rects = GetNormalizedRectsForLayout(layout);
+ Logger.LogDebug($"FancyZones thumbnail rects computed. LayoutId={layout.Id} RectCount={rects.Count}");
+ var pixelBytes = RenderBgra(rects, sizePx, layout.ApplyLayout.ShowSpacing && layout.ApplyLayout.Spacing > 0 ? layout.ApplyLayout.Spacing : 0);
+ var stream = new InMemoryRandomAccessStream();
+
+ var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
+ encoder.SetPixelData(
+ BitmapPixelFormat.Bgra8,
+ BitmapAlphaMode.Premultiplied,
+ (uint)sizePx,
+ (uint)sizePx,
+ 96,
+ 96,
+ pixelBytes);
+
+ await encoder.FlushAsync();
+ stream.Seek(0);
+
+ var cachePath = GetCachePath(layout);
+ if (!string.IsNullOrEmpty(cachePath))
+ {
+ try
+ {
+ var tempPath = FormattableString.Invariant($"{cachePath}.{Guid.NewGuid():N}.tmp");
+ Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
+ await WriteStreamToFileAsync(stream, tempPath);
+ File.Move(tempPath, cachePath, overwrite: true);
+
+ var fileIcon = new IconInfo(cachePath);
+ Logger.LogDebug($"FancyZones thumbnail render succeeded (file cache). LayoutId={layout.Id} Path=\"{cachePath}\"");
+ return fileIcon;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"FancyZones thumbnail write cache failed. LayoutId={layout.Id} Path=\"{cachePath}\" Exception={ex}");
+ }
+ }
+
+ // Fallback: return an in-memory stream icon. This may not marshal reliably cross-proc,
+ // so prefer the file-cached path above.
+ stream.Seek(0);
+ var inMemoryIcon = IconInfo.FromStream(stream);
+ Logger.LogDebug($"FancyZones thumbnail render succeeded (in-memory). LayoutId={layout.Id}");
+ return inMemoryIcon;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"FancyZones thumbnail render failed. LayoutId={layout.Id} Type={layout.ApplyLayout.Type} ZoneCount={layout.ApplyLayout.ZoneCount} Source={layout.Source} Exception={ex}");
+ return null;
+ }
+ }
+
+ private static IconInfo? TryGetCachedIcon(FancyZonesLayoutDescriptor layout)
+ {
+ var cachePath = GetCachePath(layout);
+ if (string.IsNullOrEmpty(cachePath))
+ {
+ return null;
+ }
+
+ try
+ {
+ if (File.Exists(cachePath))
+ {
+ return new IconInfo(cachePath);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"FancyZones thumbnail cache check failed. LayoutId={layout.Id} Path=\"{cachePath}\" Exception={ex}");
+ }
+
+ return null;
+ }
+
+ ///
+ /// Removes cached thumbnail files that no longer correspond to any current layout.
+ /// Call this on startup or periodically to prevent unbounded cache growth.
+ ///
+ public static void PurgeOrphanedCache()
+ {
+ try
+ {
+ var cacheFolder = GetCacheFolder();
+ if (string.IsNullOrEmpty(cacheFolder) || !Directory.Exists(cacheFolder))
+ {
+ return;
+ }
+
+ // Get all current layouts and compute their expected cache file names
+ var layouts = FancyZonesDataService.GetLayouts();
+ var validHashes = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var layout in layouts)
+ {
+ validHashes.Add(ComputeLayoutHash(layout) + ".png");
+ }
+
+ // Delete any .png files not in the valid set
+ var deletedCount = 0;
+ foreach (var filePath in Directory.EnumerateFiles(cacheFolder, "*.png"))
+ {
+ var fileName = Path.GetFileName(filePath);
+ if (!validHashes.Contains(fileName))
+ {
+ try
+ {
+ File.Delete(filePath);
+ deletedCount++;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"FancyZones thumbnail cache purge: failed to delete \"{filePath}\". Exception={ex.Message}");
+ }
+ }
+ }
+
+ if (deletedCount > 0)
+ {
+ Logger.LogInfo($"FancyZones thumbnail cache purge: deleted {deletedCount} orphaned file(s).");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"FancyZones thumbnail cache purge failed. Exception={ex}");
+ }
+ }
+
+ private static string? GetCacheFolder()
+ {
+ var basePath = InteropConstants.AppDataPath();
+ if (string.IsNullOrWhiteSpace(basePath))
+ {
+ return null;
+ }
+
+ return Path.Combine(basePath, "CmdPal", "PowerToysExtension", "Cache", "FancyZones", "LayoutThumbnails");
+ }
+
+ private static string? GetCachePath(FancyZonesLayoutDescriptor layout)
+ {
+ try
+ {
+ var cacheFolder = GetCacheFolder();
+ if (string.IsNullOrEmpty(cacheFolder))
+ {
+ return null;
+ }
+
+ var fileName = ComputeLayoutHash(layout) + ".png";
+ return Path.Combine(cacheFolder, fileName);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"FancyZones thumbnail cache path failed. LayoutId={layout.Id} Exception={ex}");
+ return null;
+ }
+ }
+
+ private static string ComputeLayoutHash(FancyZonesLayoutDescriptor layout)
+ {
+ var customType = layout.Custom?.Type?.Trim() ?? string.Empty;
+ var customInfo = layout.Custom is not null && layout.Custom.Value.Info.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null
+ ? layout.Custom.Value.Info.GetRawText()
+ : string.Empty;
+
+ var fingerprint = FormattableString.Invariant(
+ $"{layout.Id}|{layout.Source}|{layout.ApplyLayout.Type}|{layout.ApplyLayout.ZoneCount}|{layout.ApplyLayout.ShowSpacing}|{layout.ApplyLayout.Spacing}|{customType}|{customInfo}");
+
+ var bytes = Encoding.UTF8.GetBytes(fingerprint);
+ var hash = SHA256.HashData(bytes);
+ return Convert.ToHexString(hash).ToLowerInvariant();
+ }
+
+ private static async Task WriteStreamToFileAsync(IRandomAccessStream stream, string filePath)
+ {
+ stream.Seek(0);
+ var size = stream.Size;
+ if (size == 0)
+ {
+ File.WriteAllBytes(filePath, Array.Empty());
+ return;
+ }
+
+ if (size > int.MaxValue)
+ {
+ throw new InvalidOperationException("Icon stream too large.");
+ }
+
+ using var input = stream.GetInputStreamAt(0);
+ using var reader = new DataReader(input);
+ await reader.LoadAsync((uint)size);
+ var bytes = new byte[(int)size];
+ reader.ReadBytes(bytes);
+ File.WriteAllBytes(filePath, bytes);
+ }
+
+ internal static List GetNormalizedRectsForLayout(FancyZonesLayoutDescriptor layout)
+ {
+ var type = layout.ApplyLayout.Type.ToLowerInvariant();
+ if (layout.Source == FancyZonesLayoutSource.Custom && layout.Custom is not null)
+ {
+ return GetCustomRects(layout.Custom.Value);
+ }
+
+ return type switch
+ {
+ "columns" => GetColumnsRects(layout.ApplyLayout.ZoneCount),
+ "rows" => GetRowsRects(layout.ApplyLayout.ZoneCount),
+ "grid" => GetGridRects(layout.ApplyLayout.ZoneCount),
+ "priority-grid" => GetPriorityGridRects(layout.ApplyLayout.ZoneCount),
+ "focus" => GetFocusRects(layout.ApplyLayout.ZoneCount),
+ "blank" => new List(),
+ _ => GetGridRects(layout.ApplyLayout.ZoneCount),
+ };
+ }
+
+ private static List GetCustomRects(CustomLayouts.CustomLayoutWrapper custom)
+ {
+ var type = custom.Type?.Trim().ToLowerInvariant() ?? string.Empty;
+ if (custom.Info.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
+ {
+ return new List();
+ }
+
+ return type switch
+ {
+ "grid" => GetCustomGridRects(custom.Info),
+ "canvas" => GetCustomCanvasRects(custom.Info),
+ _ => new List(),
+ };
+ }
+
+ private static List GetCustomCanvasRects(JsonElement info)
+ {
+ if (!info.TryGetProperty("ref-width", out var refWidthProp) ||
+ !info.TryGetProperty("ref-height", out var refHeightProp) ||
+ !info.TryGetProperty("zones", out var zonesProp))
+ {
+ return new List();
+ }
+
+ if (refWidthProp.ValueKind != JsonValueKind.Number || refHeightProp.ValueKind != JsonValueKind.Number || zonesProp.ValueKind != JsonValueKind.Array)
+ {
+ return new List();
+ }
+
+ var refWidth = Math.Max(1, refWidthProp.GetInt32());
+ var refHeight = Math.Max(1, refHeightProp.GetInt32());
+ var rects = new List(zonesProp.GetArrayLength());
+
+ foreach (var zone in zonesProp.EnumerateArray())
+ {
+ if (zone.ValueKind != JsonValueKind.Object)
+ {
+ continue;
+ }
+
+ if (!zone.TryGetProperty("X", out var xProp) ||
+ !zone.TryGetProperty("Y", out var yProp) ||
+ !zone.TryGetProperty("width", out var wProp) ||
+ !zone.TryGetProperty("height", out var hProp))
+ {
+ continue;
+ }
+
+ if (xProp.ValueKind != JsonValueKind.Number ||
+ yProp.ValueKind != JsonValueKind.Number ||
+ wProp.ValueKind != JsonValueKind.Number ||
+ hProp.ValueKind != JsonValueKind.Number)
+ {
+ continue;
+ }
+
+ var x = xProp.GetSingle() / refWidth;
+ var y = yProp.GetSingle() / refHeight;
+ var w = wProp.GetSingle() / refWidth;
+ var h = hProp.GetSingle() / refHeight;
+ rects.Add(NormalizeRect(x, y, w, h));
+ }
+
+ return rects;
+ }
+
+ private static List GetCustomGridRects(JsonElement info)
+ {
+ if (!TryGetGridDefinition(info, out var rows, out var cols, out var rowsPercents, out var colsPercents, out var cellMap))
+ {
+ return new List();
+ }
+
+ return BuildRectsFromGridDefinition(rows, cols, rowsPercents, colsPercents, cellMap);
+ }
+
+ private static bool TryGetGridDefinition(
+ JsonElement info,
+ out int rows,
+ out int cols,
+ out int[] rowPercents,
+ out int[] colPercents,
+ out int[][] cellChildMap)
+ {
+ rows = 0;
+ cols = 0;
+ rowPercents = Array.Empty();
+ colPercents = Array.Empty();
+ cellChildMap = Array.Empty();
+
+ if (!info.TryGetProperty("rows", out var rowsProp) ||
+ !info.TryGetProperty("columns", out var colsProp) ||
+ !info.TryGetProperty("rows-percentage", out var rowsPercentsProp) ||
+ !info.TryGetProperty("columns-percentage", out var colsPercentsProp) ||
+ !info.TryGetProperty("cell-child-map", out var cellMapProp))
+ {
+ return false;
+ }
+
+ if (rowsProp.ValueKind != JsonValueKind.Number ||
+ colsProp.ValueKind != JsonValueKind.Number ||
+ rowsPercentsProp.ValueKind != JsonValueKind.Array ||
+ colsPercentsProp.ValueKind != JsonValueKind.Array ||
+ cellMapProp.ValueKind != JsonValueKind.Array)
+ {
+ return false;
+ }
+
+ rows = rowsProp.GetInt32();
+ cols = colsProp.GetInt32();
+ if (rows <= 0 || cols <= 0)
+ {
+ return false;
+ }
+
+ rowPercents = rowsPercentsProp.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.Number).Select(v => v.GetInt32()).ToArray();
+ colPercents = colsPercentsProp.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.Number).Select(v => v.GetInt32()).ToArray();
+
+ if (rowPercents.Length != rows || colPercents.Length != cols)
+ {
+ return false;
+ }
+
+ var mapRows = new List(rows);
+ foreach (var row in cellMapProp.EnumerateArray())
+ {
+ if (row.ValueKind != JsonValueKind.Array)
+ {
+ return false;
+ }
+
+ var cells = row.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.Number).Select(v => v.GetInt32()).ToArray();
+ if (cells.Length != cols)
+ {
+ return false;
+ }
+
+ mapRows.Add(cells);
+ }
+
+ if (mapRows.Count != rows)
+ {
+ return false;
+ }
+
+ cellChildMap = mapRows.ToArray();
+ return true;
+ }
+
+ private static List GetColumnsRects(int zoneCount)
+ {
+ zoneCount = Math.Clamp(zoneCount, 1, 16);
+ var rects = new List(zoneCount);
+ for (var i = 0; i < zoneCount; i++)
+ {
+ rects.Add(new NormalizedRect(i / (float)zoneCount, 0, 1f / zoneCount, 1f));
+ }
+
+ return rects;
+ }
+
+ private static List GetRowsRects(int zoneCount)
+ {
+ zoneCount = Math.Clamp(zoneCount, 1, 16);
+ var rects = new List(zoneCount);
+ for (var i = 0; i < zoneCount; i++)
+ {
+ rects.Add(new NormalizedRect(0, i / (float)zoneCount, 1f, 1f / zoneCount));
+ }
+
+ return rects;
+ }
+
+ private static List GetGridRects(int zoneCount)
+ {
+ zoneCount = Math.Clamp(zoneCount, 1, 25);
+ var rows = 1;
+ while (zoneCount / rows >= rows)
+ {
+ rows++;
+ }
+
+ rows--;
+ var cols = zoneCount / rows;
+ if (zoneCount % rows != 0)
+ {
+ cols++;
+ }
+
+ var rowPercents = Enumerable.Repeat(10000 / rows, rows).ToArray();
+ var colPercents = Enumerable.Repeat(10000 / cols, cols).ToArray();
+ var cellMap = new int[rows][];
+
+ var index = 0;
+ for (var r = 0; r < rows; r++)
+ {
+ cellMap[r] = new int[cols];
+ for (var c = 0; c < cols; c++)
+ {
+ cellMap[r][c] = index;
+ index++;
+ if (index == zoneCount)
+ {
+ index--;
+ }
+ }
+ }
+
+ return BuildRectsFromGridDefinition(rows, cols, rowPercents, colPercents, cellMap);
+ }
+
+ private static List GetPriorityGridRects(int zoneCount)
+ {
+ zoneCount = Math.Clamp(zoneCount, 1, 25);
+
+ if (zoneCount is >= 1 and <= 11 && PriorityGrid.TryGetValue(zoneCount, out var def))
+ {
+ return BuildRectsFromGridDefinition(def.Rows, def.Cols, def.RowPercents, def.ColPercents, def.CellMap);
+ }
+
+ return GetGridRects(zoneCount);
+ }
+
+ private static List GetFocusRects(int zoneCount)
+ {
+ zoneCount = Math.Clamp(zoneCount, 1, 8);
+ var rects = new List(zoneCount);
+ for (var i = 0; i < zoneCount; i++)
+ {
+ var offset = i * 0.06f;
+ rects.Add(new NormalizedRect(0.1f + offset, 0.1f + offset, 0.8f, 0.8f));
+ }
+
+ return rects;
+ }
+
+ private static List BuildRectsFromGridDefinition(int rows, int cols, int[] rowPercents, int[] colPercents, int[][] cellChildMap)
+ {
+ const float multiplier = 10000f;
+
+ var rowPrefix = new float[rows + 1];
+ var colPrefix = new float[cols + 1];
+
+ for (var r = 0; r < rows; r++)
+ {
+ rowPrefix[r + 1] = rowPrefix[r] + (rowPercents[r] / multiplier);
+ }
+
+ for (var c = 0; c < cols; c++)
+ {
+ colPrefix[c + 1] = colPrefix[c] + (colPercents[c] / multiplier);
+ }
+
+ var maxZone = -1;
+ for (var r = 0; r < rows; r++)
+ {
+ for (var c = 0; c < cols; c++)
+ {
+ maxZone = Math.Max(maxZone, cellChildMap[r][c]);
+ }
+ }
+
+ var rects = new List(maxZone + 1);
+ for (var i = 0; i <= maxZone; i++)
+ {
+ rects.Add(new NormalizedRect(1, 1, 0, 0));
+ }
+
+ for (var r = 0; r < rows; r++)
+ {
+ for (var c = 0; c < cols; c++)
+ {
+ var zoneId = cellChildMap[r][c];
+ if (zoneId < 0 || zoneId >= rects.Count)
+ {
+ continue;
+ }
+
+ var x1 = colPrefix[c];
+ var y1 = rowPrefix[r];
+ var x2 = colPrefix[c + 1];
+ var y2 = rowPrefix[r + 1];
+
+ var existing = rects[zoneId];
+ if (existing.Width <= 0 || existing.Height <= 0)
+ {
+ rects[zoneId] = new NormalizedRect(x1, y1, x2 - x1, y2 - y1);
+ }
+ else
+ {
+ var ex2 = existing.X + existing.Width;
+ var ey2 = existing.Y + existing.Height;
+ var nx1 = Math.Min(existing.X, x1);
+ var ny1 = Math.Min(existing.Y, y1);
+ var nx2 = Math.Max(ex2, x2);
+ var ny2 = Math.Max(ey2, y2);
+ rects[zoneId] = new NormalizedRect(nx1, ny1, nx2 - nx1, ny2 - ny1);
+ }
+ }
+ }
+
+ return rects
+ .Where(r => r.Width > 0 && r.Height > 0)
+ .Select(r => NormalizeRect(r.X, r.Y, r.Width, r.Height))
+ .ToList();
+ }
+
+ private static NormalizedRect NormalizeRect(float x, float y, float w, float h)
+ {
+ x = Math.Clamp(x, 0, 1);
+ y = Math.Clamp(y, 0, 1);
+ w = Math.Clamp(w, 0, 1 - x);
+ h = Math.Clamp(h, 0, 1 - y);
+ return new NormalizedRect(x, y, w, h);
+ }
+
+ private static byte[] RenderBgra(IReadOnlyList rects, int sizePx, int spacing)
+ {
+ var pixels = new byte[sizePx * sizePx * 4];
+
+ var border = Premultiply(new BgraColor(0x30, 0x30, 0x30, 0xFF));
+ var frame = Premultiply(new BgraColor(0x40, 0x40, 0x40, 0xA0));
+ var fill = Premultiply(new BgraColor(0xFF, 0xD8, 0x8C, 0xC0)); // light-ish blue with alpha
+ var background = Premultiply(new BgraColor(0x00, 0x00, 0x00, 0x00));
+
+ for (var i = 0; i < pixels.Length; i += 4)
+ {
+ pixels[i + 0] = background.B;
+ pixels[i + 1] = background.G;
+ pixels[i + 2] = background.R;
+ pixels[i + 3] = background.A;
+ }
+
+ DrawRectBorder(pixels, sizePx, 1, 1, sizePx - 1, sizePx - 1, frame);
+
+ var gapPx = spacing > 0 ? Math.Clamp(spacing / 8, 1, 3) : 0;
+ foreach (var rect in rects)
+ {
+ var (x1, y1, x2, y2) = ToPixelBounds(rect, sizePx, gapPx);
+ if (x2 <= x1 || y2 <= y1)
+ {
+ continue;
+ }
+
+ FillRect(pixels, sizePx, x1, y1, x2, y2, fill);
+ DrawRectBorder(pixels, sizePx, x1, y1, x2, y2, border);
+ }
+
+ return pixels;
+ }
+
+ private static (int X1, int Y1, int X2, int Y2) ToPixelBounds(NormalizedRect rect, int sizePx, int gapPx)
+ {
+ var x1 = (int)MathF.Round(rect.X * sizePx);
+ var y1 = (int)MathF.Round(rect.Y * sizePx);
+ var x2 = (int)MathF.Round((rect.X + rect.Width) * sizePx);
+ var y2 = (int)MathF.Round((rect.Y + rect.Height) * sizePx);
+
+ x1 = Math.Clamp(x1 + gapPx, 0, sizePx - 1);
+ y1 = Math.Clamp(y1 + gapPx, 0, sizePx - 1);
+ x2 = Math.Clamp(x2 - gapPx, 1, sizePx);
+ y2 = Math.Clamp(y2 - gapPx, 1, sizePx);
+
+ if (x2 <= x1 + 1)
+ {
+ x2 = Math.Min(sizePx, x1 + 2);
+ }
+
+ if (y2 <= y1 + 1)
+ {
+ y2 = Math.Min(sizePx, y1 + 2);
+ }
+
+ return (x1, y1, x2, y2);
+ }
+
+ private static void FillRect(byte[] pixels, int sizePx, int x1, int y1, int x2, int y2, BgraColor color)
+ {
+ for (var y = y1; y < y2; y++)
+ {
+ var rowStart = y * sizePx * 4;
+ for (var x = x1; x < x2; x++)
+ {
+ var i = rowStart + (x * 4);
+ pixels[i + 0] = color.B;
+ pixels[i + 1] = color.G;
+ pixels[i + 2] = color.R;
+ pixels[i + 3] = color.A;
+ }
+ }
+ }
+
+ private static void DrawRectBorder(byte[] pixels, int sizePx, int x1, int y1, int x2, int y2, BgraColor color)
+ {
+ var left = x1;
+ var right = x2 - 1;
+ var top = y1;
+ var bottom = y2 - 1;
+
+ for (var x = left; x <= right; x++)
+ {
+ SetPixel(pixels, sizePx, x, top, color);
+ SetPixel(pixels, sizePx, x, bottom, color);
+ }
+
+ for (var y = top; y <= bottom; y++)
+ {
+ SetPixel(pixels, sizePx, left, y, color);
+ SetPixel(pixels, sizePx, right, y, color);
+ }
+ }
+
+ private static void SetPixel(byte[] pixels, int sizePx, int x, int y, BgraColor color)
+ {
+ if ((uint)x >= (uint)sizePx || (uint)y >= (uint)sizePx)
+ {
+ return;
+ }
+
+ var i = ((y * sizePx) + x) * 4;
+ pixels[i + 0] = color.B;
+ pixels[i + 1] = color.G;
+ pixels[i + 2] = color.R;
+ pixels[i + 3] = color.A;
+ }
+
+ private static BgraColor Premultiply(BgraColor color)
+ {
+ if (color.A == 0 || color.A == 255)
+ {
+ return color;
+ }
+
+ byte Premul(byte c) => (byte)(((c * color.A) + 127) / 255);
+ return new BgraColor(Premul(color.B), Premul(color.G), Premul(color.R), color.A);
+ }
+
+ private sealed record PriorityGridDefinition(int Rows, int Cols, int[] RowPercents, int[] ColPercents, int[][] CellMap);
+
+ private static readonly IReadOnlyDictionary PriorityGrid = new Dictionary
+ {
+ [1] = new PriorityGridDefinition(1, 1, [10000], [10000], [[0]]),
+ [2] = new PriorityGridDefinition(1, 2, [10000], [6667, 3333], [[0, 1]]),
+ [3] = new PriorityGridDefinition(1, 3, [10000], [2500, 5000, 2500], [[0, 1, 2]]),
+ [4] = new PriorityGridDefinition(2, 3, [5000, 5000], [2500, 5000, 2500], [[0, 1, 2], [0, 1, 3]]),
+ [5] = new PriorityGridDefinition(2, 3, [5000, 5000], [2500, 5000, 2500], [[0, 1, 2], [3, 1, 4]]),
+ [6] = new PriorityGridDefinition(3, 3, [3333, 3334, 3333], [2500, 5000, 2500], [[0, 1, 2], [0, 1, 3], [4, 1, 5]]),
+ [7] = new PriorityGridDefinition(3, 3, [3333, 3334, 3333], [2500, 5000, 2500], [[0, 1, 2], [3, 1, 4], [5, 1, 6]]),
+ [8] = new PriorityGridDefinition(3, 4, [3333, 3334, 3333], [2500, 2500, 2500, 2500], [[0, 1, 2, 3], [4, 1, 2, 5], [6, 1, 2, 7]]),
+ [9] = new PriorityGridDefinition(3, 4, [3333, 3334, 3333], [2500, 2500, 2500, 2500], [[0, 1, 2, 3], [4, 1, 2, 5], [6, 1, 7, 8]]),
+ [10] = new PriorityGridDefinition(3, 4, [3333, 3334, 3333], [2500, 2500, 2500, 2500], [[0, 1, 2, 3], [4, 1, 5, 6], [7, 1, 8, 9]]),
+ [11] = new PriorityGridDefinition(3, 4, [3333, 3334, 3333], [2500, 2500, 2500, 2500], [[0, 1, 2, 3], [4, 1, 5, 6], [7, 8, 9, 10]]),
+ };
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesVirtualDesktop.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesVirtualDesktop.cs
new file mode 100644
index 0000000000..274b6ef5c7
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesVirtualDesktop.cs
@@ -0,0 +1,103 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+using Microsoft.Win32;
+
+namespace PowerToysExtension.Helpers;
+
+internal static class FancyZonesVirtualDesktop
+{
+ private const string VirtualDesktopsKey = @"Software\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops";
+ private const string SessionVirtualDesktopsKeyPrefix = @"Software\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\";
+ private const string SessionVirtualDesktopsKeySuffix = @"\VirtualDesktops";
+ private const string CurrentVirtualDesktopValue = "CurrentVirtualDesktop";
+ private const string VirtualDesktopIdsValue = "VirtualDesktopIDs";
+
+ public static string GetCurrentVirtualDesktopIdString()
+ {
+ var id = TryGetCurrentVirtualDesktopId()
+ ?? TryGetCurrentVirtualDesktopIdFromSession()
+ ?? TryGetFirstVirtualDesktopId()
+ ?? Guid.Empty;
+
+ return "{" + id.ToString().ToUpperInvariant() + "}";
+ }
+
+ private static Guid? TryGetCurrentVirtualDesktopId()
+ {
+ try
+ {
+ using var key = Registry.CurrentUser.OpenSubKey(VirtualDesktopsKey, writable: false);
+ var bytes = key?.GetValue(CurrentVirtualDesktopValue) as byte[];
+ return TryGetGuid(bytes);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static Guid? TryGetCurrentVirtualDesktopIdFromSession()
+ {
+ try
+ {
+ if (!ProcessIdToSessionId((uint)Environment.ProcessId, out var sessionId))
+ {
+ return null;
+ }
+
+ var path = SessionVirtualDesktopsKeyPrefix + sessionId + SessionVirtualDesktopsKeySuffix;
+ using var key = Registry.CurrentUser.OpenSubKey(path, writable: false);
+ var bytes = key?.GetValue(CurrentVirtualDesktopValue) as byte[];
+ return TryGetGuid(bytes);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static Guid? TryGetFirstVirtualDesktopId()
+ {
+ try
+ {
+ using var key = Registry.CurrentUser.OpenSubKey(VirtualDesktopsKey, writable: false);
+ var bytes = key?.GetValue(VirtualDesktopIdsValue) as byte[];
+ if (bytes is null || bytes.Length < 16)
+ {
+ return null;
+ }
+
+ var first = new byte[16];
+ Array.Copy(bytes, 0, first, 0, 16);
+ return TryGetGuid(first);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static Guid? TryGetGuid(byte[]? bytes)
+ {
+ try
+ {
+ if (bytes is null || bytes.Length < 16)
+ {
+ return null;
+ }
+
+ return new Guid(bytes.AsSpan(0, 16));
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool ProcessIdToSessionId(uint dwProcessId, out uint pSessionId);
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/GpoEnablementService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/GpoEnablementService.cs
new file mode 100644
index 0000000000..244870ebe5
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/GpoEnablementService.cs
@@ -0,0 +1,86 @@
+// 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 Microsoft.Win32;
+
+namespace PowerToysExtension.Helpers;
+
+internal enum GpoRuleConfiguredValue
+{
+ WrongValue = -3,
+ Unavailable = -2,
+ NotConfigured = -1,
+ Disabled = 0,
+ Enabled = 1,
+}
+
+///
+/// Lightweight GPO reader for module/feature enablement policies.
+/// Mirrors the logic in src/common/utils/gpo.h but avoids taking a dependency on the full GPOWrapper.
+///
+internal static class GpoEnablementService
+{
+ private const string PoliciesPath = @"SOFTWARE\Policies\PowerToys";
+ private const string PolicyConfigureEnabledGlobalAllUtilities = "ConfigureGlobalUtilityEnabledState";
+
+ internal static GpoRuleConfiguredValue GetUtilityEnabledValue(string individualPolicyValueName)
+ {
+ if (!string.IsNullOrEmpty(individualPolicyValueName))
+ {
+ var individual = GetConfiguredValue(individualPolicyValueName);
+ if (individual is GpoRuleConfiguredValue.Disabled or GpoRuleConfiguredValue.Enabled)
+ {
+ return individual;
+ }
+ }
+
+ return GetConfiguredValue(PolicyConfigureEnabledGlobalAllUtilities);
+ }
+
+ private static GpoRuleConfiguredValue GetConfiguredValue(string registryValueName)
+ {
+ try
+ {
+ // Machine scope has priority over user scope.
+ var value = ReadRegistryValue(Registry.LocalMachine, registryValueName);
+ value ??= ReadRegistryValue(Registry.CurrentUser, registryValueName);
+
+ if (!value.HasValue)
+ {
+ return GpoRuleConfiguredValue.NotConfigured;
+ }
+
+ return value.Value switch
+ {
+ 0 => GpoRuleConfiguredValue.Disabled,
+ 1 => GpoRuleConfiguredValue.Enabled,
+ _ => GpoRuleConfiguredValue.WrongValue,
+ };
+ }
+ catch
+ {
+ return GpoRuleConfiguredValue.Unavailable;
+ }
+ }
+
+ private static int? ReadRegistryValue(RegistryKey rootKey, string valueName)
+ {
+ try
+ {
+ using var key = rootKey.OpenSubKey(PoliciesPath, writable: false);
+ if (key is null)
+ {
+ return null;
+ }
+
+ var value = key.GetValue(valueName);
+ return value as int?;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleCommandCatalog.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleCommandCatalog.cs
new file mode 100644
index 0000000000..679c94bde0
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleCommandCatalog.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Modules;
+
+namespace PowerToysExtension.Helpers;
+
+///
+/// Aggregates commands exposed by individual module providers and applies fuzzy filtering.
+///
+internal static class ModuleCommandCatalog
+{
+ private static readonly ModuleCommandProvider[] Providers =
+ [
+ new AwakeModuleCommandProvider(),
+ new AdvancedPasteModuleCommandProvider(),
+ new WorkspacesModuleCommandProvider(),
+ new LightSwitchModuleCommandProvider(),
+ new PowerToysRunModuleCommandProvider(),
+ new ScreenRulerModuleCommandProvider(),
+ new ShortcutGuideModuleCommandProvider(),
+ new TextExtractorModuleCommandProvider(),
+ new ZoomItModuleCommandProvider(),
+ new ColorPickerModuleCommandProvider(),
+ new AlwaysOnTopModuleCommandProvider(),
+ new CropAndLockModuleCommandProvider(),
+ new FancyZonesModuleCommandProvider(),
+ new KeyboardManagerModuleCommandProvider(),
+ new MouseUtilsModuleCommandProvider(),
+ new MouseWithoutBordersModuleCommandProvider(),
+ new QuickAccentModuleCommandProvider(),
+ new FileExplorerAddonsModuleCommandProvider(),
+ new FileLocksmithModuleCommandProvider(),
+ new ImageResizerModuleCommandProvider(),
+ new NewPlusModuleCommandProvider(),
+ new PeekModuleCommandProvider(),
+ new PowerRenameModuleCommandProvider(),
+ new CommandNotFoundModuleCommandProvider(),
+ new EnvironmentVariablesModuleCommandProvider(),
+ new HostsModuleCommandProvider(),
+ new RegistryPreviewModuleCommandProvider(),
+ ];
+
+ public static IListItem[] GetAllItems()
+ {
+ return [.. Providers.SelectMany(provider => provider.BuildCommands())];
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleEnablementService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleEnablementService.cs
new file mode 100644
index 0000000000..fccf5b8687
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleEnablementService.cs
@@ -0,0 +1,162 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Helpers;
+
+///
+/// Reads PowerToys module enablement flags from the global settings.json.
+///
+internal static class ModuleEnablementService
+{
+ internal static string SettingsFilePath { get; } = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "Microsoft",
+ "PowerToys",
+ "settings.json");
+
+ internal static bool IsModuleEnabled(SettingsWindow module)
+ {
+ var key = GetEnabledKey(module);
+ if (string.IsNullOrEmpty(key))
+ {
+ var globalRule = GpoEnablementService.GetUtilityEnabledValue(string.Empty);
+ return globalRule != GpoRuleConfiguredValue.Disabled;
+ }
+
+ return IsKeyEnabled(key);
+ }
+
+ internal static bool IsKeyEnabled(string enabledKey)
+ {
+ if (string.IsNullOrWhiteSpace(enabledKey))
+ {
+ return true;
+ }
+
+ var gpoPolicy = GetGpoPolicyForEnabledKey(enabledKey);
+ var gpoRule = GpoEnablementService.GetUtilityEnabledValue(gpoPolicy);
+ if (gpoRule == GpoRuleConfiguredValue.Disabled)
+ {
+ return false;
+ }
+
+ if (gpoRule == GpoRuleConfiguredValue.Enabled)
+ {
+ return true;
+ }
+
+ try
+ {
+ var enabled = ReadEnabledFlags();
+ return enabled is null || !enabled.TryGetValue(enabledKey, out var value) || value;
+ }
+ catch
+ {
+ return true;
+ }
+ }
+
+ private static Dictionary? ReadEnabledFlags()
+ {
+ if (!File.Exists(SettingsFilePath))
+ {
+ return null;
+ }
+
+ var json = File.ReadAllText(SettingsFilePath).Trim('\0');
+ using var doc = JsonDocument.Parse(json);
+
+ if (!doc.RootElement.TryGetProperty("enabled", out var enabledRoot) ||
+ enabledRoot.ValueKind != JsonValueKind.Object)
+ {
+ return null;
+ }
+
+ var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var prop in enabledRoot.EnumerateObject())
+ {
+ if (prop.Value.ValueKind is JsonValueKind.True or JsonValueKind.False)
+ {
+ result[prop.Name] = prop.Value.GetBoolean();
+ }
+ }
+
+ return result;
+ }
+
+ private static string GetEnabledKey(SettingsWindow module) => module switch
+ {
+ SettingsWindow.Awake => "Awake",
+ SettingsWindow.AdvancedPaste => "AdvancedPaste",
+ SettingsWindow.AlwaysOnTop => "AlwaysOnTop",
+ SettingsWindow.ColorPicker => "ColorPicker",
+ SettingsWindow.CropAndLock => "CropAndLock",
+ SettingsWindow.EnvironmentVariables => "EnvironmentVariables",
+ SettingsWindow.FancyZones => "FancyZones",
+ SettingsWindow.FileExplorer => "File Explorer Preview",
+ SettingsWindow.FileLocksmith => "FileLocksmith",
+ SettingsWindow.Hosts => "Hosts",
+ SettingsWindow.ImageResizer => "Image Resizer",
+ SettingsWindow.KBM => "Keyboard Manager",
+ SettingsWindow.LightSwitch => "LightSwitch",
+ SettingsWindow.MeasureTool => "Measure Tool",
+ SettingsWindow.MouseWithoutBorders => "MouseWithoutBorders",
+ SettingsWindow.NewPlus => "NewPlus",
+ SettingsWindow.Peek => "Peek",
+ SettingsWindow.PowerAccent => "QuickAccent",
+ SettingsWindow.PowerLauncher => "PowerToys Run",
+ SettingsWindow.Run => "PowerToys Run",
+ SettingsWindow.PowerRename => "PowerRename",
+ SettingsWindow.PowerOCR => "TextExtractor",
+ SettingsWindow.RegistryPreview => "RegistryPreview",
+ SettingsWindow.ShortcutGuide => "Shortcut Guide",
+ SettingsWindow.Workspaces => "Workspaces",
+ SettingsWindow.ZoomIt => "ZoomIt",
+ SettingsWindow.CmdNotFound => "CmdNotFound",
+ SettingsWindow.CmdPal => "CmdPal",
+ _ => string.Empty,
+ };
+
+ private static string GetGpoPolicyForEnabledKey(string enabledKey) => enabledKey switch
+ {
+ "AdvancedPaste" => "ConfigureEnabledUtilityAdvancedPaste",
+ "AlwaysOnTop" => "ConfigureEnabledUtilityAlwaysOnTop",
+ "Awake" => "ConfigureEnabledUtilityAwake",
+ "CmdNotFound" => "ConfigureEnabledUtilityCmdNotFound",
+ "CmdPal" => "ConfigureEnabledUtilityCmdPal",
+ "ColorPicker" => "ConfigureEnabledUtilityColorPicker",
+ "CropAndLock" => "ConfigureEnabledUtilityCropAndLock",
+ "CursorWrap" => "ConfigureEnabledUtilityCursorWrap",
+ "EnvironmentVariables" => "ConfigureEnabledUtilityEnvironmentVariables",
+ "FancyZones" => "ConfigureEnabledUtilityFancyZones",
+ "FileLocksmith" => "ConfigureEnabledUtilityFileLocksmith",
+ "FindMyMouse" => "ConfigureEnabledUtilityFindMyMouse",
+ "Hosts" => "ConfigureEnabledUtilityHostsFileEditor",
+ "Image Resizer" => "ConfigureEnabledUtilityImageResizer",
+ "Keyboard Manager" => "ConfigureEnabledUtilityKeyboardManager",
+ "LightSwitch" => "ConfigureEnabledUtilityLightSwitch",
+ "Measure Tool" => "ConfigureEnabledUtilityScreenRuler",
+ "MouseHighlighter" => "ConfigureEnabledUtilityMouseHighlighter",
+ "MouseJump" => "ConfigureEnabledUtilityMouseJump",
+ "MousePointerCrosshairs" => "ConfigureEnabledUtilityMousePointerCrosshairs",
+ "MouseWithoutBorders" => "ConfigureEnabledUtilityMouseWithoutBorders",
+ "NewPlus" => "ConfigureEnabledUtilityNewPlus",
+ "Peek" => "ConfigureEnabledUtilityPeek",
+ "PowerRename" => "ConfigureEnabledUtilityPowerRename",
+ "PowerToys Run" => "ConfigureEnabledUtilityPowerLauncher",
+ "QuickAccent" => "ConfigureEnabledUtilityQuickAccent",
+ "RegistryPreview" => "ConfigureEnabledUtilityRegistryPreview",
+ "Shortcut Guide" => "ConfigureEnabledUtilityShortcutGuide",
+ "TextExtractor" => "ConfigureEnabledUtilityTextExtractor",
+ "Workspaces" => "ConfigureEnabledUtilityWorkspaces",
+ "ZoomIt" => "ConfigureEnabledUtilityZoomIt",
+ _ => string.Empty,
+ };
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysFallbackCommandItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysFallbackCommandItem.cs
new file mode 100644
index 0000000000..7ce4d2b27b
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysFallbackCommandItem.cs
@@ -0,0 +1,88 @@
+// 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 Common.Search.FuzzSearch;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace PowerToysExtension.Helpers;
+
+///
+/// A fallback item that filters itself based on the landing-page query.
+/// It hides itself (empty Title) when the query doesn't fuzzy-match the title or subtitle.
+///
+internal sealed partial class PowerToysFallbackCommandItem : FallbackCommandItem, IFallbackHandler
+{
+ private readonly string _baseTitle;
+ private readonly string _baseSubtitle;
+ private readonly string _baseName;
+ private readonly Command? _mutableCommand;
+
+ public PowerToysFallbackCommandItem(ICommand command, string title, string subtitle, IIconInfo? icon, IContextItem[]? moreCommands)
+ : base(command, title)
+ {
+ _baseTitle = title ?? string.Empty;
+ _baseSubtitle = subtitle ?? string.Empty;
+ _baseName = command?.Name ?? string.Empty;
+ _mutableCommand = command as Command;
+
+ // Start hidden; we only surface when the query matches
+ Title = string.Empty;
+ Subtitle = string.Empty;
+ if (_mutableCommand is not null)
+ {
+ _mutableCommand.Name = string.Empty;
+ }
+
+ if (icon != null)
+ {
+ Icon = icon;
+ }
+
+ MoreCommands = moreCommands ?? Array.Empty();
+
+ // Ensure fallback updates route to this instance
+ FallbackHandler = this;
+ }
+
+ public override void UpdateQuery(string query)
+ {
+ if (string.IsNullOrWhiteSpace(query))
+ {
+ Title = string.Empty;
+ Subtitle = string.Empty;
+ if (_mutableCommand is not null)
+ {
+ _mutableCommand.Name = string.Empty;
+ }
+
+ return;
+ }
+
+ // Simple fuzzy match against title/subtitle; hide if neither match
+ var titleMatch = Common.Search.FuzzSearch.StringMatcher.FuzzyMatch(query, _baseTitle);
+ var subtitleMatch = Common.Search.FuzzSearch.StringMatcher.FuzzyMatch(query, _baseSubtitle);
+ var matches = (titleMatch.Success && titleMatch.Score > 0) || (subtitleMatch.Success && subtitleMatch.Score > 0);
+
+ if (matches)
+ {
+ Title = _baseTitle;
+ Subtitle = _baseSubtitle;
+ if (_mutableCommand is not null)
+ {
+ _mutableCommand.Name = _baseName;
+ }
+ }
+ else
+ {
+ Title = string.Empty;
+ Subtitle = string.Empty;
+ if (_mutableCommand is not null)
+ {
+ _mutableCommand.Name = string.Empty;
+ }
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysPathResolver.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysPathResolver.cs
new file mode 100644
index 0000000000..d9ac9443fe
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysPathResolver.cs
@@ -0,0 +1,161 @@
+// 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;
+using Microsoft.Win32;
+
+namespace PowerToysExtension.Helpers;
+
+///
+/// Helper methods for locating the installed PowerToys binaries.
+///
+internal static class PowerToysPathResolver
+{
+ private const string PowerToysProtocolKey = @"Software\Classes\powertoys";
+ private const string PowerToysUserKey = @"Software\Microsoft\PowerToys";
+
+ internal static string GetPowerToysInstallPath()
+ {
+ var perUser = GetInstallPathFromRegistry(RegistryHive.CurrentUser);
+ if (!string.IsNullOrEmpty(perUser))
+ {
+ return perUser;
+ }
+
+ return GetInstallPathFromRegistry(RegistryHive.LocalMachine);
+ }
+
+ internal static string TryResolveExecutable(string executableName)
+ {
+ if (string.IsNullOrEmpty(executableName))
+ {
+ return string.Empty;
+ }
+
+ var baseDirectory = GetPowerToysInstallPath();
+ if (string.IsNullOrEmpty(baseDirectory))
+ {
+ return string.Empty;
+ }
+
+ var candidate = Path.Combine(baseDirectory, executableName);
+ return File.Exists(candidate) ? candidate : string.Empty;
+ }
+
+ private static string GetInstallPathFromRegistry(RegistryHive hive)
+ {
+ try
+ {
+ using var baseKey = RegistryKey.OpenBaseKey(hive, RegistryView.Registry64);
+
+ var protocolPath = GetPathFromProtocolRegistration(baseKey);
+ if (!string.IsNullOrEmpty(protocolPath))
+ {
+ return protocolPath;
+ }
+
+ if (hive == RegistryHive.CurrentUser)
+ {
+ var userPath = GetPathFromUserRegistration(baseKey);
+ if (!string.IsNullOrEmpty(userPath))
+ {
+ return userPath;
+ }
+ }
+ }
+ catch
+ {
+ // Ignore registry access failures and fall back to other checks.
+ }
+
+ return string.Empty;
+ }
+
+ private static string GetPathFromProtocolRegistration(RegistryKey baseKey)
+ {
+ try
+ {
+ using var commandKey = baseKey.OpenSubKey($@"{PowerToysProtocolKey}\shell\open\command");
+ if (commandKey == null)
+ {
+ return string.Empty;
+ }
+
+ var command = commandKey.GetValue(string.Empty)?.ToString() ?? string.Empty;
+ if (string.IsNullOrEmpty(command))
+ {
+ return string.Empty;
+ }
+
+ return ExtractInstallDirectory(command);
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+
+ private static string GetPathFromUserRegistration(RegistryKey baseKey)
+ {
+ try
+ {
+ using var userKey = baseKey.OpenSubKey(PowerToysUserKey);
+ if (userKey == null)
+ {
+ return string.Empty;
+ }
+
+ var installedValue = userKey.GetValue("installed");
+ if (installedValue != null && installedValue.ToString() == "1")
+ {
+ return GetPathFromProtocolRegistration(baseKey);
+ }
+ }
+ catch
+ {
+ // Ignore registry access failures.
+ }
+
+ return string.Empty;
+ }
+
+ private static string ExtractInstallDirectory(string command)
+ {
+ if (string.IsNullOrEmpty(command))
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ if (command.StartsWith('"'))
+ {
+ var closingQuote = command.IndexOf('"', 1);
+ if (closingQuote > 1)
+ {
+ var quotedPath = command.Substring(1, closingQuote - 1);
+ if (File.Exists(quotedPath))
+ {
+ return Path.GetDirectoryName(quotedPath) ?? string.Empty;
+ }
+ }
+ }
+ else
+ {
+ var parts = command.Split(' ');
+ if (parts.Length > 0 && File.Exists(parts[0]))
+ {
+ return Path.GetDirectoryName(parts[0]) ?? string.Empty;
+ }
+ }
+ }
+ catch
+ {
+ // Fall through and report no path.
+ }
+
+ return string.Empty;
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysResourcesHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysResourcesHelper.cs
new file mode 100644
index 0000000000..91dd3f05b1
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysResourcesHelper.cs
@@ -0,0 +1,99 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Helpers;
+
+internal static class PowerToysResourcesHelper
+{
+ private const string SettingsIconRoot = "WinUI3Apps\\Assets\\Settings\\Icons\\";
+
+ internal static IconInfo IconFromSettingsIcon(string fileName) => IconHelpers.FromRelativePath($"{SettingsIconRoot}{fileName}");
+
+ public static IconInfo ProviderIcon() => IconFromSettingsIcon("PowerToys.png");
+
+ public static IconInfo ModuleIcon(this SettingsWindow module)
+ {
+ var iconFile = module switch
+ {
+ SettingsWindow.ColorPicker => "ColorPicker.png",
+ SettingsWindow.FancyZones => "FancyZones.png",
+ SettingsWindow.Hosts => "Hosts.png",
+ SettingsWindow.PowerOCR => "TextExtractor.png",
+ SettingsWindow.RegistryPreview => "RegistryPreview.png",
+ SettingsWindow.MeasureTool => "ScreenRuler.png",
+ SettingsWindow.ShortcutGuide => "ShortcutGuide.png",
+ SettingsWindow.CropAndLock => "CropAndLock.png",
+ SettingsWindow.EnvironmentVariables => "EnvironmentVariables.png",
+ SettingsWindow.Awake => "Awake.png",
+ SettingsWindow.PowerRename => "PowerRename.png",
+ SettingsWindow.Run => "PowerToysRun.png",
+ SettingsWindow.ImageResizer => "ImageResizer.png",
+ SettingsWindow.KBM => "KeyboardManager.png",
+ SettingsWindow.MouseUtils => "MouseUtils.png",
+ SettingsWindow.Workspaces => "Workspaces.png",
+ SettingsWindow.AdvancedPaste => "AdvancedPaste.png",
+ SettingsWindow.CmdPal => "CmdPal.png",
+ SettingsWindow.ZoomIt => "ZoomIt.png",
+ SettingsWindow.FileExplorer => "FileExplorerPreview.png",
+ SettingsWindow.FileLocksmith => "FileLocksmith.png",
+ SettingsWindow.NewPlus => "NewPlus.png",
+ SettingsWindow.Peek => "Peek.png",
+ SettingsWindow.LightSwitch => "LightSwitch.png",
+ SettingsWindow.AlwaysOnTop => "AlwaysOnTop.png",
+ SettingsWindow.CmdNotFound => "CommandNotFound.png",
+ SettingsWindow.MouseWithoutBorders => "MouseWithoutBorders.png",
+ SettingsWindow.PowerAccent => "QuickAccent.png",
+ SettingsWindow.PowerLauncher => "PowerToysRun.png",
+ SettingsWindow.PowerPreview => "FileExplorerPreview.png",
+ SettingsWindow.Overview => "PowerToys.png",
+ SettingsWindow.Dashboard => "PowerToys.png",
+ _ => "PowerToys.png",
+ };
+
+ return IconFromSettingsIcon(iconFile);
+ }
+
+ public static string ModuleDisplayName(this SettingsWindow module)
+ {
+ return module switch
+ {
+ SettingsWindow.ColorPicker => "Color Picker",
+ SettingsWindow.FancyZones => "FancyZones",
+ SettingsWindow.Hosts => "Hosts File Editor",
+ SettingsWindow.PowerOCR => "Text Extractor",
+ SettingsWindow.RegistryPreview => "Registry Preview",
+ SettingsWindow.MeasureTool => "Screen Ruler",
+ SettingsWindow.ShortcutGuide => "Shortcut Guide",
+ SettingsWindow.CropAndLock => "Crop And Lock",
+ SettingsWindow.EnvironmentVariables => "Environment Variables",
+ SettingsWindow.Awake => "Awake",
+ SettingsWindow.PowerRename => "PowerRename",
+ SettingsWindow.Run => "PowerToys Run",
+ SettingsWindow.ImageResizer => "Image Resizer",
+ SettingsWindow.KBM => "Keyboard Manager",
+ SettingsWindow.MouseUtils => "Mouse Utilities",
+ SettingsWindow.Workspaces => "Workspaces",
+ SettingsWindow.AdvancedPaste => "Advanced Paste",
+ SettingsWindow.CmdPal => "Command Palette",
+ SettingsWindow.ZoomIt => "ZoomIt",
+ SettingsWindow.FileExplorer => "File Explorer Add-ons",
+ SettingsWindow.FileLocksmith => "File Locksmith",
+ SettingsWindow.NewPlus => "New+",
+ SettingsWindow.Peek => "Peek",
+ SettingsWindow.LightSwitch => "Light Switch",
+ SettingsWindow.AlwaysOnTop => "Always On Top",
+ SettingsWindow.CmdNotFound => "Command Not Found",
+ SettingsWindow.MouseWithoutBorders => "Mouse Without Borders",
+ SettingsWindow.PowerAccent => "Quick Accent",
+ SettingsWindow.Overview => "General",
+ SettingsWindow.Dashboard => "Dashboard",
+ SettingsWindow.PowerLauncher => "PowerToys Run",
+ SettingsWindow.PowerPreview => "File Explorer Add-ons",
+ _ => module.ToString(),
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/SettingsChangeNotifier.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/SettingsChangeNotifier.cs
new file mode 100644
index 0000000000..c271bc853b
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/SettingsChangeNotifier.cs
@@ -0,0 +1,72 @@
+// 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;
+using System.Threading;
+using ManagedCommon;
+
+namespace PowerToysExtension.Helpers;
+
+///
+/// Watches the global PowerToys settings.json and notifies listeners when it changes.
+///
+internal static class SettingsChangeNotifier
+{
+ private static readonly object Sync = new();
+ private static FileSystemWatcher? _watcher;
+ private static Timer? _debounceTimer;
+
+ internal static event Action? SettingsChanged;
+
+ static SettingsChangeNotifier()
+ {
+ TryStartWatcher();
+ }
+
+ private static void TryStartWatcher()
+ {
+ try
+ {
+ var filePath = ModuleEnablementService.SettingsFilePath;
+ var directory = Path.GetDirectoryName(filePath);
+ var fileName = Path.GetFileName(filePath);
+
+ if (string.IsNullOrEmpty(directory) || string.IsNullOrEmpty(fileName))
+ {
+ return;
+ }
+
+ _watcher = new FileSystemWatcher(directory)
+ {
+ Filter = fileName,
+ NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size,
+ IncludeSubdirectories = false,
+ EnableRaisingEvents = true,
+ };
+
+ _watcher.Changed += (_, _) => ScheduleRaise();
+ _watcher.Created += (_, _) => ScheduleRaise();
+ _watcher.Deleted += (_, _) => ScheduleRaise();
+ _watcher.Renamed += (_, _) => ScheduleRaise();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"SettingsChangeNotifier failed to start: {ex.Message}");
+ }
+ }
+
+ private static void ScheduleRaise()
+ {
+ lock (Sync)
+ {
+ _debounceTimer?.Dispose();
+ _debounceTimer = new Timer(
+ _ => SettingsChanged?.Invoke(),
+ null,
+ 200,
+ Timeout.Infinite);
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj
new file mode 100644
index 0000000000..36cfeb93f4
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+ WinExe
+ PowerToysExtension
+ app.manifest
+ win-$(Platform).pubxml
+ false
+ None
+ false
+ $(SolutionDir)$(Platform)\$(Configuration)\
+ false
+ false
+ enable
+ true
+ true
+ $(CmdPalVersion)
+
+
+
+ win-arm64
+ win-x64
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+ true
+ false
+
+
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AdvancedPasteModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AdvancedPasteModuleCommandProvider.cs
new file mode 100644
index 0000000000..58083919c0
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AdvancedPasteModuleCommandProvider.cs
@@ -0,0 +1,38 @@
+// 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 Common.UI;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class AdvancedPasteModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsDeepLink.SettingsWindow.AdvancedPaste;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ if (ModuleEnablementService.IsModuleEnabled(module))
+ {
+ yield return new ListItem(new OpenAdvancedPasteCommand())
+ {
+ Title = "Open Advanced Paste",
+ Subtitle = "Launch the Advanced Paste UI",
+ Icon = icon,
+ };
+ }
+
+ yield return new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Advanced Paste settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AlwaysOnTopModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AlwaysOnTopModuleCommandProvider.cs
new file mode 100644
index 0000000000..cad8a282da
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AlwaysOnTopModuleCommandProvider.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class AlwaysOnTopModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var title = SettingsWindow.AlwaysOnTop.ModuleDisplayName();
+ var icon = SettingsWindow.AlwaysOnTop.ModuleIcon();
+
+ yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.AlwaysOnTop, title))
+ {
+ Title = title,
+ Subtitle = "Open Always On Top settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AwakeModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AwakeModuleCommandProvider.cs
new file mode 100644
index 0000000000..935371fba4
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AwakeModuleCommandProvider.cs
@@ -0,0 +1,91 @@
+// 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 Awake.ModuleServices;
+using Common.UI;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using PowerToysExtension.Pages;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class AwakeModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var items = new List();
+ var module = SettingsDeepLink.SettingsWindow.Awake;
+ var title = module.ModuleDisplayName();
+ var icon = PowerToysResourcesHelper.IconFromSettingsIcon("Awake.png");
+ var moduleIcon = module.ModuleIcon();
+
+ items.Add(new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Awake settings",
+ Icon = moduleIcon,
+ });
+
+ if (!ModuleEnablementService.IsModuleEnabled(module))
+ {
+ return items;
+ }
+
+ // Direct commands surfaced in the PowerToys list page.
+ ListItem? statusItem = null;
+ Action refreshStatus = () =>
+ {
+ if (statusItem is not null)
+ {
+ statusItem.Subtitle = AwakeStatusService.GetStatusSubtitle();
+ }
+ };
+
+ var refreshCommand = new RefreshAwakeStatusCommand(refreshStatus);
+
+ statusItem = new ListItem(new CommandItem(refreshCommand))
+ {
+ Title = "Awake: Current status",
+ Subtitle = AwakeStatusService.GetStatusSubtitle(),
+ Icon = icon,
+ };
+ items.Add(statusItem);
+
+ items.Add(new ListItem(new StartAwakeCommand("Awake: Keep awake indefinitely", () => AwakeService.Instance.SetIndefiniteAsync(), "Awake set to indefinite", refreshStatus))
+ {
+ Title = "Awake: Keep awake indefinitely",
+ Subtitle = "Run Awake in indefinite mode",
+ Icon = icon,
+ });
+ items.Add(new ListItem(new StartAwakeCommand("Awake: Keep awake for 30 minutes", () => AwakeService.Instance.SetTimedAsync(30), "Awake set for 30 minutes", refreshStatus))
+ {
+ Title = "Awake: Keep awake for 30 minutes",
+ Subtitle = "Run Awake timed for 30 minutes",
+ Icon = icon,
+ });
+ items.Add(new ListItem(new StartAwakeCommand("Awake: Keep awake for 1 hour", () => AwakeService.Instance.SetTimedAsync(60), "Awake set for 1 hour", refreshStatus))
+ {
+ Title = "Awake: Keep awake for 1 hour",
+ Subtitle = "Run Awake timed for 1 hour",
+ Icon = icon,
+ });
+ items.Add(new ListItem(new StartAwakeCommand("Awake: Keep awake for 2 hours", () => AwakeService.Instance.SetTimedAsync(120), "Awake set for 2 hours", refreshStatus))
+ {
+ Title = "Awake: Keep awake for 2 hours",
+ Subtitle = "Run Awake timed for 2 hours",
+ Icon = icon,
+ });
+ items.Add(new ListItem(new StopAwakeCommand(refreshStatus))
+ {
+ Title = "Awake: Turn off",
+ Subtitle = "Switch Awake back to Off",
+ Icon = icon,
+ });
+
+ return items;
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ColorPickerModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ColorPickerModuleCommandProvider.cs
new file mode 100644
index 0000000000..27b3be6f05
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ColorPickerModuleCommandProvider.cs
@@ -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 Common.UI;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using PowerToysExtension.Pages;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class ColorPickerModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsDeepLink.SettingsWindow.ColorPicker;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ var commands = new List();
+
+ commands.Add(new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Color Picker settings",
+ Icon = icon,
+ });
+
+ if (!ModuleEnablementService.IsModuleEnabled(module))
+ {
+ return commands;
+ }
+
+ // Direct entries in the module list.
+ commands.Add(new ListItem(new OpenColorPickerCommand())
+ {
+ Title = "Open Color Picker",
+ Subtitle = "Start a color pick session",
+ Icon = icon,
+ });
+
+ commands.Add(new ListItem(new CommandItem(new ColorPickerSavedColorsPage()))
+ {
+ Title = "Saved colors",
+ Subtitle = "Browse and copy saved colors",
+ Icon = icon,
+ });
+
+ return commands;
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CommandNotFoundModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CommandNotFoundModuleCommandProvider.cs
new file mode 100644
index 0000000000..2ec95172f9
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CommandNotFoundModuleCommandProvider.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class CommandNotFoundModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var title = SettingsWindow.CmdNotFound.ModuleDisplayName();
+ var icon = SettingsWindow.CmdNotFound.ModuleIcon();
+
+ yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.CmdNotFound, title))
+ {
+ Title = title,
+ Subtitle = "Open Command Not Found settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CropAndLockModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CropAndLockModuleCommandProvider.cs
new file mode 100644
index 0000000000..c3f6d1ccd4
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CropAndLockModuleCommandProvider.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class CropAndLockModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsWindow.CropAndLock;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ if (ModuleEnablementService.IsModuleEnabled(module))
+ {
+ yield return new ListItem(new CropAndLockReparentCommand())
+ {
+ Title = "Crop and Lock (Reparent)",
+ Subtitle = "Create a cropped reparented window",
+ Icon = icon,
+ };
+
+ yield return new ListItem(new CropAndLockThumbnailCommand())
+ {
+ Title = "Crop and Lock (Thumbnail)",
+ Subtitle = "Create a cropped thumbnail window",
+ Icon = icon,
+ };
+ }
+
+ yield return new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Crop and Lock settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/EnvironmentVariablesModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/EnvironmentVariablesModuleCommandProvider.cs
new file mode 100644
index 0000000000..d72644c1bf
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/EnvironmentVariablesModuleCommandProvider.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class EnvironmentVariablesModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsWindow.EnvironmentVariables;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ if (ModuleEnablementService.IsModuleEnabled(module))
+ {
+ yield return new ListItem(new OpenEnvironmentVariablesCommand())
+ {
+ Title = "Open Environment Variables",
+ Subtitle = "Launch Environment Variables editor",
+ Icon = icon,
+ };
+
+ yield return new ListItem(new OpenEnvironmentVariablesAdminCommand())
+ {
+ Title = "Open Environment Variables (Admin)",
+ Subtitle = "Launch Environment Variables editor as admin",
+ Icon = icon,
+ };
+ }
+
+ yield return new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Environment Variables settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FancyZonesModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FancyZonesModuleCommandProvider.cs
new file mode 100644
index 0000000000..6a4287d60f
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FancyZonesModuleCommandProvider.cs
@@ -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 Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using PowerToysExtension.Pages;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class FancyZonesModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsWindow.FancyZones;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ if (ModuleEnablementService.IsModuleEnabled(module))
+ {
+ yield return new ListItem(new CommandItem(new FancyZonesLayoutsPage()))
+ {
+ Title = "FancyZones: Layouts",
+ Subtitle = "Apply a layout to all monitors or a specific monitor",
+ Icon = icon,
+ };
+
+ yield return new ListItem(new CommandItem(new FancyZonesMonitorsPage()))
+ {
+ Title = "FancyZones: Monitors",
+ Subtitle = "Identify monitors and apply layouts",
+ Icon = icon,
+ };
+
+ yield return new ListItem(new OpenFancyZonesEditorCommand())
+ {
+ Title = "Open FancyZones Editor",
+ Subtitle = "Launch layout editor",
+ Icon = icon,
+ };
+ }
+
+ yield return new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open FancyZones settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileExplorerAddonsModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileExplorerAddonsModuleCommandProvider.cs
new file mode 100644
index 0000000000..5fa5162cf8
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileExplorerAddonsModuleCommandProvider.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class FileExplorerAddonsModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var title = SettingsWindow.FileExplorer.ModuleDisplayName();
+ var icon = SettingsWindow.FileExplorer.ModuleIcon();
+
+ yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.FileExplorer, title))
+ {
+ Title = title,
+ Subtitle = "Open File Explorer add-ons settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileLocksmithModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileLocksmithModuleCommandProvider.cs
new file mode 100644
index 0000000000..19e5a135e5
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileLocksmithModuleCommandProvider.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class FileLocksmithModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var title = SettingsWindow.FileLocksmith.ModuleDisplayName();
+ var icon = SettingsWindow.FileLocksmith.ModuleIcon();
+
+ yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.FileLocksmith, title))
+ {
+ Title = title,
+ Subtitle = "Open File Locksmith settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/HostsModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/HostsModuleCommandProvider.cs
new file mode 100644
index 0000000000..839598b428
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/HostsModuleCommandProvider.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class HostsModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsWindow.Hosts;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ if (ModuleEnablementService.IsModuleEnabled(module))
+ {
+ yield return new ListItem(new OpenHostsEditorCommand())
+ {
+ Title = "Open Hosts File Editor",
+ Subtitle = "Launch Hosts File Editor",
+ Icon = icon,
+ };
+
+ yield return new ListItem(new OpenHostsEditorAdminCommand())
+ {
+ Title = "Open Hosts File Editor (Admin)",
+ Subtitle = "Launch Hosts File Editor as admin",
+ Icon = icon,
+ };
+ }
+
+ yield return new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Hosts File Editor settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ImageResizerModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ImageResizerModuleCommandProvider.cs
new file mode 100644
index 0000000000..6ec2f1b3a6
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ImageResizerModuleCommandProvider.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class ImageResizerModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var title = SettingsWindow.ImageResizer.ModuleDisplayName();
+ var icon = SettingsWindow.ImageResizer.ModuleIcon();
+
+ yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.ImageResizer, title))
+ {
+ Title = title,
+ Subtitle = "Open Image Resizer settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs
new file mode 100644
index 0000000000..bb42f484a7
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var title = SettingsWindow.KBM.ModuleDisplayName();
+ var icon = SettingsWindow.KBM.ModuleIcon();
+
+ yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.KBM, title))
+ {
+ Title = title,
+ Subtitle = "Open Keyboard Manager settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/LightSwitchModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/LightSwitchModuleCommandProvider.cs
new file mode 100644
index 0000000000..a07c06fedb
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/LightSwitchModuleCommandProvider.cs
@@ -0,0 +1,42 @@
+// 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 Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class LightSwitchModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsWindow.LightSwitch;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ var items = new List();
+
+ if (ModuleEnablementService.IsModuleEnabled(module))
+ {
+ items.Add(new ListItem(new ToggleLightSwitchCommand())
+ {
+ Title = "Toggle Light Switch",
+ Subtitle = "Toggle system/apps theme immediately",
+ Icon = icon,
+ });
+ }
+
+ items.Add(new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Light Switch settings",
+ Icon = icon,
+ });
+
+ return items;
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ModuleCommandProvider.cs
new file mode 100644
index 0000000000..4e06731a2d
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ModuleCommandProvider.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace PowerToysExtension.Modules;
+
+///
+/// Base contract for a PowerToys module to expose its command palette entries.
+///
+internal abstract class ModuleCommandProvider
+{
+ public abstract IEnumerable BuildCommands();
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseUtilsModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseUtilsModuleCommandProvider.cs
new file mode 100644
index 0000000000..34c6be193e
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseUtilsModuleCommandProvider.cs
@@ -0,0 +1,78 @@
+// 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 Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class MouseUtilsModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsWindow.MouseUtils;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ if (ModuleEnablementService.IsKeyEnabled("FindMyMouse"))
+ {
+ yield return new ListItem(new ToggleFindMyMouseCommand())
+ {
+ Title = "Trigger Find My Mouse",
+ Subtitle = "Focus the mouse pointer",
+ Icon = icon,
+ };
+ }
+
+ if (ModuleEnablementService.IsKeyEnabled("MouseHighlighter"))
+ {
+ yield return new ListItem(new ToggleMouseHighlighterCommand())
+ {
+ Title = "Toggle Mouse Highlighter",
+ Subtitle = "Highlight mouse clicks",
+ Icon = icon,
+ };
+ }
+
+ if (ModuleEnablementService.IsKeyEnabled("MousePointerCrosshairs"))
+ {
+ yield return new ListItem(new ToggleMouseCrosshairsCommand())
+ {
+ Title = "Toggle Mouse Crosshairs",
+ Subtitle = "Enable or disable pointer crosshairs",
+ Icon = icon,
+ };
+ }
+
+ if (ModuleEnablementService.IsKeyEnabled("CursorWrap"))
+ {
+ yield return new ListItem(new ToggleCursorWrapCommand())
+ {
+ Title = "Toggle Cursor Wrap",
+ Subtitle = "Wrap the cursor across monitor edges",
+ Icon = icon,
+ };
+ }
+
+ if (ModuleEnablementService.IsKeyEnabled("MouseJump"))
+ {
+ yield return new ListItem(new ShowMouseJumpPreviewCommand())
+ {
+ Title = "Show Mouse Jump Preview",
+ Subtitle = "Jump the pointer to a target",
+ Icon = icon,
+ };
+ }
+
+ yield return new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Mouse Utilities settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseWithoutBordersModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseWithoutBordersModuleCommandProvider.cs
new file mode 100644
index 0000000000..49a3f3635a
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseWithoutBordersModuleCommandProvider.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class MouseWithoutBordersModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var title = SettingsWindow.MouseWithoutBorders.ModuleDisplayName();
+ var icon = SettingsWindow.MouseWithoutBorders.ModuleIcon();
+
+ yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.MouseWithoutBorders, title))
+ {
+ Title = title,
+ Subtitle = "Open Mouse Without Borders settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/NewPlusModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/NewPlusModuleCommandProvider.cs
new file mode 100644
index 0000000000..f88d104b73
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/NewPlusModuleCommandProvider.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class NewPlusModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var title = SettingsWindow.NewPlus.ModuleDisplayName();
+ var icon = SettingsWindow.NewPlus.ModuleIcon();
+
+ yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.NewPlus, title))
+ {
+ Title = title,
+ Subtitle = "Open New+ settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PeekModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PeekModuleCommandProvider.cs
new file mode 100644
index 0000000000..a55a187206
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PeekModuleCommandProvider.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class PeekModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var title = SettingsWindow.Peek.ModuleDisplayName();
+ var icon = SettingsWindow.Peek.ModuleIcon();
+
+ yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.Peek, title))
+ {
+ Title = title,
+ Subtitle = "Open Peek settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerRenameModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerRenameModuleCommandProvider.cs
new file mode 100644
index 0000000000..434a1d53cf
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerRenameModuleCommandProvider.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class PowerRenameModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var title = SettingsWindow.PowerRename.ModuleDisplayName();
+ var icon = SettingsWindow.PowerRename.ModuleIcon();
+
+ yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.PowerRename, title))
+ {
+ Title = title,
+ Subtitle = "Open PowerRename settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerToysRunModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerToysRunModuleCommandProvider.cs
new file mode 100644
index 0000000000..593bebb3a9
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerToysRunModuleCommandProvider.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class PowerToysRunModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var title = SettingsWindow.PowerLauncher.ModuleDisplayName();
+ var icon = SettingsWindow.PowerLauncher.ModuleIcon();
+
+ yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.PowerLauncher, title))
+ {
+ Title = title,
+ Subtitle = "Open PowerToys Run settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/QuickAccentModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/QuickAccentModuleCommandProvider.cs
new file mode 100644
index 0000000000..9122b3534c
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/QuickAccentModuleCommandProvider.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class QuickAccentModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var title = SettingsWindow.PowerAccent.ModuleDisplayName();
+ var icon = SettingsWindow.PowerAccent.ModuleIcon();
+
+ yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.PowerAccent, title))
+ {
+ Title = title,
+ Subtitle = "Open Quick Accent settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/RegistryPreviewModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/RegistryPreviewModuleCommandProvider.cs
new file mode 100644
index 0000000000..7dbe3f841b
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/RegistryPreviewModuleCommandProvider.cs
@@ -0,0 +1,38 @@
+// 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 Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class RegistryPreviewModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsWindow.RegistryPreview;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ if (ModuleEnablementService.IsModuleEnabled(module))
+ {
+ yield return new ListItem(new OpenRegistryPreviewCommand())
+ {
+ Title = "Open Registry Preview",
+ Subtitle = "Launch Registry Preview",
+ Icon = icon,
+ };
+ }
+
+ yield return new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Registry Preview settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ScreenRulerModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ScreenRulerModuleCommandProvider.cs
new file mode 100644
index 0000000000..23674c3dfe
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ScreenRulerModuleCommandProvider.cs
@@ -0,0 +1,38 @@
+// 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 Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class ScreenRulerModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsWindow.MeasureTool;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ if (ModuleEnablementService.IsModuleEnabled(module))
+ {
+ yield return new ListItem(new ToggleScreenRulerCommand())
+ {
+ Title = "Toggle Screen Ruler",
+ Subtitle = "Start or close Screen Ruler",
+ Icon = icon,
+ };
+ }
+
+ yield return new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Screen Ruler settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ShortcutGuideModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ShortcutGuideModuleCommandProvider.cs
new file mode 100644
index 0000000000..20f487c1f3
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ShortcutGuideModuleCommandProvider.cs
@@ -0,0 +1,38 @@
+// 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 Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class ShortcutGuideModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsWindow.ShortcutGuide;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ if (ModuleEnablementService.IsModuleEnabled(module))
+ {
+ yield return new ListItem(new ToggleShortcutGuideCommand())
+ {
+ Title = "Toggle Shortcut Guide",
+ Subtitle = "Show or hide Shortcut Guide",
+ Icon = icon,
+ };
+ }
+
+ yield return new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Shortcut Guide settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/TextExtractorModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/TextExtractorModuleCommandProvider.cs
new file mode 100644
index 0000000000..a8e816ccc6
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/TextExtractorModuleCommandProvider.cs
@@ -0,0 +1,38 @@
+// 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 Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class TextExtractorModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsWindow.PowerOCR;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ if (ModuleEnablementService.IsModuleEnabled(module))
+ {
+ yield return new ListItem(new ToggleTextExtractorCommand())
+ {
+ Title = "Toggle Text Extractor",
+ Subtitle = "Start or close Text Extractor",
+ Icon = icon,
+ };
+ }
+
+ yield return new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Text Extractor settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/WorkspacesModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/WorkspacesModuleCommandProvider.cs
new file mode 100644
index 0000000000..49d585ba6d
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/WorkspacesModuleCommandProvider.cs
@@ -0,0 +1,64 @@
+// 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 Common.UI;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using Workspaces.ModuleServices;
+using WorkspacesCsharpLibrary.Data;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class WorkspacesModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var items = new List();
+ var module = SettingsDeepLink.SettingsWindow.Workspaces;
+ var title = module.ModuleDisplayName();
+ var icon = PowerToysResourcesHelper.IconFromSettingsIcon("Workspaces.png");
+ var moduleIcon = module.ModuleIcon();
+
+ items.Add(new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open Workspaces settings",
+ Icon = moduleIcon,
+ });
+
+ if (!ModuleEnablementService.IsModuleEnabled(module))
+ {
+ return items;
+ }
+
+ // Settings entry plus common actions.
+ items.Add(new ListItem(new OpenWorkspaceEditorCommand())
+ {
+ Title = "Workspaces: Open editor",
+ Subtitle = "Create or edit workspaces",
+ Icon = icon,
+ });
+
+ // Per-workspace entries via the shared service.
+ foreach (var workspace in LoadWorkspaces())
+ {
+ if (string.IsNullOrWhiteSpace(workspace.Id) || string.IsNullOrWhiteSpace(workspace.Name))
+ {
+ continue;
+ }
+
+ items.Add(new WorkspaceListItem(workspace, icon));
+ }
+
+ return items;
+ }
+
+ private static IReadOnlyList LoadWorkspaces()
+ {
+ var result = WorkspaceService.Instance.GetWorkspacesAsync().GetAwaiter().GetResult();
+ return result.Success && result.Value is not null ? result.Value : System.Array.Empty();
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ZoomItModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ZoomItModuleCommandProvider.cs
new file mode 100644
index 0000000000..a73ccdfbe3
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ZoomItModuleCommandProvider.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+using static Common.UI.SettingsDeepLink;
+
+namespace PowerToysExtension.Modules;
+
+internal sealed class ZoomItModuleCommandProvider : ModuleCommandProvider
+{
+ public override IEnumerable BuildCommands()
+ {
+ var module = SettingsWindow.ZoomIt;
+ var title = module.ModuleDisplayName();
+ var icon = module.ModuleIcon();
+
+ if (ModuleEnablementService.IsModuleEnabled(module))
+ {
+ // Action commands via ZoomIt IPC
+ yield return new ListItem(new ZoomItActionCommand("zoom", "ZoomIt: Zoom"))
+ {
+ Title = "ZoomIt: Zoom",
+ Subtitle = "Enter zoom mode",
+ Icon = icon,
+ };
+ yield return new ListItem(new ZoomItActionCommand("draw", "ZoomIt: Draw"))
+ {
+ Title = "ZoomIt: Draw",
+ Subtitle = "Enter drawing mode",
+ Icon = icon,
+ };
+ yield return new ListItem(new ZoomItActionCommand("break", "ZoomIt: Break"))
+ {
+ Title = "ZoomIt: Break",
+ Subtitle = "Enter break timer",
+ Icon = icon,
+ };
+ yield return new ListItem(new ZoomItActionCommand("liveZoom", "ZoomIt: Live Zoom"))
+ {
+ Title = "ZoomIt: Live Zoom",
+ Subtitle = "Toggle live zoom",
+ Icon = icon,
+ };
+ yield return new ListItem(new ZoomItActionCommand("snip", "ZoomIt: Snip"))
+ {
+ Title = "ZoomIt: Snip",
+ Subtitle = "Enter snip mode",
+ Icon = icon,
+ };
+ yield return new ListItem(new ZoomItActionCommand("record", "ZoomIt: Record"))
+ {
+ Title = "ZoomIt: Record",
+ Subtitle = "Start recording",
+ Icon = icon,
+ };
+ }
+
+ yield return new ListItem(new OpenInSettingsCommand(module, title))
+ {
+ Title = title,
+ Subtitle = "Open ZoomIt settings",
+ Icon = icon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/ColorPickerSavedColorsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/ColorPickerSavedColorsPage.cs
new file mode 100644
index 0000000000..5e06951794
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/ColorPickerSavedColorsPage.cs
@@ -0,0 +1,96 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using System.Text;
+using ColorPicker.ModuleServices;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension.Pages;
+
+internal sealed partial class ColorPickerSavedColorsPage : DynamicListPage
+{
+ private readonly CommandItem _emptyContent;
+
+ public ColorPickerSavedColorsPage()
+ {
+ Icon = PowerToysResourcesHelper.IconFromSettingsIcon("ColorPicker.png");
+ Title = "Saved colors";
+ Name = "ColorPickerSavedColors";
+ Id = "com.microsoft.powertoys.colorpicker.savedColors";
+
+ _emptyContent = new CommandItem()
+ {
+ Title = "No saved colors",
+ Subtitle = "Pick a color first, then try again.",
+ Icon = PowerToysResourcesHelper.IconFromSettingsIcon("ColorPicker.png"),
+ };
+
+ EmptyContent = _emptyContent;
+ }
+
+ public override IListItem[] GetItems()
+ {
+ var result = ColorPickerService.Instance.GetSavedColorsAsync().GetAwaiter().GetResult();
+ if (!result.Success || result.Value is null || result.Value.Count == 0)
+ {
+ return Array.Empty();
+ }
+
+ var search = SearchText;
+ var filtered = string.IsNullOrWhiteSpace(search)
+ ? result.Value
+ : result.Value.Where(saved =>
+ saved.Hex.Contains(search, StringComparison.OrdinalIgnoreCase) ||
+ saved.Formats.Any(f => f.Value.Contains(search, StringComparison.OrdinalIgnoreCase) ||
+ f.Format.Contains(search, StringComparison.OrdinalIgnoreCase)));
+
+ var items = filtered.Select(saved =>
+ {
+ var copyValue = SelectPreferredFormat(saved);
+ var subtitle = BuildSubtitle(saved);
+
+ var command = new CopySavedColorCommand(saved, copyValue);
+ return (IListItem)new ListItem(new CommandItem(command))
+ {
+ Title = saved.Hex,
+ Subtitle = subtitle,
+ Icon = ColorSwatchIconFactory.Create(saved.R, saved.G, saved.B, saved.A),
+ };
+ }).ToArray();
+
+ return items;
+ }
+
+ public override void UpdateSearchText(string oldSearch, string newSearch)
+ {
+ _emptyContent.Subtitle = string.IsNullOrWhiteSpace(newSearch)
+ ? "Pick a color first, then try again."
+ : $"No saved colors matching '{newSearch}'";
+
+ RaiseItemsChanged(0);
+ }
+
+ private static string SelectPreferredFormat(SavedColor saved) => saved.Hex;
+
+ private static string BuildSubtitle(SavedColor saved)
+ {
+ var sb = new StringBuilder();
+ foreach (var format in saved.Formats.Take(3))
+ {
+ if (sb.Length > 0)
+ {
+ sb.Append(" · ");
+ }
+
+ sb.Append(format.Value);
+ }
+
+ return sb.Length > 0 ? sb.ToString() : saved.Hex;
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesLayoutsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesLayoutsPage.cs
new file mode 100644
index 0000000000..1b568da7d6
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesLayoutsPage.cs
@@ -0,0 +1,103 @@
+// 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.Linq;
+using System.Threading.Tasks;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension.Pages;
+
+internal sealed partial class FancyZonesLayoutsPage : DynamicListPage
+{
+ private readonly CommandItem _emptyMessage;
+
+ public FancyZonesLayoutsPage()
+ {
+ Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png");
+ Name = Title = "FancyZones Layouts";
+ Id = "com.microsoft.cmdpal.powertoys.fancyzones.layouts";
+
+ _emptyMessage = new CommandItem()
+ {
+ Title = "No layouts found",
+ Subtitle = "Open FancyZones Editor once to initialize layouts.",
+ Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"),
+ };
+ EmptyContent = _emptyMessage;
+
+ // Purge orphaned cache files in background (non-blocking)
+ Task.Run(FancyZonesThumbnailRenderer.PurgeOrphanedCache);
+ }
+
+ public override void UpdateSearchText(string oldSearch, string newSearch)
+ {
+ RaiseItemsChanged(0);
+ }
+
+ public override IListItem[] GetItems()
+ {
+ try
+ {
+ var layouts = FancyZonesDataService.GetLayouts();
+ if (!string.IsNullOrWhiteSpace(SearchText))
+ {
+ layouts = layouts
+ .Where(l => l.Title.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) ||
+ l.Subtitle.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase))
+ .ToArray();
+ }
+
+ if (layouts.Count == 0)
+ {
+ return Array.Empty();
+ }
+
+ _ = FancyZonesDataService.TryGetMonitors(out var monitors, out _);
+ var fallbackIcon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png");
+
+ var items = new List(layouts.Count);
+ foreach (var layout in layouts)
+ {
+ var defaultCommand = new ApplyFancyZonesLayoutCommand(layout, monitor: null);
+
+ var item = new FancyZonesLayoutListItem(defaultCommand, layout, fallbackIcon)
+ {
+ MoreCommands = BuildLayoutContext(layout, monitors),
+ };
+
+ items.Add(item);
+ }
+
+ return items.ToArray();
+ }
+ catch (Exception ex)
+ {
+ _emptyMessage.Subtitle = ex.Message;
+ return Array.Empty();
+ }
+ }
+
+ private static IContextItem[] BuildLayoutContext(FancyZonesLayoutDescriptor layout, IReadOnlyList monitors)
+ {
+ var commands = new List(monitors.Count);
+
+ for (var i = 0; i < monitors.Count; i++)
+ {
+ var monitor = monitors[i];
+ commands.Add(new CommandContextItem(new ApplyFancyZonesLayoutCommand(layout, monitor))
+ {
+ Title = string.Format(CultureInfo.CurrentCulture, "Apply to {0}", monitor.Title),
+ Subtitle = monitor.Subtitle,
+ });
+ }
+
+ return commands.ToArray();
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorLayoutPickerPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorLayoutPickerPage.cs
new file mode 100644
index 0000000000..0269f84ed0
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorLayoutPickerPage.cs
@@ -0,0 +1,68 @@
+// 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.Linq;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension.Pages;
+
+internal sealed partial class FancyZonesMonitorLayoutPickerPage : DynamicListPage
+{
+ private readonly FancyZonesMonitorDescriptor _monitor;
+ private readonly CommandItem _emptyMessage;
+
+ public FancyZonesMonitorLayoutPickerPage(FancyZonesMonitorDescriptor monitor)
+ {
+ _monitor = monitor;
+ Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png");
+ Name = Title = $"Set active layout for {_monitor.Title}";
+ Id = $"com.microsoft.cmdpal.powertoys.fancyzones.monitor.{_monitor.Index}.layouts";
+
+ _emptyMessage = new CommandItem()
+ {
+ Title = "No layouts found",
+ Subtitle = "Open FancyZones Editor once to initialize layouts.",
+ Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"),
+ };
+ EmptyContent = _emptyMessage;
+ }
+
+ public override void UpdateSearchText(string oldSearch, string newSearch)
+ {
+ RaiseItemsChanged(0);
+ }
+
+ public override IListItem[] GetItems()
+ {
+ var layouts = FancyZonesDataService.GetLayouts();
+ if (!string.IsNullOrWhiteSpace(SearchText))
+ {
+ layouts = layouts
+ .Where(l => l.Title.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) ||
+ l.Subtitle.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase))
+ .ToArray();
+ }
+
+ if (layouts.Count == 0)
+ {
+ return Array.Empty();
+ }
+
+ var fallbackIcon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png");
+ var items = new List(layouts.Count);
+ foreach (var layout in layouts)
+ {
+ var command = new ApplyFancyZonesLayoutCommand(layout, _monitor);
+ var item = new FancyZonesLayoutListItem(command, layout, fallbackIcon);
+ items.Add(item);
+ }
+
+ return [.. items];
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorsPage.cs
new file mode 100644
index 0000000000..8422038d3d
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorsPage.cs
@@ -0,0 +1,67 @@
+// 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 Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension.Pages;
+
+internal sealed partial class FancyZonesMonitorsPage : DynamicListPage
+{
+ private readonly CommandItem _emptyMessage;
+
+ public FancyZonesMonitorsPage()
+ {
+ Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png");
+ Name = Title = "FancyZones Monitors";
+ Id = "com.microsoft.cmdpal.powertoys.fancyzones.monitors";
+
+ _emptyMessage = new CommandItem()
+ {
+ Title = "No monitors found",
+ Subtitle = "Open FancyZones Editor once to initialize monitor data.",
+ Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"),
+ };
+ EmptyContent = _emptyMessage;
+ }
+
+ public override void UpdateSearchText(string oldSearch, string newSearch)
+ {
+ RaiseItemsChanged(0);
+ }
+
+ public override IListItem[] GetItems()
+ {
+ if (!FancyZonesDataService.TryGetMonitors(out var monitors, out var error))
+ {
+ _emptyMessage.Subtitle = error;
+ return Array.Empty();
+ }
+
+ var monitorIcon = new IconInfo("\uE7F4");
+ var items = new List(monitors.Count);
+
+ foreach (var monitor in monitors)
+ {
+ if (!string.IsNullOrWhiteSpace(SearchText) &&
+ !monitor.Title.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) &&
+ !monitor.Subtitle.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase))
+ {
+ continue;
+ }
+
+ var layoutDescription = FancyZonesDataService.TryGetAppliedLayoutForMonitor(monitor.Data, out var applied) && applied is not null
+ ? $"Current layout: {applied.Value.Type}"
+ : "Current layout: unknown";
+
+ var item = new FancyZonesMonitorListItem(monitor, layoutDescription, monitorIcon);
+ items.Add(item);
+ }
+
+ return [.. items];
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysExtensionPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysExtensionPage.cs
new file mode 100644
index 0000000000..0d81573280
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysExtensionPage.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Awake.ModuleServices;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Commands;
+
+namespace PowerToysExtension;
+
+internal sealed partial class PowerToysExtensionPage : ListPage
+{
+ public PowerToysExtensionPage()
+ {
+ Icon = Helpers.PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png");
+ Title = "PowerToys";
+ Name = "PowerToys commands";
+ }
+
+ public override IListItem[] GetItems()
+ {
+ return [
+ new ListItem(new LaunchModuleCommand("PowerToys", executableName: "PowerToys.exe", displayName: "Open PowerToys"))
+ {
+ Title = "Open PowerToys",
+ Subtitle = "Launch the PowerToys shell",
+ },
+ new ListItem(new OpenPowerToysSettingsCommand("PowerToys", "General"))
+ {
+ Title = "Open PowerToys settings",
+ Subtitle = "Open the main PowerToys settings window",
+ },
+ new ListItem(new OpenPowerToysSettingsCommand("Workspaces", "Workspaces"))
+ {
+ Title = "Open Workspaces settings",
+ Subtitle = "Jump directly to Workspaces settings",
+ },
+ new ListItem(new OpenWorkspaceEditorCommand())
+ {
+ Title = "Open Workspaces editor",
+ Subtitle = "Launch the Workspaces editor",
+ },
+ ];
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysListPage.cs
new file mode 100644
index 0000000000..c7eed2594f
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysListPage.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension.Pages;
+
+internal sealed partial class PowerToysListPage : ListPage
+{
+ private readonly CommandItem _empty;
+
+ public PowerToysListPage()
+ {
+ Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png");
+ Name = Title = "PowerToys";
+ Id = "com.microsoft.cmdpal.powertoys";
+ SettingsChangeNotifier.SettingsChanged += OnSettingsChanged;
+ _empty = new CommandItem()
+ {
+ Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"),
+ Title = "No matching module found",
+ Subtitle = SearchText,
+ };
+ EmptyContent = _empty;
+ }
+
+ private void OnSettingsChanged()
+ {
+ RaiseItemsChanged(0);
+ }
+
+ public override IListItem[] GetItems() => ModuleCommandCatalog.GetAllItems();
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysCommandsProvider.cs
new file mode 100644
index 0000000000..d4dde03b46
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysCommandsProvider.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using ManagedCommon;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension;
+
+public sealed partial class PowerToysCommandsProvider : CommandProvider
+{
+ public PowerToysCommandsProvider()
+ {
+ DisplayName = "PowerToys";
+ Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png");
+ }
+
+ public override ICommandItem[] TopLevelCommands() =>
+ [
+ new CommandItem(new Pages.PowerToysListPage())
+ {
+ Title = "PowerToys",
+ Subtitle = "PowerToys commands and settings",
+ }
+ ];
+
+ public override IFallbackCommandItem[] FallbackCommands()
+ {
+ var items = ModuleCommandCatalog.GetAllItems();
+ var fallbacks = new List(items.Length);
+ foreach (var item in items)
+ {
+ if (item?.Command is not ICommand cmd)
+ {
+ continue;
+ }
+
+ fallbacks.Add(new PowerToysFallbackCommandItem(cmd, item.Title, item.Subtitle, item.Icon, item.MoreCommands));
+ }
+
+ return fallbacks.ToArray();
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtension.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtension.cs
new file mode 100644
index 0000000000..f4100db51a
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtension.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+using ManagedCommon;
+using Microsoft.CommandPalette.Extensions;
+
+namespace PowerToysExtension;
+
+[Guid("7EC02C7D-8F98-4A2E-9F23-B58C2C2F2B17")]
+public sealed partial class PowerToysExtension : IExtension, IDisposable
+{
+ private readonly ManualResetEvent _extensionDisposedEvent;
+
+ private readonly PowerToysExtensionCommandsProvider _provider = new();
+
+ public PowerToysExtension(ManualResetEvent extensionDisposedEvent)
+ {
+ this._extensionDisposedEvent = extensionDisposedEvent;
+ Logger.LogInfo($"PowerToysExtension constructed. ProcArch={RuntimeInformation.ProcessArchitecture} OSArch={RuntimeInformation.OSArchitecture} BaseDir={AppContext.BaseDirectory}");
+ }
+
+ public object? GetProvider(ProviderType providerType)
+ {
+ Logger.LogInfo($"GetProvider requested: {providerType}");
+ return providerType switch
+ {
+ ProviderType.Commands => _provider,
+ _ => null,
+ };
+ }
+
+ public void Dispose()
+ {
+ Logger.LogInfo("PowerToysExtension disposing; signalling exit.");
+ this._extensionDisposedEvent.Set();
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtensionCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtensionCommandsProvider.cs
new file mode 100644
index 0000000000..beba6b484a
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtensionCommandsProvider.cs
@@ -0,0 +1,50 @@
+// 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 Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PowerToysExtension.Helpers;
+
+namespace PowerToysExtension;
+
+public partial class PowerToysExtensionCommandsProvider : CommandProvider
+{
+ private readonly ICommandItem[] _commands;
+
+ public PowerToysExtensionCommandsProvider()
+ {
+ DisplayName = "PowerToys";
+ Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png");
+ _commands = [
+ new CommandItem(new Pages.PowerToysListPage())
+ {
+ Title = "PowerToys",
+ Subtitle = "PowerToys commands and settings",
+ },
+ ];
+ }
+
+ public override ICommandItem[] TopLevelCommands()
+ {
+ return _commands;
+ }
+
+ public override IFallbackCommandItem[] FallbackCommands()
+ {
+ var items = ModuleCommandCatalog.GetAllItems();
+ var fallbacks = new List(items.Length);
+ foreach (var item in items)
+ {
+ if (item?.Command is not ICommand cmd)
+ {
+ continue;
+ }
+
+ fallbacks.Add(new PowerToysFallbackCommandItem(cmd, item.Title, item.Subtitle, item.Icon, item.MoreCommands));
+ }
+
+ return fallbacks.ToArray();
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Program.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Program.cs
new file mode 100644
index 0000000000..2706f50f90
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Program.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+using ManagedCommon;
+using Microsoft.CommandPalette.Extensions;
+using Shmuelie.WinRTServer;
+using Shmuelie.WinRTServer.CsWinRT;
+
+namespace PowerToysExtension;
+
+public class Program
+{
+ [MTAThread]
+ public static void Main(string[] args)
+ {
+ try
+ {
+ // Initialize per-extension log under CmdPal/PowerToysExtension.
+ Logger.InitializeLogger("\\CmdPal\\PowerToysExtension\\Logs");
+ Logger.LogInfo($"PowerToysExtension starting. Args=\"{string.Join(' ', args)}\" ProcArch={RuntimeInformation.ProcessArchitecture} OSArch={RuntimeInformation.OSArchitecture} BaseDir={AppContext.BaseDirectory}");
+ }
+ catch
+ {
+ // Continue even if logging fails.
+ }
+
+ try
+ {
+ if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
+ {
+ Logger.LogInfo("RegisterProcessAsComServer mode detected.");
+ ComServer server = new();
+ ManualResetEvent extensionDisposedEvent = new(false);
+ try
+ {
+ PowerToysExtension extensionInstance = new(extensionDisposedEvent);
+ Logger.LogInfo("Registering extension via Shmuelie.WinRTServer.");
+ server.RegisterClass(() => extensionInstance);
+ server.Start();
+ Logger.LogInfo("Extension instance registered; waiting for disposal signal.");
+
+ extensionDisposedEvent.WaitOne();
+ Logger.LogInfo("Extension disposed signal received; exiting server loop.");
+ }
+ finally
+ {
+ server.Stop();
+ server.UnsafeDispose();
+ }
+ }
+ else
+ {
+ Console.WriteLine("Not being launched as a Extension... exiting.");
+ Logger.LogInfo("Exited: not launched with -RegisterProcessAsComServer.");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Unhandled exception in PowerToysExtension.Main", ex);
+ throw;
+ }
+ finally
+ {
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Public/README.md b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Public/README.md
new file mode 100644
index 0000000000..99bded1694
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Public/README.md
@@ -0,0 +1,6 @@
+# PowerToys Command Palette Extension
+
+This folder is exposed to the Windows Command Palette host via the
+`PublicFolder` attribute in the AppExtension registration. It intentionally
+contains only documentation today, but can be used for additional metadata in
+the future without requiring code changes.
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/app.manifest b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/app.manifest
new file mode 100644
index 0000000000..013aaee199
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/app.manifest
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+ PerMonitorV2
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/colorPicker/ColorPicker.ModuleServices/ColorFormatValue.cs b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorFormatValue.cs
new file mode 100644
index 0000000000..90e71e6f18
--- /dev/null
+++ b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorFormatValue.cs
@@ -0,0 +1,7 @@
+// 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 ColorPicker.ModuleServices;
+
+public sealed record ColorFormatValue(string Format, string Value);
diff --git a/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPicker.ModuleServices.csproj b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPicker.ModuleServices.csproj
new file mode 100644
index 0000000000..1efe5cdc8d
--- /dev/null
+++ b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPicker.ModuleServices.csproj
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ enable
+ enable
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerService.cs b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerService.cs
new file mode 100644
index 0000000000..6407ae6ed9
--- /dev/null
+++ b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerService.cs
@@ -0,0 +1,157 @@
+// 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.Drawing;
+using System.Text.Json;
+using Common.UI;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using PowerToys.Interop;
+using PowerToys.ModuleContracts;
+
+namespace ColorPicker.ModuleServices;
+
+///
+/// Provides programmatic control for Color Picker actions.
+///
+public sealed class ColorPickerService : ModuleServiceBase, IColorPickerService
+{
+ public static ColorPickerService Instance { get; } = new();
+
+ public override string Key => SettingsDeepLink.SettingsWindow.ColorPicker.ToString();
+
+ protected override SettingsDeepLink.SettingsWindow SettingsWindow => SettingsDeepLink.SettingsWindow.ColorPicker;
+
+ public override Task LaunchAsync(CancellationToken cancellationToken = default)
+ {
+ // Default launch -> open picker.
+ return OpenPickerAsync(cancellationToken);
+ }
+
+ public Task OpenPickerAsync(CancellationToken cancellationToken = default)
+ {
+ return SignalEventAsync(Constants.ShowColorPickerSharedEvent(), "Color Picker");
+ }
+
+ public Task>> GetSavedColorsAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ var historyPath = Path.Combine(localAppData, "Microsoft", "PowerToys", "ColorPicker", "colorHistory.json");
+ if (!File.Exists(historyPath))
+ {
+ return Task.FromResult(OperationResults.Ok>(Array.Empty()));
+ }
+
+ using var stream = File.OpenRead(historyPath);
+ var colors = JsonSerializer.Deserialize(stream, ColorPickerServiceJsonContext.Default.ListString) ?? new List();
+
+ var settingsUtils = SettingsUtils.Default;
+ var settings = settingsUtils.GetSettingsOrDefault(ColorPickerSettings.ModuleName);
+
+ var results = new List(colors.Count);
+ foreach (var entry in colors)
+ {
+ if (!TryParseArgb(entry, out var color))
+ {
+ continue;
+ }
+
+ var formats = BuildFormats(color, settings);
+ var hex = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
+
+ results.Add(new SavedColor(
+ hex,
+ color.A,
+ color.R,
+ color.G,
+ color.B,
+ formats));
+ }
+
+ return Task.FromResult(OperationResults.Ok>(results));
+ }
+ catch (OperationCanceledException)
+ {
+ return Task.FromResult(OperationResults.Fail>("Reading saved colors was cancelled."));
+ }
+ catch (Exception ex)
+ {
+ return Task.FromResult(OperationResults.Fail>($"Failed to read saved colors: {ex.Message}"));
+ }
+ }
+
+ private static Task SignalEventAsync(string eventName, string actionDescription)
+ {
+ try
+ {
+ using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
+ if (!eventHandle.Set())
+ {
+ return Task.FromResult(OperationResult.Fail($"Failed to signal {actionDescription}."));
+ }
+
+ return Task.FromResult(OperationResult.Ok());
+ }
+ catch (Exception ex)
+ {
+ return Task.FromResult(OperationResult.Fail($"Failed to signal {actionDescription}: {ex.Message}"));
+ }
+ }
+
+ private static bool TryParseArgb(string value, out Color color)
+ {
+ color = Color.Empty;
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return false;
+ }
+
+ var parts = value.Split('|');
+ if (parts.Length != 4)
+ {
+ return false;
+ }
+
+ if (byte.TryParse(parts[0], out var a) &&
+ byte.TryParse(parts[1], out var r) &&
+ byte.TryParse(parts[2], out var g) &&
+ byte.TryParse(parts[3], out var b))
+ {
+ color = Color.FromArgb(a, r, g, b);
+ return true;
+ }
+
+ return false;
+ }
+
+ private static IReadOnlyList BuildFormats(Color color, ColorPickerSettings settings)
+ {
+ var formats = new List();
+ foreach (var kvp in settings.Properties.VisibleColorFormats)
+ {
+ var formatName = kvp.Key;
+ var (isVisible, formatString) = kvp.Value;
+ if (!isVisible)
+ {
+ continue;
+ }
+
+ var formatted = ColorFormatHelper.GetStringRepresentation(color, formatString);
+ if (formatName.Equals("HEX", StringComparison.OrdinalIgnoreCase) && !formatted.StartsWith('#'))
+ {
+ formatted = "#" + formatted;
+ }
+
+ formats.Add(new ColorFormatValue(formatName, formatted));
+ }
+
+ return formats;
+ }
+}
diff --git a/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerServiceJsonContext.cs b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerServiceJsonContext.cs
new file mode 100644
index 0000000000..f26e9009d3
--- /dev/null
+++ b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerServiceJsonContext.cs
@@ -0,0 +1,19 @@
+// 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 Microsoft.PowerToys.Settings.UI.Library;
+
+namespace ColorPicker.ModuleServices;
+
+[JsonSourceGenerationOptions(WriteIndented = true)]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(SavedColor))]
+[JsonSerializable(typeof(ColorFormatValue))]
+[JsonSerializable(typeof(ColorPickerSettings))]
+internal sealed partial class ColorPickerServiceJsonContext : JsonSerializerContext
+{
+}
diff --git a/src/modules/colorPicker/ColorPicker.ModuleServices/IColorPickerService.cs b/src/modules/colorPicker/ColorPicker.ModuleServices/IColorPickerService.cs
new file mode 100644
index 0000000000..4ad2ca3da3
--- /dev/null
+++ b/src/modules/colorPicker/ColorPicker.ModuleServices/IColorPickerService.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using PowerToys.ModuleContracts;
+
+namespace ColorPicker.ModuleServices;
+
+public interface IColorPickerService : IModuleService
+{
+ Task OpenPickerAsync(CancellationToken cancellationToken = default);
+
+ Task>> GetSavedColorsAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/modules/colorPicker/ColorPicker.ModuleServices/SavedColor.cs b/src/modules/colorPicker/ColorPicker.ModuleServices/SavedColor.cs
new file mode 100644
index 0000000000..3697129aa0
--- /dev/null
+++ b/src/modules/colorPicker/ColorPicker.ModuleServices/SavedColor.cs
@@ -0,0 +1,7 @@
+// 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 ColorPicker.ModuleServices;
+
+public sealed record SavedColor(string Hex, byte A, byte R, byte G, byte B, IReadOnlyList Formats);
diff --git a/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs b/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs
index 705324cb93..72c874d839 100644
--- a/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs
+++ b/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs
@@ -205,7 +205,7 @@ namespace ColorPicker.Helpers
private void ColorEditorViewModel_OpenSettingsRequested(object sender, EventArgs e)
{
- SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ColorPicker, false);
+ SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ColorPicker);
}
internal void RegisterWindowHandle(System.Windows.Interop.HwndSource hwndSource)
diff --git a/src/modules/fancyzones/FancyZones.FuzzTests/FancyZones.FuzzTests.csproj b/src/modules/fancyzones/FancyZones.FuzzTests/FancyZones.FuzzTests.csproj
index 9428e10608..dd1665bc0d 100644
--- a/src/modules/fancyzones/FancyZones.FuzzTests/FancyZones.FuzzTests.csproj
+++ b/src/modules/fancyzones/FancyZones.FuzzTests/FancyZones.FuzzTests.csproj
@@ -10,10 +10,16 @@
+
-
+
+
+
+
+
+
diff --git a/src/modules/fancyzones/FancyZones/FancyZonesApp.cpp b/src/modules/fancyzones/FancyZones/FancyZonesApp.cpp
index 97faec65d5..a20a6fc7f6 100644
--- a/src/modules/fancyzones/FancyZones/FancyZonesApp.cpp
+++ b/src/modules/fancyzones/FancyZones/FancyZonesApp.cpp
@@ -22,7 +22,7 @@ FancyZonesApp::FancyZonesApp(const std::wstring& appName, const std::wstring& ap
m_app = MakeFancyZones(reinterpret_cast(&__ImageBase), std::bind(&FancyZonesApp::DisableModule, this));
m_mainThreadId = GetCurrentThreadId();
- m_exitEventWaiter = EventWaiter(CommonSharedConstants::FZE_EXIT_EVENT, [&](int err) {
+ m_exitEventWaiter.start(CommonSharedConstants::FZE_EXIT_EVENT, [&](DWORD err) {
if (err == ERROR_SUCCESS)
{
DisableModule();
diff --git a/src/modules/fancyzones/FancyZonesEditor.UITests/Utils/AppZoneHistory.cs b/src/modules/fancyzones/FancyZonesEditor.UITests/Utils/AppZoneHistory.cs
index e8daa83348..f246665aea 100644
--- a/src/modules/fancyzones/FancyZonesEditor.UITests/Utils/AppZoneHistory.cs
+++ b/src/modules/fancyzones/FancyZonesEditor.UITests/Utils/AppZoneHistory.cs
@@ -55,24 +55,24 @@ namespace FancyZonesEditorCommon.Data
public JsonElement ToJsonElement(ZoneHistoryWrapper info)
{
- string json = JsonSerializer.Serialize(info, this.JsonOptions);
+ string json = JsonSerializer.Serialize(info, JsonOptions);
return JsonSerializer.Deserialize(json);
}
public JsonElement ToJsonElement(DeviceIdWrapper info)
{
- string json = JsonSerializer.Serialize(info, this.JsonOptions);
+ string json = JsonSerializer.Serialize(info, JsonOptions);
return JsonSerializer.Deserialize(json);
}
public ZoneHistoryWrapper ZoneHistoryFromJsonElement(string json)
{
- return JsonSerializer.Deserialize(json, this.JsonOptions);
+ return JsonSerializer.Deserialize(json, JsonOptions);
}
public DeviceIdWrapper GridFromJsonElement(string json)
{
- return JsonSerializer.Deserialize(json, this.JsonOptions);
+ return JsonSerializer.Deserialize(json, JsonOptions);
}
}
}
diff --git a/src/modules/fancyzones/FancyZonesEditorCommon/Data/CustomLayouts.cs b/src/modules/fancyzones/FancyZonesEditorCommon/Data/CustomLayouts.cs
index 7be8746964..75671e8ed8 100644
--- a/src/modules/fancyzones/FancyZonesEditorCommon/Data/CustomLayouts.cs
+++ b/src/modules/fancyzones/FancyZonesEditorCommon/Data/CustomLayouts.cs
@@ -78,24 +78,24 @@ namespace FancyZonesEditorCommon.Data
public JsonElement ToJsonElement(CanvasInfoWrapper info)
{
- string json = JsonSerializer.Serialize(info, this.JsonOptions);
+ string json = JsonSerializer.Serialize(info, FancyZonesJsonContext.Default.CanvasInfoWrapper);
return JsonSerializer.Deserialize(json);
}
public JsonElement ToJsonElement(GridInfoWrapper info)
{
- string json = JsonSerializer.Serialize(info, this.JsonOptions);
+ string json = JsonSerializer.Serialize(info, FancyZonesJsonContext.Default.GridInfoWrapper);
return JsonSerializer.Deserialize(json);
}
public CanvasInfoWrapper CanvasFromJsonElement(string json)
{
- return JsonSerializer.Deserialize(json, this.JsonOptions);
+ return JsonSerializer.Deserialize(json, FancyZonesJsonContext.Default.CanvasInfoWrapper);
}
public GridInfoWrapper GridFromJsonElement(string json)
{
- return JsonSerializer.Deserialize(json, this.JsonOptions);
+ return JsonSerializer.Deserialize(json, FancyZonesJsonContext.Default.GridInfoWrapper);
}
}
}
diff --git a/src/modules/fancyzones/FancyZonesEditorCommon/Data/EditorData`1.cs b/src/modules/fancyzones/FancyZonesEditorCommon/Data/EditorData`1.cs
index fdfa32cc28..83b35f4ac8 100644
--- a/src/modules/fancyzones/FancyZonesEditorCommon/Data/EditorData`1.cs
+++ b/src/modules/fancyzones/FancyZonesEditorCommon/Data/EditorData`1.cs
@@ -4,6 +4,7 @@
using System;
using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
using FancyZonesEditorCommon.Utils;
@@ -16,28 +17,20 @@ namespace FancyZonesEditorCommon.Data
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
}
- protected JsonSerializerOptions JsonOptions
- {
- get
- {
- return new JsonSerializerOptions
- {
- PropertyNamingPolicy = new DashCaseNamingPolicy(),
- WriteIndented = true,
- };
- }
- }
+ protected static JsonSerializerOptions JsonOptions => FancyZonesJsonContext.Default.Options;
+
+ protected static JsonTypeInfo TypeInfo => (JsonTypeInfo)FancyZonesJsonContext.Default.GetTypeInfo(typeof(T));
public T Read(string file)
{
IOUtils ioUtils = new IOUtils();
string data = ioUtils.ReadFile(file);
- return JsonSerializer.Deserialize(data, JsonOptions);
+ return JsonSerializer.Deserialize(data, TypeInfo);
}
public string Serialize(T data)
{
- return JsonSerializer.Serialize(data, JsonOptions);
+ return JsonSerializer.Serialize(data, TypeInfo);
}
}
}
diff --git a/src/modules/fancyzones/FancyZonesEditorCommon/Data/FancyZonesJsonContext.cs b/src/modules/fancyzones/FancyZonesEditorCommon/Data/FancyZonesJsonContext.cs
new file mode 100644
index 0000000000..69fa8a97b7
--- /dev/null
+++ b/src/modules/fancyzones/FancyZonesEditorCommon/Data/FancyZonesJsonContext.cs
@@ -0,0 +1,38 @@
+// 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 FancyZonesEditorCommon.Utils;
+
+namespace FancyZonesEditorCommon.Data
+{
+ ///
+ /// JSON serialization context for AOT-compatible serialization of FancyZones data types.
+ ///
+ [JsonSourceGenerationOptions(
+ PropertyNamingPolicy = JsonKnownNamingPolicy.KebabCaseLower,
+ WriteIndented = true)]
+ [JsonSerializable(typeof(AppliedLayouts.AppliedLayoutsListWrapper))]
+ [JsonSerializable(typeof(AppliedLayouts.AppliedLayoutWrapper))]
+ [JsonSerializable(typeof(AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper))]
+ [JsonSerializable(typeof(AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper), TypeInfoPropertyName = "AppliedLayoutLayoutWrapper")]
+ [JsonSerializable(typeof(CustomLayouts.CustomLayoutListWrapper))]
+ [JsonSerializable(typeof(CustomLayouts.CustomLayoutWrapper))]
+ [JsonSerializable(typeof(CustomLayouts.CanvasInfoWrapper))]
+ [JsonSerializable(typeof(CustomLayouts.CanvasInfoWrapper.CanvasZoneWrapper))]
+ [JsonSerializable(typeof(CustomLayouts.GridInfoWrapper))]
+ [JsonSerializable(typeof(LayoutTemplates.TemplateLayoutsListWrapper))]
+ [JsonSerializable(typeof(LayoutTemplates.TemplateLayoutWrapper))]
+ [JsonSerializable(typeof(LayoutHotkeys.LayoutHotkeysWrapper))]
+ [JsonSerializable(typeof(LayoutHotkeys.LayoutHotkeyWrapper))]
+ [JsonSerializable(typeof(EditorParameters.ParamsWrapper))]
+ [JsonSerializable(typeof(EditorParameters.NativeMonitorDataWrapper))]
+ [JsonSerializable(typeof(DefaultLayouts.DefaultLayoutsListWrapper))]
+ [JsonSerializable(typeof(DefaultLayouts.DefaultLayoutWrapper))]
+ [JsonSerializable(typeof(DefaultLayouts.DefaultLayoutWrapper.LayoutWrapper), TypeInfoPropertyName = "DefaultLayoutLayoutWrapper")]
+ public partial class FancyZonesJsonContext : JsonSerializerContext
+ {
+ }
+}
diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp
index 68bd08c6af..4e83a450f5 100644
--- a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp
+++ b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp
@@ -261,7 +261,7 @@ FancyZones::Run() noexcept
} })
.wait();
- m_toggleEditorEventWaiter = EventWaiter(CommonSharedConstants::FANCY_ZONES_EDITOR_TOGGLE_EVENT, [&](int err) {
+ m_toggleEditorEventWaiter.start(CommonSharedConstants::FANCY_ZONES_EDITOR_TOGGLE_EVENT, [&](DWORD err) {
if (err == ERROR_SUCCESS)
{
Logger::trace(L"{} event was signaled", CommonSharedConstants::FANCY_ZONES_EDITOR_TOGGLE_EVENT);
diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/EditorWindow.cs b/src/modules/fancyzones/editor/FancyZonesEditor/EditorWindow.cs
index 2543568436..c6e5e5aec5 100644
--- a/src/modules/fancyzones/editor/FancyZonesEditor/EditorWindow.cs
+++ b/src/modules/fancyzones/editor/FancyZonesEditor/EditorWindow.cs
@@ -4,7 +4,6 @@
using System;
using System.Windows;
-
using FancyZonesEditor.Models;
using ManagedCommon;
diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs
index 3562175bca..eebd0bb54f 100644
--- a/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs
+++ b/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs
@@ -563,7 +563,7 @@ namespace FancyZonesEditor
private void SettingsBtn_Click(object sender, RoutedEventArgs e)
{
- SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.FancyZones, false);
+ SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.FancyZones);
}
private void EditLayoutDialogTitle_Loaded(object sender, RoutedEventArgs e)
diff --git a/src/modules/imageresizer/ui/ViewModels/InputViewModel.cs b/src/modules/imageresizer/ui/ViewModels/InputViewModel.cs
index c05768d9e0..c241728276 100644
--- a/src/modules/imageresizer/ui/ViewModels/InputViewModel.cs
+++ b/src/modules/imageresizer/ui/ViewModels/InputViewModel.cs
@@ -195,7 +195,7 @@ namespace ImageResizer.ViewModels
public static void OpenSettings()
{
- SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ImageResizer, false);
+ SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ImageResizer);
}
private void HandleEnterKeyPress(KeyPressParams parameters)
diff --git a/src/modules/keyboardmanager/KeyboardManagerEngine/main.cpp b/src/modules/keyboardmanager/KeyboardManagerEngine/main.cpp
index 5969ed6cfd..df48555df5 100644
--- a/src/modules/keyboardmanager/KeyboardManagerEngine/main.cpp
+++ b/src/modules/keyboardmanager/KeyboardManagerEngine/main.cpp
@@ -51,7 +51,8 @@ int WINAPI wWinMain(_In_ HINSTANCE /*hInstance*/,
auto mainThreadId = GetCurrentThreadId();
- EventWaiter ev = EventWaiter(CommonSharedConstants::TERMINATE_KBM_SHARED_EVENT, [&](int) {
+ EventWaiter ev;
+ ev.start(CommonSharedConstants::TERMINATE_KBM_SHARED_EVENT, [&](DWORD) {
PostThreadMessage(mainThreadId, WM_QUIT, 0, 0);
});
diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManager.cpp b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManager.cpp
index 8d9ec63698..3eb3261524 100644
--- a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManager.cpp
+++ b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManager.cpp
@@ -70,7 +70,7 @@ KeyboardManager::KeyboardManager()
};
editorIsRunningEvent = CreateEvent(nullptr, true, false, KeyboardManagerConstants::EditorWindowEventName.c_str());
- settingsEventWaiter = EventWaiter(KeyboardManagerConstants::SettingsEventName, changeSettingsCallback);
+ settingsEventWaiter.start(KeyboardManagerConstants::SettingsEventName, changeSettingsCallback);
}
void KeyboardManager::LoadSettings()
diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs
index 03f0090c80..0b154f8e9f 100644
--- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs
+++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs
@@ -98,7 +98,7 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys.Components
AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
Action = _ =>
{
- SettingsDeepLink.OpenSettings(settingsWindow.Value, false);
+ SettingsDeepLink.OpenSettings(settingsWindow.Value);
return true;
},
});
diff --git a/src/modules/registrypreview/RegistryPreviewExt/dllmain.cpp b/src/modules/registrypreview/RegistryPreviewExt/dllmain.cpp
index fb39833553..70e63d9bb0 100644
--- a/src/modules/registrypreview/RegistryPreviewExt/dllmain.cpp
+++ b/src/modules/registrypreview/RegistryPreviewExt/dllmain.cpp
@@ -161,7 +161,7 @@ public:
init_settings();
triggerEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::REGISTRY_PREVIEW_TRIGGER_EVENT);
- triggerEventWaiter = EventWaiter(CommonSharedConstants::REGISTRY_PREVIEW_TRIGGER_EVENT, [this](int) {
+ triggerEventWaiter.start(CommonSharedConstants::REGISTRY_PREVIEW_TRIGGER_EVENT, [this](DWORD) {
on_hotkey(0);
});
}
diff --git a/src/settings-ui/Settings.UI.XamlIndexBuilder/Settings.UI.XamlIndexBuilder.csproj b/src/settings-ui/Settings.UI.XamlIndexBuilder/Settings.UI.XamlIndexBuilder.csproj
index f8138b6bfa..dba7fdcd3b 100644
--- a/src/settings-ui/Settings.UI.XamlIndexBuilder/Settings.UI.XamlIndexBuilder.csproj
+++ b/src/settings-ui/Settings.UI.XamlIndexBuilder/Settings.UI.XamlIndexBuilder.csproj
@@ -40,4 +40,4 @@
-
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
index a6751adb98..dd396112d3 100644
--- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
+++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
@@ -214,4 +214,4 @@
-
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs
index 774245eb34..6edc318089 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs
@@ -336,7 +336,7 @@ namespace Microsoft.PowerToys.Settings.UI
});
#else
/* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */
- Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard, true);
+ Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard);
Exit();
#endif
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs
index cf4488b759..9d7f0e78f3 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs
@@ -348,7 +348,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
private void DataDiagnostics_OpenSettings_Click(Microsoft.UI.Xaml.Documents.Hyperlink sender, Microsoft.UI.Xaml.Documents.HyperlinkClickEventArgs args)
{
- Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Overview, true);
+ Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Overview);
}
private async void LoadReleaseNotes_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs
index 19973a69bc..7f82ab4b97 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs
@@ -96,7 +96,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void NavigateCmdPalSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
- SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.CmdPal, true);
+ SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.CmdPal);
}
/*
diff --git a/tools/build/build-installer.ps1 b/tools/build/build-installer.ps1
index 257f70e6b6..a0e1f3c809 100644
--- a/tools/build/build-installer.ps1
+++ b/tools/build/build-installer.ps1
@@ -28,11 +28,8 @@ Runs the pipeline for x64 Release with machine-wide installer.
.NOTES
- Generated MSIX files will be signed using cert-sign-package.ps1.
-- This script uses git to manage workspace state:
- * Uncommitted changes are stashed before build and popped afterwards.
- * Version files and manifests modified during build are reverted.
- * Untracked generated files are cleaned up.
-- Use the -Clean parameter to clean build outputs (bin/obj) and ignored files.
+- If the working tree is not clean, the script will prompt before continuing (use -Force to skip the prompt).
+- Use the -Clean parameter to clean build outputs (bin/obj) and MSBuild outputs.
- The built installer will be placed under: installer/PowerToysSetupVNext/[Platform]/[Configuration]/User[Machine]Setup
relative to the solution root directory.
- To run the full installation in other machines, call "./cert-management.ps1" to export the cert used to sign the packages.
@@ -44,6 +41,7 @@ param (
[string]$Configuration = 'Release',
[string]$PerUser = 'true',
[string]$Version,
+ [switch]$Force,
[switch]$EnableCmdPalAOT,
[switch]$Clean,
[switch]$SkipBuild,
@@ -51,11 +49,12 @@ param (
)
if ($Help) {
- Write-Host "Usage: .\build-installer.ps1 [-Platform ] [-Configuration ] [-PerUser ] [-Version <0.0.1>] [-EnableCmdPalAOT] [-Clean] [-SkipBuild]"
+ Write-Host "Usage: .\build-installer.ps1 [-Platform ] [-Configuration ] [-PerUser ] [-Version <0.0.1>] [-Force] [-EnableCmdPalAOT] [-Clean] [-SkipBuild]"
Write-Host " -Platform Target platform (default: auto-detect or x64)"
Write-Host " -Configuration Build configuration (default: Release)"
Write-Host " -PerUser Build per-user installer (default: true)"
Write-Host " -Version Sets the PowerToys version (default: from src\Version.props)"
+ Write-Host " -Force Continue even if the git working tree is not clean (skips the interactive prompt)."
Write-Host " -EnableCmdPalAOT Enable AOT compilation for CmdPal (slower build)"
Write-Host " -Clean Clean output directories before building"
Write-Host " -SkipBuild Skip building the main solution and tools (assumes they are already built)"
@@ -103,6 +102,73 @@ if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot "PowerToys.slnx")))
Write-Host "PowerToys repository root detected: $repoRoot"
+# Safety check: avoid mixing build outputs with existing local changes unless the user confirms.
+if (-not $Force) {
+ Push-Location $repoRoot
+ try {
+ $gitStatus = $null
+ $gitRelevantStatus = @()
+ try {
+ $gitStatus = git status --porcelain=v1 --untracked-files=all --ignore-submodules=all
+ } catch {
+ Write-Warning ("[GIT] Failed to query git status: {0}" -f $_.Exception.Message)
+ }
+
+ if ($gitStatus -and $gitStatus.Length -gt 0) {
+ foreach ($line in $gitStatus) {
+ if (-not $line) { continue }
+
+ # Porcelain v1 format: XY
+ # We only care about changes that affect the working tree (Y != ' ') or untracked files (??).
+ # Index-only changes (staged, Y == ' ') are ignored per user request.
+ if ($line.StartsWith('??')) {
+ $gitRelevantStatus += $line
+ continue
+ }
+
+ if ($line.StartsWith('!!')) {
+ continue
+ }
+
+ if ($line.Length -ge 2) {
+ $workTreeStatus = $line[1]
+ if ($workTreeStatus -ne ' ') {
+ $gitRelevantStatus += $line
+ }
+ }
+ }
+ }
+
+ if ($gitRelevantStatus.Count -gt 0) {
+ Write-Warning "[GIT] Working tree is NOT clean."
+ Write-Warning "[GIT] This build will generate untracked files and may modify tracked files, which can mix with your current changes."
+ Write-Host "[GIT] Unstaged/untracked status (first 50 lines):"
+ $gitRelevantStatus | Select-Object -First 50 | ForEach-Object { Write-Host (" {0}" -f $_) }
+
+ $shouldContinue = $false
+ try {
+ $choices = [System.Management.Automation.Host.ChoiceDescription[]]@(
+ (New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Continue the build."),
+ (New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Cancel the build.")
+ )
+ $decision = $Host.UI.PromptForChoice("Working tree not clean", "Continue anyway?", $choices, 1)
+ $shouldContinue = ($decision -eq 0)
+ } catch {
+ Write-Warning "[GIT] Interactive prompt not available."
+ Write-Error "Refusing to proceed with a dirty working tree. Re-run with -Force to continue anyway."
+ exit 1
+ }
+
+ if (-not $shouldContinue) {
+ Write-Host "[GIT] Cancelled by user."
+ exit 1
+ }
+ }
+ } finally {
+ Pop-Location
+ }
+}
+
$cmdpalOutputPath = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\CmdPal"
$buildOutputPath = Join-Path $repoRoot "$Platform\$Configuration"
@@ -117,53 +183,10 @@ if ($Clean) {
Remove-Item $buildOutputPath -Recurse -Force -ErrorAction Ignore
}
- Write-Host "[CLEAN] Cleaning all build artifacts (git clean -Xfd)..."
- Push-Location $repoRoot
- try {
- git clean -Xfd | Out-Null
- } catch {
- Write-Warning "[CLEAN] git clean failed: $_"
- } finally {
- Pop-Location
- }
-
Write-Host "[CLEAN] Cleaning solution (msbuild /t:Clean)..."
RunMSBuild 'PowerToys.slnx' '/t:Clean' $Platform $Configuration
}
-# Git Stash Logic to handle workspace cleanup
-$stashedChanges = $false
-$scriptPathRelative = "tools/build/build-installer.ps1"
-
-# Calculate relative path of this script to exclude it from stash/reset
-$currentScriptPath = $MyInvocation.MyCommand.Definition
-if ($currentScriptPath.StartsWith($repoRoot)) {
- $scriptPathRelative = $currentScriptPath.Substring($repoRoot.Length).TrimStart('\', '/')
- $scriptPathRelative = $scriptPathRelative -replace '\\', '/'
-}
-
-Push-Location $repoRoot
-try {
- $gitStatus = git status --porcelain
- if ($gitStatus.Length -gt 0) {
- Write-Host "[GIT] Uncommitted changes detected. Stashing (excluding this script)..."
- $stashCountBefore = (git stash list).Count
-
- # Exclude the current script from stash so we don't revert it while running
- git stash push --include-untracked -m "PowerToys Build Auto-Stash" -- . ":(exclude)$scriptPathRelative"
-
- $stashCountAfter = (git stash list).Count
- if ($stashCountAfter -gt $stashCountBefore) {
- $stashedChanges = $true
- Write-Host "[GIT] Changes stashed."
- } else {
- Write-Host "[GIT] No changes to stash (likely only this script is modified)."
- }
- }
-} finally {
- Pop-Location
-}
-
try {
if ($Version) {
Write-Host "[VERSION] Setting PowerToys version to $Version using versionSetting.ps1..."
@@ -384,28 +407,7 @@ try {
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysBootstrapperVNext /p:PerUser=$PerUser" $Platform $Configuration
} finally {
- # Restore workspace state using Git
- Write-Host "[GIT] Cleaning up build artifacts..."
- Push-Location $repoRoot
- try {
- # Revert all changes EXCEPT the script itself
- # This cleans up Version.props, AppxManifests, etc.
- git checkout HEAD -- . ":(exclude)$scriptPathRelative"
-
- # Remove untracked files (generated manifests, etc.)
- # -f: force, -d: remove directories, -q: quiet
- git clean -fd -q
-
- if ($stashedChanges) {
- Write-Host "[GIT] Restoring stashed changes..."
- git stash pop --index
- if ($LASTEXITCODE -ne 0) {
- Write-Warning "[GIT] 'git stash pop' reported conflicts or errors. Your changes are in the stash list."
- }
- }
- } finally {
- Pop-Location
- }
+ # No git cleanup; leave workspace state as-is.
}
Write-Host '[PIPELINE] Completed'