Merge remote-tracking branch 'origin/main' into cmdpal-pt

This commit is contained in:
Kai Tao
2025-11-27 14:28:05 +08:00
38 changed files with 2877 additions and 598 deletions

240
README.md
View File

@@ -51,19 +51,20 @@ But to get started quickly, choose one of the installation methods below:
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-arm64.exe
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysUserSetup-0.96.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysUserSetup-0.96.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysSetup-0.96.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysSetup-0.96.0-arm64.exe
| Description | Filename |
|----------------|----------|
| Per user - x64 | [PowerToysUserSetup-0.95.1-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.95.1-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.95.1-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.95.1-arm64.exe][ptMachineArm64] |
| Per user - x64 | [PowerToysUserSetup-0.96.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.96.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.96.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.96.0-arm64.exe][ptMachineArm64] |
</details>
<details>
@@ -102,156 +103,131 @@ There are [community driven install methods](./doc/unofficialInstallMethods.md)
</details>
## ✨ What's new
**Version 0.95 (October 2025)**
**Version 0.96 (November 2025)**
For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
**✨ Highlights**
- **NEW:** The **Light Switch** utility in PowerToys allows you to automatically switch between light and dark themes in Windows based on the time of day.
- Command Palette delivered major search performance gains (new fuzzy matcher and smarter fallbacks) improving relevance and speed.
- Peek can now be activated using just the Spacebar!
- Find My Mouse added transparent spotlight with independent backdrop opacity, boosting focus and accessibility.
- Settings now lets you delete shortcuts entirely and ignore conflicts.
- Mouse Pointer Crosshairs gained orientation options (vertical / horizontal / both) for customizable accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
- PowerRename fixed enumeration counter skipping ensuring reliable batch renames. Thanks [@daverayment](https://github.com/daverayment)!
- ZoomIt restored legacy draw and snipping behaviors, and fixed recording issues, improving reliability. Thanks [@chakrik73](https://github.com/chakrik73)!
- Advanced Paste now supports multiple online and on-device AI model providers: Azure OpenAI, OpenAI, Google Gemini, Mistral, Foundry Local and Ollama.
- Command Palette received extensive improvements including file search filters, better clipboard history metadata, context-menu styling, and dozens of bug fixes and enhancements.
- PowerRename can now extract and use photo metadata (EXIF, XMP) in renaming patterns like `%Camera`, `%Lens`, and `%ExposureTime`.
### Advanced Paste
- Advanced Paste now lets you connect to multiple AI providers instead of being limited to a single OpenAI provider. See [Advanced Paste documentation](https://learn.microsoft.com/windows/powertoys/advanced-paste) for usage.
### Awake
- The Awake countdown timer now stays accurate over long periods. Thanks [@daverayment](https://github.com/daverayment)!
- Fixed Awake context menu positioning. The fix removed the conversion of the mouse cursor from screen to client-window coordinates, instead using the raw screen coordinates returned by GetCursorPos; the context menu now appears at the correct screen position. Thanks [@lzandman](https://github.com/lzandman)!
### Command Palette
- Applied conditional margin for icon-only tags to tighten layout. Thanks [@samrueby](https://github.com/samrueby)
- Improved the reliability of accessing Command Palette settings through PowerToys Settings and executing other x-cmdpal:// protocol commands. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Enabled AOT by default for improved performance while simplifying publish configs.
- Replaced service state color dots with play/pause/stop icons for enhanced accessibility. Thanks [@samrueby](https://github.com/samrueby)
- Fixed filter dropdown sync and crash by binding SelectedValue and raising UI-thread notifications. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Ensured long links wrap correctly in details view.
- Removed animation and enforced minimum width on filter dropdown for clarity. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Restored focus to More button after ESC closes context menu, improving keyboard flow. Thanks [@chatasweetie](https://github.com/chatasweetie)
- Marked main and toast windows as tool windows to keep them out of Alt+Tab while preserving style. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Fixed AOT template and theming issues for filter separators. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Introduced grid layouts (small, medium, gallery) for richer page presentation.
- Materialized result lists to avoid rescoring overhead.
- Disabled problematic selection TextToSuggest behind environment flag.
- Major search performance improvements (new fuzzy matcher, smarter fallbacks, fewer exceptions).
- Added context menu "Show Details" command when details pane is hidden.
- Reduced window flicker by avoiding unnecessary cloaking. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Restored EmptyContent rendering for blank states. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)
- Saved new state even if prior app state file was corrupt (better resilience). Thanks [@jiripolasek](https://github.com/jiripolasek)
- Migrated settings window to WinUI TitleBar control. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Prevented crash on duplicate keybindings and simplified matching. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Hotkeys now always respect the “Ignore shortcut in fullscreen” setting. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Hid search box on content pages, improving focus and accessibility, and added Home title. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Blocked Ctrl+I from inserting stray tabs in search box.
- Logged HRESULT codes in error logs for deeper diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Advanced font and emoji icon classification and alignment improvements. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Ensured that fallback command icons are visible on the extension settings page. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Fixed breadcrumb margin misalignment (visual polish). Thanks [@jiripolasek](https://github.com/jiripolasek)
- Truncated overly long command labels with ellipsis to prevent overflow.
- Added a setting to configure the page transition animation.
- Collection of small improvements and nits for Run Commands.
- Improved bookmarks performance and experience. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Added Ctrl+O shortcut in Clipboard History to open links directly.
- Resolved conflict with external software that blocked Command Palette from hiding.
- Updated context menu items to reflect name and icon changes, and ensured application icons are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Added Alt+Home shortcut to return immediately to the Command Palette home page. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Fixed a crash when displaying code blocks in markdown on detail or content pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Fixed an issue where the search bar icon and title were not updated when rapidly switching pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
- Improved the appearance of the search box in the context menu.
- The search field in context menus now matches the look of the Command Palette, with a smoke backdrop and improved padding.
- Fallback items such as math calculations or the Run command now appear in results more quickly. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Ensured the command bar updates correctly after navigating to another page and commands are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- The Command Palette settings page has been reorganized. Activation-key options are grouped under an expander and extension settings are framed for improved readability.
- When you modify a command, its alias, hotkey, and tags now update in the top-level list, keeping the displayed information in sync. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Press `Ctrl + ,` to open Command Palette settings from anywhere. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- You can use `Page Up` and `Page Down` to navigate the list while focus is in the search box. Thanks [@samrueby](https://github.com/samrueby)!
- Fixed an issue where the search box could disappear when navigating pages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Ensured search text is selected when *Go home when activated* and *Highlight search on activate* are both enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed an issue where Command Palette window occasionally appeared on the taskbar under certain Windows settings. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Ensured that labels and icons of list items and menu items update when they change. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed visibility of list filters when navigating to a content page. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
- Added search to the extension list and a link to extensions on the Microsoft Store. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added options to open the Command Palette window at its last position or re-center it.
- The Command Palette now remembers its window size after restarting.
- Added a global error handler that logs fatal errors and provides feedback when unexpected failures force Command Palette to close. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed forms and extension settings not showing on some machines due to a missing VC++ runtime.
- Restored ranking of fallback commands for built-in extensions (Sleep, Shutdown, Windows settings, Web search, etc.). Thanks [@jiripolasek](https://github.com/jiripolasek).
- Improved and unified labels and texts across the application!
- Maintainance: Resolved numerous build warnings in Command Palette projects; no user-visible impact. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Maintainance: Fixed a logging issue so exception messages are properly recorded instead of placeholder text, improving troubleshooting. Thanks [@jiripolasek](https://github.com/jiripolasek)!
### Command Palette Extensions
- Replaced localized WebSearch setting keys with stable literals and numeric history count. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Enabled advanced markdown tables and emphasis extensions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added setting to choose Clipboard History primary action (Paste vs Copy). Thanks [@jiripolasek](https://github.com/jiripolasek)
- Added actionable empty-state hints for File Search (search PC / open indexing settings). Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Ensured all WinGet extension assets copy reliably to output. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved Run command line parsing for paths with spaces; sped up related tests.
- Updated WebSearch extension icon set for enhanced clarity and contrast. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added Terminal profile sort order setting including MRU tracking. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added Uninstall Application command (UWP direct, Win32 via Settings). Thanks [@mKpwnz](https://github.com/mKpwnz)!
- Deferred WinGet details loading and added timing logs.
- Removed LINQ from All Apps extension for performance.
- Added standardized key chord system + shortcuts to File Search commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added Terminal channel filter & remembered selection option. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Enabled loading local/data/app images in markdown with sizing hints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added external extension reload via x-cmdpal://reload (configurable). Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Instant WebSearch history updates with in-memory store & events. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added keep-after-paste option and safe delete with confirmation for Clipboard History. Thanks [@jiripolasek](https://github.com/jiripolasek)!
### Environment Variables
- Replaced custom window chrome with WinUI TitleBar for cleaner, maintainable Environment Variables UI.
### File Locksmith
- Adopted WinUI TitleBar to simplify window chrome while preserving appearance.
- Bookmarks: Added hints about bookmark placeholders to the Add/Edit Bookmark form. — Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Bookmarks: Improved migration of bookmarks from older versions and fixed an issue where aliases or keyboard shortcuts could be lost after restart. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Clipboard history: Items shown in Command Palettes clipboard history now include helpful metadata. For example, image items show dimensions, text files show names and sizes, web links include page titles, and text entries display word counts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- File search: Added filter buttons to show *all items*, *files only*, or *folders only*. Selecting a filter adds `kind:folders` or `kind:not folders` to narrow results.
- System commands: Replaced the `:red_circle:` placeholder with an actual red-circle emoji so the correct icon appears in the UI. Thanks [@samrueby](https://github.com/samrueby)!
- WinGet: Search performance feels more responsive because typed input is now processed via a task queue rather than complex cancellation tokens!
- Window Walker: UWP apps no longer show a "not responding" label when suspended. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Window Walker: Now displays the actual icon of each window rather than using the process icon, improving recognition of PWAs and Python GUIs. Thanks [@Lee-WonJun](https://github.com/Lee-WonJun)!
- Windows Terminal profiles: Fixed a rare crash in the Windows Terminal extension when the `LOCALAPPDATA` environment variable was missing. The path is now retrieved via a reliable API. Thanks [@jiripolasek](https://github.com/jiripolasek)!
### Find My Mouse
- Added transparent spotlight support with separate backdrop opacity; migrated to Windows App SDK composition APIs.
- Activating Find My Mouse no longer makes the cursor change to the busy (hourglass) icon or steals focus from your active application.
### Hosts File Editor
- Migrated to native WinUI TitleBar for cleaner, maintainable window chrome.
- Added customizable backup settings allowing users to configure backup frequency, location, and auto-deletion policies. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
### Image Resizer
- Fixed settings consistency during batch resize operations by capturing settings once before processing. Thanks [@daverayment](https://github.com/daverayment)!
### Light Switch
- Introduced as a brand-new PowerToy module.
- Automatically switches between light and dark themes.
- Supports time-based scheduling or location-based sunrise/sunset switching.
- Supports using a keyboard shortcut to force a change.
- Supports filtering changes for Apps and/or System Theme.
- Introduced new UI to allow users to manually enter their latitude and longitude in Sunrise to Sunset mode.
- Refactored service with cleaner state management for stability.
- Removed logs from every tick, only logging key events to largely reduce log size.
### Mouse Pointer Crosshairs
- Added Esc key to cancel active gliding cursor sequence. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
- Added orientation option (vertical / horizontal / both) for crosshairs customization. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
- Enabled switching between Mouse Pointer Crosshairs and Gliding Cursor modes. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
### Mouse Without Borders
- Continued Common class refactor (part 5/7) by extracting clipboard and init/cleanup logic into focused classes. Thanks [@mikeclayton](https://github.com/mikeclayton)!
- Fix connection failures caused by conflicting MachineId across machines. Thanks [@noraa-junker](https://github.com/noraa-junker) for troubleshooting!
- Added horizontal scrolling support. Thanks [@MasonBergstrom](https://github.com/MasonBergstrom)!
### Peek
- Added the option to activate Peek with just the Spacebar.
- Fixed media files remaining locked after preview window closes. Thanks [@daverayment](https://github.com/daverayment)!
- Added a command-line interface for file previewing. See the [Peek documentation](https://learn.microsoft.com/windows/powertoys/peek) for usage. Thanks [@prochan2](https://github.com/prochan2)!
### PowerRename
- Fixed enumeration counter skipping when regex replacement equals original filename (counters now advance reliably). Thanks [@daverayment](https://github.com/daverayment)!
- PowerRename no longer crashes due to a missing resources file.
- Added photo metadata extraction support using EXIF and XMP for pattern-based renaming with camera info, GPS coordinates, and date taken. See [PowerRename Documentation](https://learn.microsoft.com/en-us/windows/powertoys/powerrename).
### Quick Accent
- Expanded Welsh layout with acute, grave, and dieresis variants for vowels (consistent ordering). Thanks [@PesBandi](https://github.com/PesBandi)!
### PowerToys Run
- Added retry logic with exponential backoff to handle DWM composition errors during theme changes. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Updated OneNote icons to reflect new Microsoft 365 design. Thanks [@trevorNgo](https://github.com/trevorNgo)!
### Registry Preview
- Migrated to native TitleBar and AppWindow APIs for cleaner window chrome.
### Quick Accent
- Added diameter symbol (⌀) for Shift+O in Special Characters mode, thanks to [@anselumjuju](https://github.com/anselumjuju)!
### Screen Ruler
- Fixed ARM64 crash by aligning cursor position structure to 8-byte boundary.
### Zoomit
- Smoothed out zoom-animation in ZoomIt by coalescing mouse-move and timer events, thanks to [@foxmsft](https://github.com/foxmsft)!
- Enabled GIF support for ZoomIt, thanks to [@MarioHewardt](https://github.com/MarioHewardt)!
- Fixed spelling mistakes, and refactored some literal strings to string constants, thanks to [@lzandman](https://github.com/lzandman)!
- Fixed inaccurate "actual size" screenshots in ZoomIt and resolves a GDI handle leak, improving capture fidelity and long-session stability. thanks to [@daverayment](https://github.com/daverayment)!
### Settings
- Added ability to ignore specific hotkey conflicts to reduce noise.
- Stopped creating backup directory during dry-run status checks (cleaner first-run).
- Standardized casing and localization for ZoomIt and modules header.
- Improved search results page accessibility and conditional module grouping.
- Fixed title bar overlapping issue at smaller window sizes.
- Refined shortcut control visual design with improved consistency and spacing.
- Added dashboard utilities sorting by name or status.
- Made update notification InfoBar in flyout clickable for direct navigation to update page.
- Expanded installation instructions by default in README.
- Improved accessibility for shortcut conflict button with static resource-based automation properties.
- Added ScrollViewer to Command Palette page in PowerToys Settings. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed module list glitches and Sort Status checkmark issue. Thanks [@daverayment](https://github.com/daverayment)!
### ZoomIt
- Updated resource file to reflect standalone v9.01 and current copyright year. Thanks [@foxmsft](https://github.com/foxmsft)!
- Restored legacy draw/snipping behaviors and fixed recording race conditions. Thanks [@chakrik73](https://github.com/chakrik73)!
- Added smooth image option for improved zoom quality using GDI+ for static zoom and Magnifier API for live zoom. Thanks [@markrussinovich](https://github.com/markrussinovich)!
### Documentation
- New Microsoft Learn documentation for the Light Switch module.
- New dev docs for the Light Switch module.
### Development (Area-Build & Area-Tests)
- Allowed debug launches to continue when modules fail to load, speeding developer iteration.
- Fixed spell checker dictionary entry (advapi) to eliminate false error.
- Added VS Code development guide and launch configs to streamline cross-editor workflows.
- Upgraded Windows App SDK and related dependencies to 1.8 for newer platform features.
- Rewrote YAML comment to resolve new spell checker forbidden pattern. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Corrected solution structure by returning misplaced Common project, reducing build confusion.
- Modernized build scripts with shared helpers and VS environment autodetection for simpler CLI builds.
- Standardized build scripts and platform detection to improve reliability and reuse.
- Added missing Command Palette version bump to align module release cadence.
- Added EXECUTEDEFAULT term to dictionary to prevent regression build failures. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Introduced nightly pre-warm pipeline and configurable MSBuild cache mode to improve CI performance.
- Resolved CI forbidden pattern spelling complaint to keep pipelines green.
- Added AI contributor instruction set to clarify code area expectations.
- Added accessibility IDs to settings and FancyZones toggles, stabilizing UI tests.
- Added automatic log collection on UI test failures to speed root cause analysis.
- Stabilized Mouse Utils tests by switching to AccessibilityId selectors.
- Added Screen Ruler UI test coverage to validate core measurement workflows.
### Development
- Fixed accessibility by associating controls with labels for screen readers.
- Added accessible name to Shortcut Conflicts button for screen readers.
- Excluded TitleBars from tab navigation across multiple utilities. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Migrated build infrastructure from Windows Server 2019 to Server 2022 with improved failure logging and predictable NuGet package paths.
- Configured build agents to use larger P: drive for release builds to address disk space constraints.
- Enhanced DSC v3 support by organizing resource manifests in a dedicated subfolder with PATH configuration.
- Reduced installer bundle size by 6-7MB through centralized Hybrid CRT configuration across all C++ projects.
- Updated .NET packages to version 9.0.10 for security fixes. Thanks [@snickler](https://github.com/snickler)!
- Fixed spell check dictionary entries for consistency.
- Restored accidentally deleted NuGet configuration file for Command Palette extensions.
- Fixed package identity build by updating AppxManifest entry points to use PowerShell Core.
- Optimized CI pipeline by replacing file copy operations with hard links and moves, reducing build time and disk usage by 10-15GB.
- Updated Copilot guidance and PR prompt workflow.
- Included high-volume bugs in issue template header. Thanks [@daverayment](https://github.com/daverayment)!
- Fixed incorrect HRESULT logging for inner exceptions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Introduced shared sparse package identity for PowerToys Win32 components to enable access to Windows platform APIs.
- Consolidated installer builds to produce both machine and user installers simultaneously, reducing build time and complexity.
- Migrated exclusively to WiX v5 installer infrastructure, removing legacy WiX v3 support.
- Temporarily removed PowerToys installer path from PATH environment variable to prevent application crashes.
- Added complete OCR UI test coverage with automated tests for activation, settings, language selection, and text extraction.
- Fixed test input for drive path normalization in bookmark resolver unit tests.
- Fixed Peek UI tests by restoring Ctrl+Space activation shortcut for test scenarios.
- Hided apps in PowerToys.SpareApps package from Start Menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
## 🛣️ Roadmap
We are planning some nice new features and improvements for the next releases a revamped Keyboard Manager UI, custom endpoint and local model support for Advanced Paste, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.96][github-next-release-work]!

View File

@@ -33,8 +33,11 @@ The **Light Switch** module lets users automatically transition between light an
> **Note:** Using the shortcut overrides the current schedule until the next transition event.
* **LightSwitchService**
Reads settings and applies theming. Runs a check every minute to ensure the state is correct.
* **LightSwitchService.cpp**
is the heart beat of the module. Controls ticking every minute and depending on user actions (manual override, settings changing, etc) triggers the state manager to perform the corresponding operation.
* **LightSwitchStateManager.cpp**
handles updating the state based on the signals sent by LightSwitchService.
* **SettingsXAML/LightSwitch**
Provides the settings UI for configuring schedules, syncing location, and customizing shortcuts.

View File

@@ -43,7 +43,8 @@
Description="PowerToys OCR Module"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
Square44x44Logo="Images\Square44x44Logo.png"
AppListEntry="none">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.SettingsUI" Executable="WinUI3Apps\PowerToys.Settings.exe" EntryPoint="Windows.FullTrustApplication">
@@ -52,7 +53,8 @@
Description="PowerToys Settings UI"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
Square44x44Logo="Images\Square44x44Logo.png"
AppListEntry="none">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.ImageResizerUI" Executable="WinUI3Apps\PowerToys.ImageResizer.exe" EntryPoint="Windows.FullTrustApplication">
@@ -61,7 +63,8 @@
Description="PowerToys Image Resizer UI"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
Square44x44Logo="Images\Square44x44Logo.png"
AppListEntry="none">
</uap:VisualElements>
</Application>
<!-- Temporarily disabled: PowerToys Command Palette Extension now packaged with its own manifest -->

View File

@@ -10,6 +10,23 @@ namespace LanguageModelProvider.FoundryLocal;
internal sealed class FoundryClient
{
public static async Task<FoundryClient?> CreateAsync()
{
// First attempt with current environment
var client = await TryCreateClientAsync().ConfigureAwait(false);
if (client != null)
{
return client;
}
// If failed, refresh PATH from registry and retry once
// This handles cases where PowerToys was launched by MSI installer.
Logger.LogInfo("[FoundryClient] First attempt failed, refreshing PATH and retrying");
RefreshEnvironmentPath();
return await TryCreateClientAsync().ConfigureAwait(false);
}
private static async Task<FoundryClient?> TryCreateClientAsync()
{
try
{
@@ -195,4 +212,68 @@ internal sealed class FoundryClient
await _foundryManager.StartServiceAsync();
}
}
/// <summary>
/// Refreshes the PATH environment variable from the system registry.
/// This is necessary when tools are installed while PowerToys is running,
/// as the installer updates the system PATH but running processes don't see the change.
/// </summary>
private static void RefreshEnvironmentPath()
{
try
{
Logger.LogInfo("[FoundryClient] Refreshing PATH environment variable from system");
var currentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process) ?? string.Empty;
var machinePath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? string.Empty;
var userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty;
var pathsToAdd = new List<string>();
if (!string.IsNullOrWhiteSpace(currentPath))
{
pathsToAdd.AddRange(currentPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries));
}
if (!string.IsNullOrWhiteSpace(userPath))
{
var userPaths = userPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
foreach (var path in userPaths)
{
if (!pathsToAdd.Contains(path, StringComparer.OrdinalIgnoreCase))
{
pathsToAdd.Add(path);
}
}
}
if (!string.IsNullOrWhiteSpace(machinePath))
{
var machinePaths = machinePath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
foreach (var path in machinePaths)
{
if (!pathsToAdd.Contains(path, StringComparer.OrdinalIgnoreCase))
{
pathsToAdd.Add(path);
}
}
}
var newPath = string.Join(Path.PathSeparator.ToString(), pathsToAdd);
if (currentPath != newPath)
{
Logger.LogInfo("[FoundryClient] Updating process PATH with latest system values");
Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.Process);
}
else
{
Logger.LogInfo("[FoundryClient] PATH is already up to date");
}
}
catch (Exception ex)
{
Logger.LogError($"[FoundryClient] Failed to refresh PATH: {ex.Message}");
}
}
}

View File

@@ -12,9 +12,8 @@ namespace LanguageModelProvider;
public sealed class FoundryLocalModelProvider : ILanguageModelProvider
{
private IEnumerable<ModelDetails>? _downloadedModels;
private IEnumerable<FoundryCatalogModel>? _catalogModels;
private FoundryClient? _foundryClient;
private IEnumerable<FoundryCatalogModel>? _catalogModels;
private string? _serviceUrl;
public static FoundryLocalModelProvider Instance { get; } = new();
@@ -43,15 +42,6 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
throw new InvalidOperationException(errorMessage);
}
// Check if model is cached
var isInCache = _downloadedModels?.Any(m => m.ProviderModelDetails is FoundryCachedModel cached && cached.Name == modelId) ?? false;
if (!isInCache)
{
var errorMessage = $"The requested model '{modelId}' is not cached. Please download it using Foundry Local.";
Logger.LogError($"[FoundryLocal] {errorMessage}");
throw new InvalidOperationException(errorMessage);
}
// Ensure the model is loaded before returning chat client
var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
if (!isLoaded)
@@ -74,7 +64,7 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
return new OpenAIClient(
new ApiKeyCredential("none"),
new OpenAIClientOptions { Endpoint = endpointUri })
new OpenAIClientOptions { Endpoint = endpointUri, NetworkTimeout = TimeSpan.FromMinutes(5) })
.GetChatClient(modelId)
.AsIChatClient();
}
@@ -100,30 +90,38 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{_serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()";
}
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default)
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(CancellationToken cancelationToken = default)
{
if (ignoreCached)
{
Logger.LogInfo("[FoundryLocal] Ignoring cached models, resetting");
Reset();
}
await InitializeAsync(cancelationToken);
Logger.LogInfo($"[FoundryLocal] Returning {_downloadedModels?.Count() ?? 0} downloaded models");
return _downloadedModels ?? [];
}
if (_foundryClient == null)
{
return Array.Empty<ModelDetails>();
}
private void Reset()
{
_downloadedModels = null;
_catalogModels = null;
_ = InitializeAsync();
var cachedModels = await _foundryClient.ListCachedModels();
List<ModelDetails> downloadedModels = [];
foreach (var model in cachedModels)
{
Logger.LogInfo($"[FoundryLocal] Adding unmatched cached model: {model.Name}");
downloadedModels.Add(new ModelDetails
{
Id = $"fl-{model.Name}",
Name = model.Name,
Url = $"fl://{model.Name}",
Description = $"{model.Name} running locally with Foundry Local",
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
ProviderModelDetails = model,
});
}
return downloadedModels;
}
private async Task InitializeAsync(CancellationToken cancelationToken = default)
{
if (_foundryClient != null && _downloadedModels != null && _downloadedModels.Any() && _catalogModels != null && _catalogModels.Any())
if (_foundryClient != null && _catalogModels != null && _catalogModels.Any())
{
await _foundryClient.EnsureRunning().ConfigureAwait(false);
return;
@@ -145,29 +143,6 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
var catalogModels = await _foundryClient.ListCatalogModels();
Logger.LogInfo($"[FoundryLocal] Found {catalogModels.Count} catalog models");
_catalogModels = catalogModels;
var cachedModels = await _foundryClient.ListCachedModels();
Logger.LogInfo($"[FoundryLocal] Found {cachedModels.Count} cached models");
List<ModelDetails> downloadedModels = [];
foreach (var model in cachedModels)
{
Logger.LogInfo($"[FoundryLocal] Adding unmatched cached model: {model.Name}");
downloadedModels.Add(new ModelDetails
{
Id = $"fl-{model.Name}",
Name = model.Name,
Url = $"fl://{model.Name}",
Description = $"{model.Name} running locally with Foundry Local",
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
SupportedOnQualcomm = true,
ProviderModelDetails = model,
});
}
_downloadedModels = downloadedModels;
Logger.LogInfo($"[FoundryLocal] Initialization complete. Total downloaded models: {downloadedModels.Count}");
}
public async Task<bool> IsAvailable()

View File

@@ -12,7 +12,7 @@ public interface ILanguageModelProvider
string ProviderDescription { get; }
Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default);
Task<IEnumerable<ModelDetails>> GetModelsAsync(CancellationToken cancelationToken = default);
IChatClient? GetIChatClient(string modelId);

View File

@@ -24,8 +24,6 @@ public class ModelDetails
public List<HardwareAccelerator> HardwareAccelerators { get; set; } = [];
public bool SupportedOnQualcomm { get; set; }
public string License { get; set; } = string.Empty;
public object? ProviderModelDetails { get; set; }

View File

@@ -299,47 +299,49 @@
</StackPanel>
</controls:PromptBox.Footer>
</controls:PromptBox>
<Grid Grid.Row="2" RowSpacing="4">
<Grid.RowDefinitions>
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsToMinHeightConverter}}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListView
x:Name="PasteOptionsListView"
Grid.Row="0"
VerticalAlignment="Bottom"
IsItemClickEnabled="True"
ItemClick="PasteFormat_ItemClick"
ItemContainerTransitions="{x:Null}"
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Auto"
SelectionMode="None"
TabIndex="1" />
<Rectangle
Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
Visibility="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
<ScrollViewer Grid.Row="2">
<Grid RowSpacing="4">
<Grid.RowDefinitions>
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsToMinHeightConverter}}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListView
x:Name="PasteOptionsListView"
Grid.Row="0"
VerticalAlignment="Bottom"
IsItemClickEnabled="True"
ItemClick="PasteFormat_ItemClick"
ItemContainerTransitions="{x:Null}"
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollMode="Disabled"
SelectionMode="None"
TabIndex="1" />
<Rectangle
Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
Visibility="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
<ListView
x:Name="CustomActionsListView"
Grid.Row="2"
VerticalAlignment="Top"
IsItemClickEnabled="True"
ItemClick="PasteFormat_ItemClick"
ItemContainerTransitions="{x:Null}"
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Auto"
SelectionMode="None"
TabIndex="2" />
</Grid>
<ListView
x:Name="CustomActionsListView"
Grid.Row="2"
VerticalAlignment="Top"
IsItemClickEnabled="True"
ItemClick="PasteFormat_ItemClick"
ItemContainerTransitions="{x:Null}"
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollMode="Disabled"
SelectionMode="None"
TabIndex="2" />
</Grid>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -215,7 +215,6 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
return new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
Temperature = 0.01,
};
}
}

View File

@@ -146,6 +146,7 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
var options = new ChatOptions
{
ModelId = modelReference,
MaxOutputTokens = 2048,
};
if (!string.IsNullOrWhiteSpace(systemPrompt))

View File

@@ -157,8 +157,6 @@ namespace AdvancedPaste.Services.CustomActions
{
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
{
Temperature = 0.01,
MaxTokens = 2000,
FunctionChoiceBehavior = null,
},
_ => new PromptExecutionSettings(),

View File

@@ -121,7 +121,7 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
BEGIN
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
LTEXT "ZoomIt v9.20",IDC_VERSION,42,7,73,10
LTEXT "ZoomIt v9.21",IDC_VERSION,42,7,73,10
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
"SysLink",WS_TABSTOP,42,26,150,9

View File

@@ -350,7 +350,7 @@ namespace Awake.Core
TrayHelper.TimedIcon,
TrayIconAction.Update);
},
_ => HandleTimerCompletion("timed"),
() => HandleTimerCompletion("timed"),
_tokenSource.Token);
}

View File

@@ -2,21 +2,52 @@
// 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 System.Text;
using System.Threading.Tasks;
using Windows.Graphics;
namespace Microsoft.CmdPal.UI.ViewModels;
public sealed class WindowPosition
{
/// <summary>
/// Gets or sets left position in device pixels.
/// </summary>
public int X { get; set; }
/// <summary>
/// Gets or sets top position in device pixels.
/// </summary>
public int Y { get; set; }
/// <summary>
/// Gets or sets width in device pixels.
/// </summary>
public int Width { get; set; }
/// <summary>
/// Gets or sets height in device pixels.
/// </summary>
public int Height { get; set; }
/// <summary>
/// Gets or sets width of the screen in device pixels where the window is located.
/// </summary>
public int ScreenWidth { get; set; }
/// <summary>
/// Gets or sets height of the screen in device pixels where the window is located.
/// </summary>
public int ScreenHeight { get; set; }
/// <summary>
/// Gets or sets DPI (dots per inch) of the display where the window is located.
/// </summary>
public int Dpi { get; set; }
/// <summary>
/// Converts the window position properties to a <see cref="RectInt32"/> structure representing the physical window rectangle.
/// </summary>
public RectInt32 ToPhysicalWindowRectangle()
{
return new RectInt32(X, Y, Width, Height);
}
}

View File

@@ -18,6 +18,7 @@ using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Input;
@@ -33,6 +34,8 @@ using Windows.UI.WindowManagement;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.HiDpi;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging;
using WinRT;
@@ -48,6 +51,9 @@ public sealed partial class MainWindow : WindowEx,
IRecipient<QuitMessage>,
IDisposable
{
private const int DefaultWidth = 800;
private const int DefaultHeight = 480;
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
private readonly uint WM_TASKBAR_RESTART;
@@ -173,22 +179,8 @@ public sealed partial class MainWindow : WindowEx,
return;
}
AppWindow.Resize(new SizeInt32 { Width = savedPosition.Width, Height = savedPosition.Height });
var savedRect = new RectInt32(savedPosition.X, savedPosition.Y, savedPosition.Width, savedPosition.Height);
var displayArea = DisplayArea.GetFromRect(savedRect, DisplayAreaFallback.Nearest);
var workArea = displayArea.WorkArea;
var maxX = workArea.X + Math.Max(0, workArea.Width - savedPosition.Width);
var maxY = workArea.Y + Math.Max(0, workArea.Height - savedPosition.Height);
var targetPoint = new PointInt32
{
X = Math.Clamp(savedPosition.X, workArea.X, maxX),
Y = Math.Clamp(savedPosition.Y, workArea.Y, maxY),
};
AppWindow.Move(targetPoint);
var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi);
AppWindow.MoveAndResize(newRect);
}
private void PositionCentered(DisplayArea displayArea)
@@ -207,12 +199,16 @@ public sealed partial class MainWindow : WindowEx,
private void UpdateWindowPositionInMemory()
{
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
_currentWindowPosition = new WindowPosition
{
X = AppWindow.Position.X,
Y = AppWindow.Position.Y,
Width = AppWindow.Size.Width,
Height = AppWindow.Size.Height,
Dpi = (int)this.GetDpiForWindow(),
ScreenWidth = displayArea.WorkArea.Width,
ScreenHeight = displayArea.WorkArea.Height,
};
}
@@ -300,8 +296,8 @@ public sealed partial class MainWindow : WindowEx,
if (target == MonitorBehavior.ToLast)
{
AppWindow.Resize(new SizeInt32 { Width = _currentWindowPosition.Width, Height = _currentWindowPosition.Height });
AppWindow.Move(new PointInt32 { X = _currentWindowPosition.X, Y = _currentWindowPosition.Y });
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
AppWindow.MoveAndResize(newRect);
}
else
{
@@ -330,6 +326,114 @@ public sealed partial class MainWindow : WindowEx,
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
}
/// <summary>
/// Ensures that the window rectangle is visible on-screen.
/// </summary>
/// <param name="windowRect">The window rectangle in physical pixels.</param>
/// <param name="originalScreen">The desktop area the window was positioned on.</param>
/// <param name="originalDpi">The window's original DPI.</param>
/// <returns>
/// A window rectangle in physical pixels, moved to the nearest display and resized
/// if the DPI has changed.
/// </returns>
private static RectInt32 EnsureWindowIsVisible(RectInt32 windowRect, SizeInt32 originalScreen, int originalDpi)
{
var displayArea = DisplayArea.GetFromRect(windowRect, DisplayAreaFallback.Nearest);
if (displayArea is null)
{
return windowRect;
}
var workArea = displayArea.WorkArea;
if (workArea.Width <= 0 || workArea.Height <= 0)
{
// Fallback, nothing reasonable to do
return windowRect;
}
var effectiveDpi = GetEffectiveDpiFromDisplayId(displayArea);
if (originalDpi <= 0)
{
originalDpi = effectiveDpi; // use current DPI as baseline (no scaling adjustment needed)
}
var hasInvalidSize = windowRect.Width <= 0 || windowRect.Height <= 0;
if (hasInvalidSize)
{
windowRect = new RectInt32(windowRect.X, windowRect.Y, DefaultWidth, DefaultHeight);
}
// If we have a DPI change, scale the window rectangle accordingly
if (effectiveDpi != originalDpi)
{
var scalingFactor = effectiveDpi / (double)originalDpi;
windowRect = new RectInt32(
(int)Math.Round(windowRect.X * scalingFactor),
(int)Math.Round(windowRect.Y * scalingFactor),
(int)Math.Round(windowRect.Width * scalingFactor),
(int)Math.Round(windowRect.Height * scalingFactor));
}
var targetWidth = Math.Min(windowRect.Width, workArea.Width);
var targetHeight = Math.Min(windowRect.Height, workArea.Height);
// Ensure at least some minimum visible area (e.g., 100 pixels)
// This helps prevent the window from being entirely offscreen, regardless of display scaling.
const int minimumVisibleSize = 100;
var isOffscreen =
windowRect.X + minimumVisibleSize > workArea.X + workArea.Width ||
windowRect.X + windowRect.Width - minimumVisibleSize < workArea.X ||
windowRect.Y + minimumVisibleSize > workArea.Y + workArea.Height ||
windowRect.Y + windowRect.Height - minimumVisibleSize < workArea.Y;
// if the work area size has changed, re-center the window
var workAreaSizeChanged =
originalScreen.Width != workArea.Width ||
originalScreen.Height != workArea.Height;
int targetX;
int targetY;
var recenter = isOffscreen || workAreaSizeChanged || hasInvalidSize;
if (recenter)
{
targetX = workArea.X + ((workArea.Width - targetWidth) / 2);
targetY = workArea.Y + ((workArea.Height - targetHeight) / 2);
}
else
{
targetX = windowRect.X;
targetY = windowRect.Y;
}
return new RectInt32(targetX, targetY, targetWidth, targetHeight);
}
private static int GetEffectiveDpiFromDisplayId(DisplayArea displayArea)
{
var effectiveDpi = 96;
var hMonitor = (HMONITOR)Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId);
if (!hMonitor.IsNull)
{
var hr = PInvoke.GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _);
if (hr == 0)
{
effectiveDpi = (int)dpiX;
}
else
{
Logger.LogWarning($"GetDpiForMonitor failed with HRESULT: 0x{hr.Value:X8} on display {displayArea.DisplayId}");
}
}
if (effectiveDpi <= 0)
{
effectiveDpi = 96;
}
return effectiveDpi;
}
private DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
{
// Leaving a note here, in case we ever need it:
@@ -479,6 +583,9 @@ public sealed partial class MainWindow : WindowEx,
Y = _currentWindowPosition.Y,
Width = _currentWindowPosition.Width,
Height = _currentWindowPosition.Height,
Dpi = _currentWindowPosition.Dpi,
ScreenWidth = _currentWindowPosition.ScreenWidth,
ScreenHeight = _currentWindowPosition.ScreenHeight,
};
SettingsModel.SaveSettings(settings);

View File

@@ -5,6 +5,11 @@
"nativeDebugging": false,
"doNotLaunchApp": false
},
"Microsoft.CmdPal.UI (Package) + Native debugging": {
"commandName": "MsixPackage",
"nativeDebugging": true,
"doNotLaunchApp": false
},
"Microsoft.CmdPal.UI (Unpackaged)": {
"commandName": "Project"
}

View File

@@ -24,13 +24,13 @@
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<!-- <PropertyGroup>
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(CIBuild)'=='true'">
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
</PropertyGroup>
</PropertyGroup> -->
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">

View File

@@ -19,7 +19,7 @@ public static class AIServiceTypeRegistry
{
ServiceType = AIServiceType.AzureAIInference,
DisplayName = "Azure AI Inference",
IconPath = "ms-appx:///Assets/Settings/Icons/Models/FoundryLocal.svg", // No icon for Azure AI Inference, use Foundry Local temporarily
IconPath = "ms-appx:///Assets/Settings/Icons/Models/Azure.svg",
IsOnlineService = true,
LegalDescription = "AdvancedPaste_AzureAIInference_LegalDescription",
TermsLabel = "AdvancedPaste_AzureAIInference_TermsLabel",

View File

@@ -1,59 +1,34 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2092_1741)">
<mask id="mask0_2092_1741" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
<path d="M16.5 0H0.5V16H16.5V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_2092_1741)">
<mask id="mask1_2092_1741" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="-1" y="-2" width="19" height="20">
<path d="M17.8337 -1.33337H-0.833008V17.3333H17.8337V-1.33337Z" fill="white"/>
</mask>
<g mask="url(#mask1_2092_1741)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1137 0.315668C11.57 0.315668 11.9744 0.657891 12.1196 1.15567C12.2648 1.65345 13.1152 4.73345 13.1152 4.73345V10.852H10.0352L10.0974 0.305298H11.1137V0.315668Z" fill="url(#paint0_linear_2092_1741)"/>
<path d="M15.6352 5.09586C15.6352 4.87808 15.4589 4.71216 15.2515 4.71216H13.4366C12.1611 4.71216 11.124 5.7492 11.124 7.02472V10.8618H13.3226C14.5982 10.8618 15.6352 9.82472 15.6352 8.54919V5.09586Z" fill="url(#paint1_linear_2092_1741)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1133 0.315674C10.7607 0.315674 10.4807 0.595674 10.4807 0.948265L10.4185 12.5942C10.4185 14.2949 9.0392 15.6742 7.33847 15.6742H1.74885C1.47921 15.6742 1.30292 15.4149 1.38589 15.1661L5.86589 2.37938C6.30144 1.14531 7.46293 0.315674 8.7696 0.315674H11.1237H11.1133Z" fill="url(#paint2_linear_2092_1741)"/>
</g>
</g>
</g>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.2663 0H0.231885C0.103572 0 0 0.10358 0 0.231885V2.55072C0 2.67904 0.103572 2.78261 0.231885 2.78261H12.5217C12.9059 2.78261 13.2174 3.09411 13.2174 3.47826V3.18995C13.2174 1.53971 11.9861 0 10.2663 0Z" fill="url(#paint0_linear_178_3940)"/>
<path d="M12.2334 0.81543C12.8633 1.44693 13.2174 2.29872 13.2174 3.19069V15.7689C13.2174 15.8972 13.3209 16.0007 13.4492 16.0007H15.7681C15.8964 16.0007 16 15.8972 16 15.7689V5.73524C16 4.99707 15.707 4.28983 15.1853 3.76732L12.2334 0.81543Z" fill="url(#paint1_linear_178_3940)"/>
<path d="M6.78804 3.47852H0.231885C0.103572 3.47852 0 3.58209 0 3.71039V6.02921C0 6.1575 0.103572 6.26112 0.231885 6.26112H9.04346C9.42759 6.26112 9.7391 6.57263 9.7391 6.95676V6.6685C9.7391 5.01822 8.50778 3.47852 6.78804 3.47852Z" fill="url(#paint2_linear_178_3940)"/>
<path d="M8.75537 4.29297C9.38531 4.92446 9.73928 5.77628 9.73928 6.6682V15.7681C9.73928 15.8964 9.8429 16 9.97119 16H12.29C12.4183 16 12.5219 15.8964 12.5219 15.7681V9.21281C12.5219 8.47462 12.229 7.76735 11.7072 7.24482L8.75537 4.29297Z" fill="url(#paint3_linear_178_3940)"/>
<path d="M3.30975 6.95703H0.231885C0.103572 6.95703 0 7.06056 0 7.18886V9.50771C0 9.63609 0.103572 9.73962 0.231885 9.73962H5.56521C5.94936 9.73962 6.26087 10.0511 6.26087 10.4353V10.147C6.26087 8.49675 5.02956 6.95703 3.30975 6.95703Z" fill="url(#paint4_linear_178_3940)"/>
<path d="M5.27686 7.77148C5.90677 8.40302 6.26083 9.25477 6.26083 10.1468V15.7684C6.26083 15.8967 6.36436 16.0003 6.49274 16.0003H8.8115C8.93988 16.0003 9.04341 15.8967 9.04341 15.7684V12.6913C9.04341 11.9531 8.75051 11.2459 8.22874 10.7234L5.27686 7.77148Z" fill="url(#paint5_linear_178_3940)"/>
<defs>
<linearGradient id="paint0_linear_2092_1741" x1="12.3996" y1="11.0801" x2="9.80702" y2="0.699373" gradientUnits="userSpaceOnUse">
<stop stop-color="#712575"/>
<stop offset="0.09" stop-color="#9A2884"/>
<stop offset="0.18" stop-color="#BF2C92"/>
<stop offset="0.27" stop-color="#DA2E9C"/>
<stop offset="0.34" stop-color="#EB30A2"/>
<stop offset="0.4" stop-color="#F131A5"/>
<stop offset="0.5" stop-color="#EC30A3"/>
<stop offset="0.61" stop-color="#DF2F9E"/>
<stop offset="0.72" stop-color="#C92D96"/>
<stop offset="0.83" stop-color="#AA2A8A"/>
<stop offset="0.95" stop-color="#83267C"/>
<stop offset="1" stop-color="#712575"/>
<linearGradient id="paint0_linear_178_3940" x1="13.2174" y1="3.15349" x2="0" y2="3.15349" gradientUnits="userSpaceOnUse">
<stop stop-color="#2C08AC"/>
<stop offset="0.8" stop-color="#4F42FD"/>
</linearGradient>
<linearGradient id="paint1_linear_2092_1741" x1="13.3848" y1="0.532897" x2="13.3848" y2="15.1759" gradientUnits="userSpaceOnUse">
<stop stop-color="#DA7ED0"/>
<stop offset="0.08" stop-color="#B17BD5"/>
<stop offset="0.19" stop-color="#8778DB"/>
<stop offset="0.3" stop-color="#6276E1"/>
<stop offset="0.41" stop-color="#4574E5"/>
<stop offset="0.54" stop-color="#2E72E8"/>
<stop offset="0.67" stop-color="#1D71EB"/>
<stop offset="0.81" stop-color="#1471EC"/>
<stop offset="1" stop-color="#1171ED"/>
<linearGradient id="paint1_linear_178_3940" x1="14.2303" y1="0.81543" x2="23.44" y2="11.9747" gradientUnits="userSpaceOnUse">
<stop offset="0.3" stop-color="#7274FF"/>
<stop offset="1" stop-color="#4F42FD"/>
</linearGradient>
<linearGradient id="paint2_linear_2092_1741" x1="12.5029" y1="0.865306" x2="2.79625" y2="16.4313" gradientUnits="userSpaceOnUse">
<stop stop-color="#DA7ED0"/>
<stop offset="0.05" stop-color="#B77BD4"/>
<stop offset="0.11" stop-color="#9079DA"/>
<stop offset="0.18" stop-color="#6E77DF"/>
<stop offset="0.25" stop-color="#5175E3"/>
<stop offset="0.33" stop-color="#3973E7"/>
<stop offset="0.42" stop-color="#2772E9"/>
<stop offset="0.54" stop-color="#1A71EB"/>
<stop offset="0.68" stop-color="#1371EC"/>
<stop offset="1" stop-color="#1171ED"/>
<linearGradient id="paint2_linear_178_3940" x1="9.7391" y1="6.63202" x2="0" y2="6.63202" gradientUnits="userSpaceOnUse">
<stop stop-color="#2C08AC"/>
<stop offset="0.8" stop-color="#4F42FD"/>
</linearGradient>
<linearGradient id="paint3_linear_178_3940" x1="10.7523" y1="4.29297" x2="17.3026" y2="14.5881" gradientUnits="userSpaceOnUse">
<stop offset="0.3" stop-color="#7274FF"/>
<stop offset="1" stop-color="#4F42FD"/>
</linearGradient>
<linearGradient id="paint4_linear_178_3940" x1="6.26087" y1="9.91172" x2="0" y2="9.91172" gradientUnits="userSpaceOnUse">
<stop stop-color="#2C08AC"/>
<stop offset="0.8" stop-color="#4F42FD"/>
</linearGradient>
<linearGradient id="paint5_linear_178_3940" x1="7.2738" y1="7.77148" x2="11.0624" y2="16.243" gradientUnits="userSpaceOnUse">
<stop offset="0.3" stop-color="#7274FF"/>
<stop offset="1" stop-color="#4F42FD"/>
</linearGradient>
<clipPath id="clip0_2092_1741">
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -87,12 +87,12 @@
FontSize="24"
Glyph="&#xF158;" />
<TextBlock
x:Uid="AdvancedPaste_FL_NoModelsDownloaded."
x:Uid="AdvancedPaste_FL_NoModelsDownloaded"
HorizontalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}"
TextAlignment="Center" />
<TextBlock
x:Uid="AdvancedPaste_FL_RunFoundryLocalText.Text"
x:Uid="AdvancedPaste_FL_RunFoundryLocalText"
HorizontalAlignment="Center"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"

View File

@@ -30,7 +30,7 @@ public sealed partial class FoundryLocalModelPicker : UserControl
public delegate void DownloadRequestedEventHandler(object sender, object payload);
public delegate void LoadRequestedEventHandler(object sender, FoundryLoadRequestedEventArgs args);
public delegate void LoadRequestedEventHandler(object sender);
public event ModelSelectionChangedEventHandler SelectionChanged;
@@ -94,7 +94,7 @@ public sealed partial class FoundryLocalModelPicker : UserControl
public bool HasDownloadableModels => DownloadableModels?.Cast<object>().Any() ?? false;
public void RequestLoad(bool refresh)
public void RequestLoad()
{
if (IsLoading)
{
@@ -107,7 +107,7 @@ public sealed partial class FoundryLocalModelPicker : UserControl
IsAvailable = false;
StatusText = "Loading Foundry Local status...";
LoadRequested?.Invoke(this, new FoundryLoadRequestedEventArgs(refresh));
LoadRequested?.Invoke(this);
}
private static void OnCachedModelsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -310,7 +310,7 @@ public sealed partial class FoundryLocalModelPicker : UserControl
private void RefreshModelsButton_Click(object sender, RoutedEventArgs e)
{
RequestLoad(refresh: true);
RequestLoad();
}
private void UpdateVisualStates()
@@ -444,14 +444,4 @@ public sealed partial class FoundryLocalModelPicker : UserControl
{
return string.IsNullOrWhiteSpace(license) ? Visibility.Collapsed : Visibility.Visible;
}
public sealed class FoundryLoadRequestedEventArgs : EventArgs
{
public FoundryLoadRequestedEventArgs(bool refresh)
{
Refresh = refresh;
}
public bool Refresh { get; }
}
}

View File

@@ -82,7 +82,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
{
ViewModel.RefreshEnabledState();
UpdatePasteAIUIVisibility();
_ = UpdateFoundryLocalUIAsync(refreshFoundry: true);
_ = UpdateFoundryLocalUIAsync();
}
private void EnableAdvancedPasteAI() => ViewModel.EnableAI();
@@ -384,7 +384,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
}
private Task UpdateFoundryLocalUIAsync(bool refreshFoundry = false)
private Task UpdateFoundryLocalUIAsync()
{
string selectedType = ViewModel?.PasteAIProviderDraft?.ServiceType ?? string.Empty;
bool isFoundryLocal = string.Equals(selectedType, "FoundryLocal", StringComparison.OrdinalIgnoreCase);
@@ -419,12 +419,12 @@ namespace Microsoft.PowerToys.Settings.UI.Views
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
}
FoundryLocalPicker?.RequestLoad(refreshFoundry);
FoundryLocalPicker?.RequestLoad();
return Task.CompletedTask;
}
private async Task LoadFoundryLocalModelsAsync(bool refresh = false)
private async Task LoadFoundryLocalModelsAsync()
{
if (FoundryLocalPanel is null)
{
@@ -456,9 +456,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return;
}
IEnumerable<ModelDetails> cachedModelsEnumerable = refresh
? await provider.GetModelsAsync(ignoreCached: true, cancelationToken: cancellationToken)
: await provider.GetModelsAsync(cancelationToken: cancellationToken);
IEnumerable<ModelDetails> cachedModelsEnumerable = await provider.GetModelsAsync(cancelationToken: cancellationToken).ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested)
{
@@ -467,9 +465,12 @@ namespace Microsoft.PowerToys.Settings.UI.Views
var cachedModels = cachedModelsEnumerable?.ToList() ?? new List<ModelDetails>();
UpdateFoundryCollections(cachedModels);
ShowFoundryAvailableState();
RestoreFoundrySelection(cachedModels);
DispatcherQueue.TryEnqueue(() =>
{
UpdateFoundryCollections(cachedModels);
ShowFoundryAvailableState();
RestoreFoundrySelection(cachedModels);
});
}
catch (OperationCanceledException)
{
@@ -478,12 +479,18 @@ namespace Microsoft.PowerToys.Settings.UI.Views
catch (Exception ex)
{
var errorMessage = $"Unable to load Foundry Local models. {ex.Message}";
ShowFoundryUnavailableState(errorMessage);
System.Diagnostics.Debug.WriteLine($"[AdvancedPastePage] Failed to load Foundry Local models: {ex}");
DispatcherQueue.TryEnqueue(() =>
{
ShowFoundryUnavailableState(errorMessage);
});
}
finally
{
UpdateFoundrySaveButtonState();
DispatcherQueue.TryEnqueue(() =>
{
UpdateFoundrySaveButtonState();
});
}
}
@@ -672,9 +679,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
UpdateFoundrySaveButtonState();
}
private async void FoundryLocalPicker_LoadRequested(object sender, FoundryLocalModelPicker.FoundryLoadRequestedEventArgs args)
private async void FoundryLocalPicker_LoadRequested(object sender)
{
await LoadFoundryLocalModelsAsync(args?.Refresh ?? false);
await LoadFoundryLocalModelsAsync();
}
private sealed class FoundryDownloadableModel : INotifyPropertyChanged
@@ -1089,7 +1096,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
PasteAIProviderConfigurationDialog.Title = $"{displayName} provider configuration";
}
await UpdateFoundryLocalUIAsync(refreshFoundry: true);
await UpdateFoundryLocalUIAsync();
UpdatePasteAIUIVisibility();
RefreshDialogBindings();
@@ -1118,7 +1125,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
: $"{titlePrefix} provider configuration";
UpdatePasteAIUIVisibility();
await UpdateFoundryLocalUIAsync(refreshFoundry: false);
await UpdateFoundryLocalUIAsync();
RefreshDialogBindings();
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(provider.Id, provider.ServiceType);
await PasteAIProviderConfigurationDialog.ShowAsync();

View File

@@ -11,199 +11,201 @@
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
<Grid>
<Grid
MaxWidth="1000"
Padding="16,0,16,0"
VerticalAlignment="Top"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="16"
RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<tkcontrols:OpacityMaskView Margin="-16,0,-16,0" HorizontalAlignment="Stretch">
<tkcontrols:OpacityMaskView.OpacityMask>
<Rectangle>
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Offset="0.50" Color="Black" />
<GradientStop Offset="0.75" Color="#80000000" />
<GradientStop Offset="0.95" Color="Transparent" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</tkcontrols:OpacityMaskView.OpacityMask>
<Grid Height="560">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Image
Grid.RowSpan="3"
HorizontalAlignment="Stretch"
Source="/Assets/Settings/Modules/CmdPal_Background.png"
Stretch="UniformToFill" />
<TextBlock
Margin="0,24,0,12"
HorizontalAlignment="Center"
FontSize="36"
FontWeight="Bold"
Text="Command Palette">
<TextBlock.Foreground>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0.0" Color="#FFB9EBFF" />
<GradientStop Offset="0.49" Color="#FF86CBFF" />
<GradientStop Offset="1.0" Color="#FFA1E7FF" />
</LinearGradientBrush>
</TextBlock.Foreground>
</TextBlock>
<TextBlock
Grid.Row="1"
HorizontalAlignment="Center"
Foreground="White"
TextAlignment="Center"
TextWrapping="Wrap">
<Run x:Uid="CmdPal_Description" />
<Hyperlink NavigateUri="">
<Run x:Uid="LearnMore_CmdPal.Text" Foreground="White" />
</Hyperlink>
</TextBlock>
<Image
Grid.Row="2"
Margin="0,16,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Source="/Assets/Settings/Modules/CmdPal_Hero.png"
Stretch="Uniform" />
</Grid>
</tkcontrols:OpacityMaskView>
<ScrollViewer AutomationProperties.AutomationId="PageScrollViewer">
<Grid
Grid.Row="1"
Margin="0,-12,0,24"
ColumnSpacing="32"
MaxWidth="1000"
Padding="16,0,16,0"
VerticalAlignment="Top"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="16"
RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xEB3B;" />
<TextBlock
Grid.Row="1"
HorizontalAlignment="Center"
TextAlignment="Center"
TextWrapping="Wrap">
<Run x:Uid="CmdPal_ExtensibleHeader" FontWeight="SemiBold" /> <LineBreak />
<Run
x:Uid="CmdPal_ExtensibleDescription"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</TextBlock>
<tkcontrols:OpacityMaskView Margin="-16,0,-16,0" HorizontalAlignment="Stretch">
<tkcontrols:OpacityMaskView.OpacityMask>
<Rectangle>
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Offset="0.50" Color="Black" />
<GradientStop Offset="0.75" Color="#80000000" />
<GradientStop Offset="0.95" Color="Transparent" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</tkcontrols:OpacityMaskView.OpacityMask>
<Grid Height="560">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Image
Grid.RowSpan="3"
HorizontalAlignment="Stretch"
Source="/Assets/Settings/Modules/CmdPal_Background.png"
Stretch="UniformToFill" />
<TextBlock
Margin="0,24,0,12"
HorizontalAlignment="Center"
FontSize="36"
FontWeight="Bold"
Text="Command Palette">
<TextBlock.Foreground>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0.0" Color="#FFB9EBFF" />
<GradientStop Offset="0.49" Color="#FF86CBFF" />
<GradientStop Offset="1.0" Color="#FFA1E7FF" />
</LinearGradientBrush>
</TextBlock.Foreground>
</TextBlock>
<TextBlock
Grid.Row="1"
HorizontalAlignment="Center"
Foreground="White"
TextAlignment="Center"
TextWrapping="Wrap">
<Run x:Uid="CmdPal_Description" />
<Hyperlink NavigateUri="">
<Run x:Uid="LearnMore_CmdPal.Text" Foreground="White" />
</Hyperlink>
</TextBlock>
<Image
Grid.Row="2"
Margin="0,16,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Source="/Assets/Settings/Modules/CmdPal_Hero.png"
Stretch="Uniform" />
</Grid>
</tkcontrols:OpacityMaskView>
<FontIcon
Grid.Column="1"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE945;" />
<TextBlock
<Grid
Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Center"
TextAlignment="Center"
TextWrapping="Wrap">
<Run x:Uid="CmdPal_FastHeader" FontWeight="SemiBold" /> <LineBreak />
<Run
x:Uid="CmdPal_FastDescription"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</TextBlock>
Margin="0,-12,0,24"
ColumnSpacing="32"
RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xEB3B;" />
<TextBlock
Grid.Row="1"
HorizontalAlignment="Center"
TextAlignment="Center"
TextWrapping="Wrap">
<Run x:Uid="CmdPal_ExtensibleHeader" FontWeight="SemiBold" /> <LineBreak />
<Run
x:Uid="CmdPal_ExtensibleDescription"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</TextBlock>
<FontIcon
Grid.Column="2"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE790;" />
<TextBlock
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Center"
TextAlignment="Center"
TextWrapping="Wrap">
<Run x:Uid="CmdPal_ModernHeader" FontWeight="SemiBold" /> <LineBreak />
<Run
x:Uid="CmdPal_ModernDescription"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</TextBlock>
</Grid>
<StackPanel
Grid.Row="2"
Margin="0,8,0,0"
Orientation="Vertical"
Spacing="{StaticResource SettingsCardSpacing}">
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
<FontIcon
Grid.Column="1"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE945;" />
<TextBlock
Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Center"
TextAlignment="Center"
TextWrapping="Wrap">
<Run x:Uid="CmdPal_FastHeader" FontWeight="SemiBold" /> <LineBreak />
<Run
x:Uid="CmdPal_FastDescription"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</TextBlock>
<FontIcon
Grid.Column="2"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE790;" />
<TextBlock
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Center"
TextAlignment="Center"
TextWrapping="Wrap">
<Run x:Uid="CmdPal_ModernHeader" FontWeight="SemiBold" /> <LineBreak />
<Run
x:Uid="CmdPal_ModernDescription"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</TextBlock>
</Grid>
<StackPanel
Grid.Row="2"
Margin="0,8,0,0"
Orientation="Vertical"
Spacing="{StaticResource SettingsCardSpacing}">
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
<tkcontrols:SettingsCard
Name="CmdPalEnableCmdPal"
x:Uid="CmdPal_Enable_CmdPal"
HorizontalAlignment="Stretch"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/CmdPal.png}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<tkcontrols:SettingsCard
Name="CmdPalEnableCmdPal"
x:Uid="CmdPal_Enable_CmdPal"
HorizontalAlignment="Stretch"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/CmdPal.png}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
x:Uid="CmdPal_Launch"
Grid.Row="3"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
Click="LaunchCard_Click"
Header="Launch Command Palette"
HeaderIcon="{ui:FontIcon Glyph=&#xE945;}"
IsClickEnabled="True"
IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<ItemsControl
AutomationProperties.AccessibilityView="Raw"
IsTabStop="False"
ItemsSource="{x:Bind Path=ViewModel.Hotkey.GetKeysList(), Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:KeyVisual
Padding="8,8,8,8"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Content="{Binding}"
Style="{StaticResource AccentKeyVisualStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<tkcontrols:SettingsCard
x:Uid="CmdPal_Launch"
Grid.Row="3"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
Click="LaunchCard_Click"
Header="Launch Command Palette"
HeaderIcon="{ui:FontIcon Glyph=&#xE945;}"
IsClickEnabled="True"
IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<ItemsControl
AutomationProperties.AccessibilityView="Raw"
IsTabStop="False"
ItemsSource="{x:Bind Path=ViewModel.Hotkey.GetKeysList(), Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:KeyVisual
Padding="8,8,8,8"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Content="{Binding}"
Style="{StaticResource AccentKeyVisualStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
x:Uid="CmdPal_Settings"
Grid.Row="4"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
Click="SettingsCard_Click"
HeaderIcon="{ui:FontIcon Glyph=&#xE713;}"
IsClickEnabled="True"
IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}" />
</StackPanel>
</Grid>
<tkcontrols:SettingsCard
x:Uid="CmdPal_Settings"
Grid.Row="4"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
Click="SettingsCard_Click"
HeaderIcon="{ui:FontIcon Glyph=&#xE713;}"
IsClickEnabled="True"
IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}" />
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</local:NavigablePage>

View File

@@ -2703,7 +2703,7 @@ From there, simply click on one of the supported files in the File Explorer and
<value>Mouse Pointer Crosshairs</value>
<comment>Mouse as in the hardware peripheral.</comment>
</data>
<data name="Oobe_MouseUtils_MousePointerCrosshairs.Description" xml:space="preserve">
<data name="Oobe_MouseUtils_MousePointerCrosshairs_Description.Text" xml:space="preserve">
<value>Draw crosshairs centered around the mouse pointer.</value>
<comment>Mouse as in the hardware peripheral.</comment>
</data>
@@ -2827,7 +2827,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<comment>Refers to the utility name</comment>
</data>
<data name="MouseUtils_FindMyMouse.Description" xml:space="preserve">
<value>Find My Mouse highlights the position of the cursor when pressing the Ctrl key twice, using a custom shortcut or when shaking the mouse.</value>
<value>Highlight the position of the cursor when pressing the Ctrl key twice, using a custom shortcut or when shaking the mouse.</value>
<comment>"Ctrl" is a keyboard key. "Find My Mouse" is the name of the utility</comment>
</data>
<data name="MouseUtils_Enable_FindMyMouse.Header" xml:space="preserve">
@@ -2916,7 +2916,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<comment>Refers to the utility name</comment>
</data>
<data name="MouseUtils_MouseHighlighter.Description" xml:space="preserve">
<value>Mouse Highlighter mode will highlight mouse clicks.</value>
<value>Highlight mouse clicks.</value>
<comment>"Mouse Highlighter" is the name of the utility. Mouse is the hardware mouse.</comment>
</data>
<data name="MouseUtils_Enable_MouseHighlighter.Header" xml:space="preserve">
@@ -2961,7 +2961,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<comment>Refers to the utility name</comment>
</data>
<data name="MouseUtils_MousePointerCrosshairs.Description" xml:space="preserve">
<value>Mouse Pointer Crosshairs draws crosshairs centered on the mouse pointer.</value>
<value>Draw crosshairs centered on the mouse pointer.</value>
<comment>"Mouse Pointer Crosshairs" is the name of the utility. Mouse is the hardware mouse.</comment>
</data>
<data name="MouseUtils_Enable_MousePointerCrosshairs.Header" xml:space="preserve">
@@ -3410,7 +3410,7 @@ Activate by holding the key for the character you want to add an accent to, then
<value>An AI powered tool to put your clipboard content into any format you need, focused towards developer workflows.</value>
</data>
<data name="AdvancedPaste_EnableAISettingsCardDescription.Text" xml:space="preserve">
<value>Transform your clipboard content with the power of AI. An cloud or local endpoint is required.</value>
<value>Transform your clipboard content with the power of AI. A cloud or local endpoint is required.</value>
</data>
<data name="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore.Content" xml:space="preserve">
<value>Learn more</value>
@@ -5164,7 +5164,7 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<comment>Command Palette is a product name, do not loc</comment>
</data>
<data name="LearnMore_CmdPal.Text" xml:space="preserve">
<value>Learn more</value>
<value>Learn more about Command Palette</value>
</data>
<data name="Shell_CmdPal.Content" xml:space="preserve">
<value>Command Palette</value>

View File

@@ -40,6 +40,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public ObservableCollection<DashboardListItem> ActionModules { get; set; } = new ObservableCollection<DashboardListItem>();
// Master list of module items that is sorted and projected into AllModules.
private List<DashboardListItem> _moduleItems = new List<DashboardListItem>();
// Flag to prevent circular updates when a UI toggle triggers settings changes.
private bool _isUpdatingFromUI;
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
public AllHotkeyConflictsData AllHotkeyConflictsData
@@ -74,7 +80,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
generalSettingsConfig.DashboardSortOrder = value;
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettingsConfig);
SendConfigMSG(outgoing.ToString());
RefreshModuleList();
SortModuleList();
}
}
}
@@ -96,8 +102,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
RefreshModuleList();
GetShortcutModules();
BuildModuleList();
SortModuleList();
RefreshShortcutModules();
}
protected override void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
@@ -129,11 +136,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
}
private void RefreshModuleList()
/// <summary>
/// Builds the master list of module items. Called once during initialization.
/// Each module item contains its configuration, enabled state, and GPO lock status.
/// </summary>
private void BuildModuleList()
{
AllModules.Clear();
var moduleItems = new List<DashboardListItem>();
_moduleItems.Clear();
foreach (ModuleType moduleType in Enum.GetValues<ModuleType>())
{
@@ -149,47 +158,143 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
DashboardModuleItems = GetModuleItems(moduleType),
};
newItem.EnabledChangedCallback = EnabledChangedOnUI;
moduleItems.Add(newItem);
}
// Sort based on current sort order
var sortedItems = DashboardSortOrder switch
{
DashboardSortOrder.ByStatus => moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label),
_ => moduleItems.OrderBy(x => x.Label), // Default alphabetical
};
foreach (var item in sortedItems)
{
AllModules.Add(item);
_moduleItems.Add(newItem);
}
}
/// <summary>
/// Sorts the module list according to the current sort order and updates the AllModules collection.
/// On first call, populates AllModules. On subsequent calls, uses Move() to reorder items in-place
/// to avoid destroying and recreating UI elements.
/// </summary>
private void SortModuleList()
{
var sortedItems = (DashboardSortOrder switch
{
DashboardSortOrder.ByStatus => _moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label),
_ => _moduleItems.OrderBy(x => x.Label), // Default alphabetical
}).ToList();
// If AllModules is empty (first load), just populate it.
if (AllModules.Count == 0)
{
foreach (var item in sortedItems)
{
AllModules.Add(item);
}
return;
}
// Otherwise, update the collection in place using Move to avoid UI glitches.
for (int i = 0; i < sortedItems.Count; i++)
{
var currentItem = sortedItems[i];
var currentIndex = AllModules.IndexOf(currentItem);
if (currentIndex != -1 && currentIndex != i)
{
AllModules.Move(currentIndex, i);
}
}
// Notify that DashboardSortOrder changed so the menu updates its checked state.
OnPropertyChanged(nameof(DashboardSortOrder));
}
/// <summary>
/// Refreshes module enabled/locked states by re-reading GPO configuration. Only
/// updates properties that have actually changed to minimize UI notifications
/// then re-sorts the list according to the current sort order.
/// </summary>
private void RefreshModuleList()
{
foreach (var item in _moduleItems)
{
GpoRuleConfigured gpo = ModuleHelper.GetModuleGpoConfiguration(item.Tag);
// GPO can force-enable (Enabled) or force-disable (Disabled) a module.
// If Enabled: module is on and the user cannot disable it.
// If Disabled: module is off and the user cannot enable it.
// Otherwise, the setting is unlocked and the user can enable/disable it.
bool newEnabledState = gpo == GpoRuleConfigured.Enabled || (gpo != GpoRuleConfigured.Disabled && ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, item.Tag));
// Lock the toggle when GPO is controlling the module.
bool newLockedState = gpo == GpoRuleConfigured.Enabled || gpo == GpoRuleConfigured.Disabled;
// Only update if there's an actual change to minimize UI notifications.
if (item.IsEnabled != newEnabledState)
{
item.IsEnabled = newEnabledState;
}
if (item.IsLocked != newLockedState)
{
item.IsLocked = newLockedState;
}
}
SortModuleList();
}
/// <summary>
/// Callback invoked when a user toggles a module's enabled state in the UI.
/// Sets the _isUpdatingFromUI flag to prevent circular updates, then updates
/// settings, re-sorts if needed, and refreshes dependent collections.
/// </summary>
private void EnabledChangedOnUI(DashboardListItem dashboardListItem)
{
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, dashboardListItem.IsEnabled);
if (dashboardListItem.Tag == ModuleType.NewPlus && dashboardListItem.IsEnabled == true)
_isUpdatingFromUI = true;
try
{
var settingsUtils = new SettingsUtils();
var settings = NewPlusViewModel.LoadSettings(settingsUtils);
NewPlusViewModel.CopyTemplateExamples(settings.Properties.TemplateLocation.Value);
}
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, dashboardListItem.IsEnabled);
// Request updated conflicts after module state change
RequestConflictData();
if (dashboardListItem.Tag == ModuleType.NewPlus && dashboardListItem.IsEnabled == true)
{
var settingsUtils = new SettingsUtils();
var settings = NewPlusViewModel.LoadSettings(settingsUtils);
NewPlusViewModel.CopyTemplateExamples(settings.Properties.TemplateLocation.Value);
}
// Re-sort only required if sorting by enabled status.
if (DashboardSortOrder == DashboardSortOrder.ByStatus)
{
SortModuleList();
}
// Always refresh shortcuts/actions to reflect enabled state changes.
RefreshShortcutModules();
// Request updated conflicts after module state change.
RequestConflictData();
}
finally
{
_isUpdatingFromUI = false;
}
}
/// <summary>
/// Callback invoked when module enabled state changes from other parts of the
/// settings UI. Ignores the notification if it was triggered by a UI toggle
/// we're already handling, to prevent circular updates.
/// </summary>
public void ModuleEnabledChangedOnSettingsPage()
{
// Ignore if this was triggered by a UI change that we're already handling.
if (_isUpdatingFromUI)
{
return;
}
try
{
RefreshModuleList();
GetShortcutModules();
RefreshShortcutModules();
OnPropertyChanged(nameof(ShortcutModules));
// Request updated conflicts after module state change
// Request updated conflicts after module state change.
RequestConflictData();
}
catch (Exception ex)
@@ -198,7 +303,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
private void GetShortcutModules()
/// <summary>
/// Rebuilds ShortcutModules and ActionModules collections by filtering AllModules
/// to only include enabled modules and their respective shortcut/action items.
/// </summary>
private void RefreshShortcutModules()
{
ShortcutModules.Clear();
ActionModules.Clear();

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="Microsoft.PowerToys.ModuleLoader"
type="win32"
/>
<description>PowerToys Module Loader - Standalone module testing utility</description>
<!-- Per-Monitor DPI Awareness V2 -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<!-- Request administrator execution level if needed -->
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<!-- Windows 10+ compatibility -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9b}"/>
</application>
</compatibility>
</assembly>

View File

@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}</ProjectGuid>
<RootNamespace>ModuleLoader</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>ModuleLoader</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>ModuleLoader</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>ModuleLoader</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>ModuleLoader</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>ModuleLoader</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
</Link>
<Manifest>
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
</Link>
<Manifest>
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
</Link>
<Manifest>
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
</Link>
<Manifest>
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="src\main.cpp" />
<ClCompile Include="src\ModuleLoader.cpp" />
<ClCompile Include="src\SettingsLoader.cpp" />
<ClCompile Include="src\HotkeyManager.cpp" />
<ClCompile Include="src\ConsoleHost.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\ModuleLoader.h" />
<ClInclude Include="src\SettingsLoader.h" />
<ClInclude Include="src\HotkeyManager.h" />
<ClInclude Include="src\ConsoleHost.h" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\ModuleLoader.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\SettingsLoader.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\HotkeyManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\ConsoleHost.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\ModuleLoader.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\SettingsLoader.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\HotkeyManager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\ConsoleHost.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="README.md" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,483 @@
# Sharing ModuleLoader and Modules
This guide explains how to share the ModuleLoader tool and PowerToy modules with others for testing purposes.
## Overview
The ModuleLoader is designed to be a **portable, standalone testing tool** that can be shared with module developers and testers. It has minimal dependencies and can work with any compatible PowerToy module DLL.
---
## What You Need to Share
### For Testing a Module (e.g., CursorWrap)
#### **Minimum Package** (Recommended for Quick Testing)
1. **ModuleLoader.exe** - The standalone loader application
- Location: `x64\Debug\ModuleLoader.exe` or `x64\Release\ModuleLoader.exe`
- No additional DLLs required (uses only Windows system libraries)
2. **The Module DLL** - The PowerToy module to test
- Example: `CursorWrap.dll` from `x64\Debug\` or `x64\Release\`
- Location varies by module (see module-specific locations below)
3. **settings.json** - Module configuration (place in same folder as the DLL)
- **NEW**: Settings can be placed alongside the module DLL for portable testing
- Location: Same directory as the module DLL (e.g., `settings.json` next to `CursorWrap.dll`)
- Falls back to: `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json` if not found locally
#### **Complete Standalone Package** (For Users Without PowerToys Installed)
1. **ModuleLoader.exe**
2. **Module DLL**
3. **Sample settings.json** - Pre-configured settings file
4. **Installation instructions** - See "Standalone Package Setup" section below
---
### Debug Builds
If you build the module in Debug configuration:
- The module will output debug messages via `OutputDebugString()`
- View these with [DebugView](https://learn.microsoft.com/sysinternals/downloads/debugview) or Visual Studio Output window
- Example: CursorWrap outputs detailed topology and cursor wrapping debug info
---
## Module-Specific File Locations
### CursorWrap
```
Files to share:
- x64\Debug\CursorWrap.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\CursorWrap\settings.json
Size: ~100KB
```
### MouseHighlighter
```
Files to share:
- x64\Debug\MouseHighlighter.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\MouseHighlighter\settings.json
Size: ~150KB
```
### FindMyMouse
```
Files to share:
- x64\Debug\FindMyMouse.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\FindMyMouse\settings.json
Size: ~120KB
```
### MousePointerCrosshairs
```
Files to share:
- x64\Debug\MousePointerCrosshairs.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\MousePointerCrosshairs\settings.json
Size: ~140KB
```
### MouseJump
```
Files to share:
- x64\Debug\MouseJump.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\MouseJump\settings.json
Note: MouseJump is a UI-based module and may not work fully with ModuleLoader
Size: ~200KB
```
### AlwaysOnTop
```
Files to share:
- x64\Debug\AlwaysOnTop.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\AlwaysOnTop\settings.json
Size: ~100KB
```
---
## Dependency Analysis
### ModuleLoader.exe Dependencies
**Windows System Libraries Only** (automatically available on all Windows systems):
- `KERNEL32.dll` - Core Windows API
- `USER32.dll` - User interface functions
- `SHELL32.dll` - Shell functions
- `ole32.dll` - COM library
**No PowerToys dependencies required!** The ModuleLoader is completely standalone.
### Module DLL Dependencies (Typical)
Most PowerToy modules depend on:
- Windows system DLLs (automatically available)
- PowerToys common libraries (if any, they're typically statically linked)
- **Module settings** - Must be present in `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\`
**Important**: Modules are generally **self-contained** and statically link most dependencies. You typically only need the module DLL itself.
---
## Creating a Standalone Package
### Step 1: Prepare the Files
Create a folder structure like this:
```
ModuleLoaderPackage\
??? ModuleLoader.exe
??? CursorWrap.dll (or other module)
??? settings.json (module settings - placed locally!)
```
**NEW Simplified Structure**: You can now place `settings.json` directly alongside the module DLL! The ModuleLoader will check this location first before looking in the standard PowerToys settings directories.
### Step 2: Extract Settings from Your Machine
```powershell
# Copy settings from your development machine
$moduleName = "CursorWrap" # Change as needed
$settingsPath = "$env:LOCALAPPDATA\Microsoft\PowerToys\$moduleName\settings.json"
Copy-Item $settingsPath ".\settings\$moduleName\settings.json"
```
### Step 3: Create Installation Instructions (README.txt)
```text
PowerToys Module Testing Package
=================================
This package contains the ModuleLoader tool for testing PowerToy modules.
Contents:
- ModuleLoader.exe : Standalone module loader
- modules\*.dll : PowerToy module(s) to test
- settings\*\*.json : Module configuration files
Setup (First Time):
-------------------
1. Create settings directory:
%LOCALAPPDATA%\Microsoft\PowerToys\
2. Copy settings:
Copy the entire "settings\<ModuleName>" folder to:
%LOCALAPPDATA%\Microsoft\PowerToys\
Example for CursorWrap:
Copy "settings\CursorWrap" to:
%LOCALAPPDATA%\Microsoft\PowerToys\CursorWrap\
Usage:
------
ModuleLoader.exe modules\CursorWrap.dll
The tool will:
- Load the module DLL
- Read settings from %LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\
- Register hotkeys
- Enable the module
Press Ctrl+C to exit.
Press the module's hotkey to toggle functionality.
Requirements:
-------------
- Windows 10 1803 or later
- No PowerToys installation required!
Troubleshooting:
----------------
If you see "Settings file not found":
1. Make sure you copied the settings folder correctly
2. Check that the path is:
%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json
3. You can also run PowerToys once to generate default settings
Debug Logs:
-----------
Module logs are written to:
%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\Logs\
For debug builds, use DebugView to see real-time output.
```
---
## Quick Distribution Methods
### Method 1: ZIP Archive
```powershell
# Create a complete package
$moduleName = "CursorWrap"
$packageName = "ModuleLoader-$moduleName-Package"
# Collect files
New-Item $packageName -ItemType Directory
Copy-Item "x64\Debug\ModuleLoader.exe" "$packageName\"
New-Item "$packageName\modules" -ItemType Directory
Copy-Item "x64\Debug\$moduleName.dll" "$packageName\modules\"
New-Item "$packageName\settings\$moduleName" -ItemType Directory -Force
Copy-Item "$env:LOCALAPPDATA\Microsoft\PowerToys\$moduleName\settings.json" "$packageName\settings\$moduleName\"
# Create README
@"
See README in the tools\module_loader folder for instructions
"@ | Out-File "$packageName\README.txt"
# Zip it
Compress-Archive -Path $packageName -DestinationPath "$packageName.zip"
```
### Method 2: Direct Share (Advanced Users)
For developers who already have PowerToys installed:
```powershell
# Just share the executables
Copy-Item "x64\Debug\ModuleLoader.exe" "\\ShareLocation\"
Copy-Item "x64\Debug\CursorWrap.dll" "\\ShareLocation\"
```
They can run: `ModuleLoader.exe CursorWrap.dll`
(Settings will be loaded from their existing PowerToys installation)
---
## Platform-Specific Notes
### x64 vs ARM64
**Important**: Match architectures!
- `x64\Debug\ModuleLoader.exe` ? Only works with `x64` module DLLs
- `ARM64\Debug\ModuleLoader.exe` ? Only works with `ARM64` module DLLs
**Distribution Tip**: Provide both architectures if targeting multiple platforms:
```
ModuleLoaderPackage\
??? x64\
? ??? ModuleLoader.exe
? ??? modules\CursorWrap.dll
??? ARM64\
? ??? ModuleLoader.exe
? ??? modules\CursorWrap.dll
??? settings\...
```
### Debug vs Release
**Debug builds**:
- Larger file size
- Include debug symbols
- Verbose logging via `OutputDebugString()`
- Recommended for testing/development
**Release builds**:
- Smaller file size
- Optimized performance
- Minimal logging
- Recommended for end-user testing
---
## Testing Checklist
Before sharing a module package:
- [ ] ModuleLoader.exe is included
- [ ] Module DLL is included (matching architecture)
- [ ] Sample settings.json is included
- [ ] README/instructions are included
- [ ] Tested on a clean machine (no PowerToys installed)
- [ ] Verified hotkeys work
- [ ] Verified Ctrl+C exits cleanly
- [ ] Confirmed settings path in documentation
---
## Advanced: Portable Package Script
Here's a complete PowerShell script to create a fully portable package:
```powershell
param(
[Parameter(Mandatory=$true)]
[string]$ModuleName,
[ValidateSet("Debug", "Release")]
[string]$Configuration = "Debug",
[ValidateSet("x64", "ARM64")]
[string]$Platform = "x64"
)
$packageName = "ModuleLoader-$ModuleName-$Platform-$Configuration"
$packagePath = ".\$packageName"
Write-Host "Creating portable package: $packageName" -ForegroundColor Green
# Create structure
New-Item $packagePath -ItemType Directory -Force | Out-Null
New-Item "$packagePath\modules" -ItemType Directory -Force | Out-Null
New-Item "$packagePath\settings\$ModuleName" -ItemType Directory -Force | Out-Null
# Copy ModuleLoader
$loaderPath = "$Platform\$Configuration\ModuleLoader.exe"
if (Test-Path $loaderPath) {
Copy-Item $loaderPath "$packagePath\"
Write-Host "? Copied ModuleLoader.exe" -ForegroundColor Green
} else {
Write-Host "? ModuleLoader.exe not found at $loaderPath" -ForegroundColor Red
exit 1
}
# Copy Module DLL
$modulePath = "$Platform\$Configuration\$ModuleName.dll"
if (Test-Path $modulePath) {
Copy-Item $modulePath "$packagePath\modules\"
Write-Host "? Copied $ModuleName.dll" -ForegroundColor Green
} else {
Write-Host "? $ModuleName.dll not found at $modulePath" -ForegroundColor Red
exit 1
}
# Copy Settings
$settingsPath = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleName\settings.json"
if (Test-Path $settingsPath) {
Copy-Item $settingsPath "$packagePath\settings\$ModuleName\"
Write-Host "? Copied settings.json" -ForegroundColor Green
} else {
Write-Host "? Settings not found at $settingsPath - creating placeholder" -ForegroundColor Yellow
@"
{
"name": "$ModuleName",
"version": "1.0"
}
"@ | Out-File "$packagePath\settings\$ModuleName\settings.json"
}
# Create README
@"
PowerToys $ModuleName Testing Package
======================================
Configuration: $Configuration
Platform: $Platform
Setup Instructions:
-------------------
1. Copy the 'settings\$ModuleName' folder to:
%LOCALAPPDATA%\Microsoft\PowerToys\
2. Run:
ModuleLoader.exe modules\$ModuleName.dll
3. Press Ctrl+C to exit
Logs are written to:
%LOCALAPPDATA%\Microsoft\PowerToys\$ModuleName\Logs\
For more information, see:
https://github.com/microsoft/PowerToys/tree/main/tools/module_loader
"@ | Out-File "$packagePath\README.txt"
# Create ZIP
$zipPath = "$packageName.zip"
Compress-Archive -Path $packagePath -DestinationPath $zipPath -Force
Write-Host "? Created $zipPath" -ForegroundColor Green
# Show summary
Write-Host "`nPackage Contents:" -ForegroundColor Cyan
Get-ChildItem $packagePath -Recurse | ForEach-Object {
Write-Host " $($_.FullName.Replace($packagePath, ''))"
}
Write-Host "`nPackage ready: $zipPath" -ForegroundColor Green
Write-Host "Size: $([math]::Round((Get-Item $zipPath).Length / 1KB, 2)) KB"
```
**Usage**:
```powershell
.\CreateModulePackage.ps1 -ModuleName "CursorWrap" -Configuration Release -Platform x64
```
---
## FAQ
### Q: Can I share just ModuleLoader.exe and the module DLL?
**A**: Yes, but the recipient must have PowerToys installed (or manually create the settings file).
### Q: Does the tester need PowerToys installed?
**A**: No, if you provide the complete package with settings. ModuleLoader is fully standalone.
### Q: What if settings.json doesn't exist?
**A**: ModuleLoader will show an error. Either:
1. Run PowerToys once with the module enabled to generate settings
2. Manually create a minimal settings.json file
3. Include a sample settings.json in your package
### Q: Can I test modules on a virtual machine?
**A**: Yes! This is a great use case. Just copy the package to the VM - no PowerToys installation needed.
### Q: Do I need to include PDB files?
**A**: Only for debugging. For normal testing, just the EXE and DLL are sufficient.
### Q: Can I distribute this to end users?
**A**: ModuleLoader is a **development/testing tool**, not intended for end-user distribution. For production use, direct users to install PowerToys.
---
## Security Considerations
When sharing module DLLs:
1. **Verify Source**: Only share modules you built from trusted source code
2. **Scan for Malware**: Run antivirus scans on the package before sharing
3. **HTTPS Only**: Use secure channels (HTTPS, OneDrive, SharePoint) for distribution
4. **Hash Verification**: Consider providing SHA256 hashes for file integrity:
```powershell
Get-FileHash ModuleLoader.exe -Algorithm SHA256
Get-FileHash modules\CursorWrap.dll -Algorithm SHA256
```
---
## Example Package (CursorWrap)
Here's what a complete CursorWrap testing package looks like:
```
ModuleLoader-CursorWrap-x64-Debug.zip (220 KB)
?
??? ModuleLoader-CursorWrap-x64-Debug\
??? ModuleLoader.exe (160 KB)
??? README.txt (2 KB)
??? modules\
? ??? CursorWrap.dll (55 KB)
??? settings\
??? CursorWrap\
??? settings.json (3 KB)
```
**Total package size**: ~220 KB (compressed)
---
## Support
For issues with ModuleLoader, see:
- [ModuleLoader README](./README.md)
- [PowerToys Documentation](https://aka.ms/PowerToysOverview)
- [PowerToys GitHub Issues](https://github.com/microsoft/PowerToys/issues)
---
## License
ModuleLoader is part of PowerToys and is licensed under the MIT License.
See the LICENSE file in the PowerToys repository root for details.

View File

@@ -0,0 +1,80 @@
// 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.
#include "ConsoleHost.h"
#include <iostream>
bool ConsoleHost::s_exitRequested = false;
ConsoleHost::ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager)
: m_moduleLoader(moduleLoader)
, m_hotkeyManager(hotkeyManager)
{
}
ConsoleHost::~ConsoleHost()
{
}
BOOL WINAPI ConsoleHost::ConsoleCtrlHandler(DWORD ctrlType)
{
switch (ctrlType)
{
case CTRL_C_EVENT:
case CTRL_BREAK_EVENT:
case CTRL_CLOSE_EVENT:
std::wcout << L"\nCtrl+C received, shutting down...\n";
s_exitRequested = true;
// Post a quit message to break the message loop
PostQuitMessage(0);
return TRUE;
default:
return FALSE;
}
}
void ConsoleHost::Run()
{
// Install console control handler
if (!SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE))
{
std::wcerr << L"Warning: Failed to set console control handler\n";
}
s_exitRequested = false;
// Message loop
MSG msg;
while (!s_exitRequested)
{
// Wait for a message with a timeout so we can check s_exitRequested
DWORD result = MsgWaitForMultipleObjects(0, nullptr, FALSE, 100, QS_ALLINPUT);
if (result == WAIT_OBJECT_0)
{
// Process all pending messages
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
{
s_exitRequested = true;
break;
}
if (msg.message == WM_HOTKEY)
{
m_hotkeyManager.HandleHotkey(static_cast<int>(msg.wParam), m_moduleLoader);
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
// Remove console control handler
SetConsoleCtrlHandler(ConsoleCtrlHandler, FALSE);
}

View File

@@ -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.
#pragma once
#include <Windows.h>
#include "ModuleLoader.h"
#include "HotkeyManager.h"
/// <summary>
/// Console host that runs the message loop and handles Ctrl+C
/// </summary>
class ConsoleHost
{
public:
ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager);
~ConsoleHost();
// Prevent copying
ConsoleHost(const ConsoleHost&) = delete;
ConsoleHost& operator=(const ConsoleHost&) = delete;
/// <summary>
/// Run the message loop until Ctrl+C is pressed
/// </summary>
void Run();
private:
ModuleLoader& m_moduleLoader;
HotkeyManager& m_hotkeyManager;
static bool s_exitRequested;
/// <summary>
/// Console control handler (for Ctrl+C)
/// </summary>
static BOOL WINAPI ConsoleCtrlHandler(DWORD ctrlType);
};

View File

@@ -0,0 +1,279 @@
// 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.
#include "HotkeyManager.h"
#include <iostream>
#include <sstream>
HotkeyManager::HotkeyManager()
: m_nextHotkeyId(1) // Start from 1
, m_hotkeyExRegistered(false)
, m_hotkeyExId(0)
{
}
HotkeyManager::~HotkeyManager()
{
UnregisterAll();
}
UINT HotkeyManager::ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const
{
UINT modifiers = MOD_NOREPEAT; // Prevent repeat events
if (win) modifiers |= MOD_WIN;
if (ctrl) modifiers |= MOD_CONTROL;
if (alt) modifiers |= MOD_ALT;
if (shift) modifiers |= MOD_SHIFT;
return modifiers;
}
bool HotkeyManager::RegisterModuleHotkeys(ModuleLoader& moduleLoader)
{
if (!moduleLoader.IsLoaded())
{
std::wcerr << L"Error: Module not loaded\n";
return false;
}
bool anyRegistered = false;
// First, try the newer GetHotkeyEx() API
auto hotkeyEx = moduleLoader.GetHotkeyEx();
if (hotkeyEx.has_value())
{
std::wcout << L"Module has HotkeyEx activation hotkey\n";
UINT modifiers = hotkeyEx->modifiersMask | MOD_NOREPEAT;
UINT vkCode = hotkeyEx->vkCode;
if (vkCode != 0)
{
int hotkeyId = m_nextHotkeyId++;
std::wcout << L" Registering HotkeyEx: ";
std::wcout << ModifiersToString(modifiers) << L"+" << VKeyToString(vkCode);
if (RegisterHotKey(nullptr, hotkeyId, modifiers, vkCode))
{
m_hotkeyExRegistered = true;
m_hotkeyExId = hotkeyId;
std::wcout << L" - OK (Activation/Toggle)\n";
anyRegistered = true;
}
else
{
DWORD error = GetLastError();
std::wcout << L" - FAILED (Error: " << error << L")\n";
if (error == ERROR_HOTKEY_ALREADY_REGISTERED)
{
std::wcout << L" (Hotkey is already registered by another application)\n";
}
}
}
}
// Also check the legacy get_hotkeys() API
size_t hotkeyCount = moduleLoader.GetHotkeys(nullptr, 0);
if (hotkeyCount > 0)
{
std::wcout << L"Module reports " << hotkeyCount << L" legacy hotkey(s)\n";
// Allocate buffer and get the hotkeys
std::vector<PowertoyModuleIface::Hotkey> hotkeys(hotkeyCount);
size_t actualCount = moduleLoader.GetHotkeys(hotkeys.data(), hotkeyCount);
// Register each hotkey
for (size_t i = 0; i < actualCount; i++)
{
const auto& hotkey = hotkeys[i];
UINT modifiers = ConvertModifiers(hotkey.win, hotkey.ctrl, hotkey.alt, hotkey.shift);
UINT vkCode = hotkey.key;
if (vkCode == 0)
{
std::wcout << L" Skipping hotkey " << i << L" (no key code)\n";
continue;
}
int hotkeyId = m_nextHotkeyId++;
std::wcout << L" Registering hotkey " << i << L": ";
std::wcout << ModifiersToString(modifiers) << L"+" << VKeyToString(vkCode);
if (RegisterHotKey(nullptr, hotkeyId, modifiers, vkCode))
{
HotkeyInfo info;
info.id = hotkeyId;
info.moduleHotkeyId = i;
info.modifiers = modifiers;
info.vkCode = vkCode;
info.description = ModifiersToString(modifiers) + L"+" + VKeyToString(vkCode);
m_registeredHotkeys.push_back(info);
std::wcout << L" - OK\n";
anyRegistered = true;
}
else
{
DWORD error = GetLastError();
std::wcout << L" - FAILED (Error: " << error << L")\n";
if (error == ERROR_HOTKEY_ALREADY_REGISTERED)
{
std::wcout << L" (Hotkey is already registered by another application)\n";
}
}
}
}
if (!anyRegistered && hotkeyCount == 0 && !hotkeyEx.has_value())
{
std::wcout << L"Module has no hotkeys\n";
}
return anyRegistered;
}
void HotkeyManager::UnregisterAll()
{
for (const auto& hotkey : m_registeredHotkeys)
{
UnregisterHotKey(nullptr, hotkey.id);
}
m_registeredHotkeys.clear();
if (m_hotkeyExRegistered)
{
UnregisterHotKey(nullptr, m_hotkeyExId);
m_hotkeyExRegistered = false;
m_hotkeyExId = 0;
}
}
bool HotkeyManager::HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader)
{
// Check if it's the HotkeyEx activation hotkey
if (m_hotkeyExRegistered && hotkeyId == m_hotkeyExId)
{
std::wcout << L"\nActivation hotkey triggered (HotkeyEx)\n";
moduleLoader.OnHotkeyEx();
std::wcout << L"Module toggled via activation hotkey\n";
std::wcout << L"Module enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n\n";
return true;
}
// Check legacy hotkeys
for (const auto& hotkey : m_registeredHotkeys)
{
if (hotkey.id == hotkeyId)
{
std::wcout << L"\nHotkey triggered: " << hotkey.description << L"\n";
bool result = moduleLoader.OnHotkey(hotkey.moduleHotkeyId);
std::wcout << L"Module handled hotkey: " << (result ? L"Swallowed" : L"Not swallowed") << L"\n";
std::wcout << L"Module enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n\n";
return true;
}
}
return false;
}
void HotkeyManager::PrintHotkeys() const
{
for (const auto& hotkey : m_registeredHotkeys)
{
std::wcout << L" " << hotkey.description << L"\n";
}
}
std::wstring HotkeyManager::ModifiersToString(UINT modifiers) const
{
std::wstringstream ss;
bool first = true;
if (modifiers & MOD_WIN)
{
if (!first) ss << L"+";
ss << L"Win";
first = false;
}
if (modifiers & MOD_CONTROL)
{
if (!first) ss << L"+";
ss << L"Ctrl";
first = false;
}
if (modifiers & MOD_ALT)
{
if (!first) ss << L"+";
ss << L"Alt";
first = false;
}
if (modifiers & MOD_SHIFT)
{
if (!first) ss << L"+";
ss << L"Shift";
first = false;
}
return ss.str();
}
std::wstring HotkeyManager::VKeyToString(UINT vkCode) const
{
// Handle special keys
switch (vkCode)
{
case VK_SPACE: return L"Space";
case VK_RETURN: return L"Enter";
case VK_ESCAPE: return L"Esc";
case VK_TAB: return L"Tab";
case VK_BACK: return L"Backspace";
case VK_DELETE: return L"Del";
case VK_INSERT: return L"Ins";
case VK_HOME: return L"Home";
case VK_END: return L"End";
case VK_PRIOR: return L"PgUp";
case VK_NEXT: return L"PgDn";
case VK_LEFT: return L"Left";
case VK_RIGHT: return L"Right";
case VK_UP: return L"Up";
case VK_DOWN: return L"Down";
case VK_F1: return L"F1";
case VK_F2: return L"F2";
case VK_F3: return L"F3";
case VK_F4: return L"F4";
case VK_F5: return L"F5";
case VK_F6: return L"F6";
case VK_F7: return L"F7";
case VK_F8: return L"F8";
case VK_F9: return L"F9";
case VK_F10: return L"F10";
case VK_F11: return L"F11";
case VK_F12: return L"F12";
}
// For alphanumeric keys, use MapVirtualKey
wchar_t keyName[256];
UINT scanCode = MapVirtualKeyW(vkCode, MAPVK_VK_TO_VSC);
if (GetKeyNameTextW(scanCode << 16, keyName, 256) > 0)
{
return keyName;
}
// Fallback to hex code
std::wstringstream ss;
ss << L"0x" << std::hex << vkCode;
return ss.str();
}

View File

@@ -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.
#pragma once
#include <Windows.h>
#include <string>
#include <vector>
#include <map>
#include "ModuleLoader.h"
/// <summary>
/// Manages hotkey registration using RegisterHotKey API
/// </summary>
class HotkeyManager
{
public:
HotkeyManager();
~HotkeyManager();
// Prevent copying
HotkeyManager(const HotkeyManager&) = delete;
HotkeyManager& operator=(const HotkeyManager&) = delete;
/// <summary>
/// Register all hotkeys from a module
/// </summary>
/// <param name="moduleLoader">Module to get hotkeys from</param>
/// <returns>True if at least one hotkey was registered</returns>
bool RegisterModuleHotkeys(ModuleLoader& moduleLoader);
/// <summary>
/// Unregister all hotkeys
/// </summary>
void UnregisterAll();
/// <summary>
/// Handle a WM_HOTKEY message
/// </summary>
/// <param name="hotkeyId">ID from the WM_HOTKEY message</param>
/// <param name="moduleLoader">Module to trigger the hotkey on</param>
/// <returns>True if the hotkey was handled</returns>
bool HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader);
/// <summary>
/// Get the number of registered hotkeys
/// </summary>
/// <returns>Number of registered hotkeys</returns>
size_t GetRegisteredCount() const { return m_registeredHotkeys.size() + (m_hotkeyExRegistered ? 1 : 0); }
/// <summary>
/// Print registered hotkeys to console
/// </summary>
void PrintHotkeys() const;
private:
struct HotkeyInfo
{
int id = 0;
size_t moduleHotkeyId = 0;
UINT modifiers = 0;
UINT vkCode = 0;
std::wstring description;
};
std::vector<HotkeyInfo> m_registeredHotkeys;
int m_nextHotkeyId;
bool m_hotkeyExRegistered;
int m_hotkeyExId;
/// <summary>
/// Convert modifier bools to RegisterHotKey modifiers
/// </summary>
UINT ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const;
/// <summary>
/// Get a string representation of modifiers
/// </summary>
std::wstring ModifiersToString(UINT modifiers) const;
/// <summary>
/// Get a string representation of a virtual key code
/// </summary>
std::wstring VKeyToString(UINT vkCode) const;
};

View File

@@ -0,0 +1,183 @@
// 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.
#include "ModuleLoader.h"
#include <iostream>
#include <stdexcept>
ModuleLoader::ModuleLoader()
: m_hModule(nullptr)
, m_module(nullptr)
{
}
ModuleLoader::~ModuleLoader()
{
if (m_module)
{
try
{
m_module->destroy();
}
catch (...)
{
// Ignore exceptions during cleanup
}
m_module = nullptr;
}
if (m_hModule)
{
FreeLibrary(m_hModule);
m_hModule = nullptr;
}
}
bool ModuleLoader::Load(const std::wstring& dllPath)
{
if (m_hModule || m_module)
{
std::wcerr << L"Error: Module already loaded\n";
return false;
}
m_dllPath = dllPath;
// Load the DLL
m_hModule = LoadLibraryW(dllPath.c_str());
if (!m_hModule)
{
DWORD error = GetLastError();
std::wcerr << L"Error: Failed to load DLL. Error code: " << error << L"\n";
return false;
}
// Get the powertoy_create function
using powertoy_create_func = PowertoyModuleIface* (*)();
auto create_func = reinterpret_cast<powertoy_create_func>(
GetProcAddress(m_hModule, "powertoy_create"));
if (!create_func)
{
std::wcerr << L"Error: DLL does not export 'powertoy_create' function\n";
FreeLibrary(m_hModule);
m_hModule = nullptr;
return false;
}
// Create the module instance
m_module = create_func();
if (!m_module)
{
std::wcerr << L"Error: powertoy_create() returned nullptr\n";
FreeLibrary(m_hModule);
m_hModule = nullptr;
return false;
}
std::wcout << L"Module instance created successfully\n";
return true;
}
void ModuleLoader::Enable()
{
if (!m_module)
{
throw std::runtime_error("Module not loaded");
}
m_module->enable();
}
void ModuleLoader::Disable()
{
if (!m_module)
{
return;
}
m_module->disable();
}
bool ModuleLoader::IsEnabled() const
{
if (!m_module)
{
return false;
}
return m_module->is_enabled();
}
void ModuleLoader::SetConfig(const std::wstring& configJson)
{
if (!m_module)
{
throw std::runtime_error("Module not loaded");
}
m_module->set_config(configJson.c_str());
}
std::wstring ModuleLoader::GetModuleName() const
{
if (!m_module)
{
return L"<not loaded>";
}
const wchar_t* name = m_module->get_name();
return name ? name : L"<unknown>";
}
std::wstring ModuleLoader::GetModuleKey() const
{
if (!m_module)
{
return L"<not loaded>";
}
const wchar_t* key = m_module->get_key();
return key ? key : L"<unknown>";
}
size_t ModuleLoader::GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize)
{
if (!m_module)
{
return 0;
}
return m_module->get_hotkeys(buffer, bufferSize);
}
bool ModuleLoader::OnHotkey(size_t hotkeyId)
{
if (!m_module)
{
return false;
}
return m_module->on_hotkey(hotkeyId);
}
std::optional<PowertoyModuleIface::HotkeyEx> ModuleLoader::GetHotkeyEx()
{
if (!m_module)
{
return std::nullopt;
}
return m_module->GetHotkeyEx();
}
void ModuleLoader::OnHotkeyEx()
{
if (!m_module)
{
return;
}
m_module->OnHotkeyEx();
}

View File

@@ -0,0 +1,102 @@
// 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.
#pragma once
#include <Windows.h>
#include <string>
#include <vector>
#include <powertoy_module_interface.h>
/// <summary>
/// Wrapper class for loading and managing a PowerToy module DLL
/// </summary>
class ModuleLoader
{
public:
ModuleLoader();
~ModuleLoader();
// Prevent copying
ModuleLoader(const ModuleLoader&) = delete;
ModuleLoader& operator=(const ModuleLoader&) = delete;
/// <summary>
/// Load a PowerToy module DLL
/// </summary>
/// <param name="dllPath">Path to the module DLL</param>
/// <returns>True if successful, false otherwise</returns>
bool Load(const std::wstring& dllPath);
/// <summary>
/// Enable the loaded module
/// </summary>
void Enable();
/// <summary>
/// Disable the loaded module
/// </summary>
void Disable();
/// <summary>
/// Check if the module is enabled
/// </summary>
/// <returns>True if enabled, false otherwise</returns>
bool IsEnabled() const;
/// <summary>
/// Set configuration for the module
/// </summary>
/// <param name="configJson">JSON configuration string</param>
void SetConfig(const std::wstring& configJson);
/// <summary>
/// Get the module's localized name
/// </summary>
/// <returns>Module name</returns>
std::wstring GetModuleName() const;
/// <summary>
/// Get the module's non-localized key
/// </summary>
/// <returns>Module key</returns>
std::wstring GetModuleKey() const;
/// <summary>
/// Get the module's hotkeys
/// </summary>
/// <param name="buffer">Buffer to store hotkeys</param>
/// <param name="bufferSize">Size of the buffer</param>
/// <returns>Number of hotkeys returned</returns>
size_t GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize);
/// <summary>
/// Trigger a hotkey callback on the module
/// </summary>
/// <param name="hotkeyId">ID of the hotkey to trigger</param>
/// <returns>True if the key press should be swallowed</returns>
bool OnHotkey(size_t hotkeyId);
/// <summary>
/// Check if the module is loaded
/// </summary>
/// <returns>True if loaded, false otherwise</returns>
bool IsLoaded() const { return m_module != nullptr; }
/// <summary>
/// Get the module's activation hotkey (newer HotkeyEx API)
/// </summary>
/// <returns>Optional HotkeyEx struct</returns>
std::optional<PowertoyModuleIface::HotkeyEx> GetHotkeyEx();
/// <summary>
/// Trigger the newer-style hotkey callback on the module
/// </summary>
void OnHotkeyEx();
private:
HMODULE m_hModule;
PowertoyModuleIface* m_module;
std::wstring m_dllPath;
};

View File

@@ -0,0 +1,182 @@
// 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.
#include "SettingsLoader.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <filesystem>
#include <Shlobj.h>
SettingsLoader::SettingsLoader()
{
}
SettingsLoader::~SettingsLoader()
{
}
std::wstring SettingsLoader::GetPowerToysSettingsRoot() const
{
// Get %LOCALAPPDATA%
PWSTR localAppDataPath = nullptr;
HRESULT hr = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppDataPath);
if (FAILED(hr) || !localAppDataPath)
{
std::wcerr << L"Error: Failed to get LOCALAPPDATA path\n";
return L"";
}
std::wstring result(localAppDataPath);
CoTaskMemFree(localAppDataPath);
// Append PowerToys directory
result += L"\\Microsoft\\PowerToys";
return result;
}
std::wstring SettingsLoader::GetSettingsPath(const std::wstring& moduleName) const
{
std::wstring root = GetPowerToysSettingsRoot();
if (root.empty())
{
return L"";
}
// Construct path: %LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json
std::wstring settingsPath = root + L"\\" + moduleName + L"\\settings.json";
return settingsPath;
}
std::wstring SettingsLoader::ReadFileContents(const std::wstring& filePath) const
{
std::wifstream file(filePath, std::ios::binary);
if (!file.is_open())
{
std::wcerr << L"Error: Could not open file: " << filePath << L"\n";
return L"";
}
// Read the entire file
std::wstringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
std::wstring SettingsLoader::LoadSettings(const std::wstring& moduleName, const std::wstring& moduleDllPath)
{
const std::wstring powerToysPrefix = L"PowerToys.";
// Build list of possible module name variations to try
std::vector<std::wstring> moduleNameVariants;
// Try exact name first
moduleNameVariants.push_back(moduleName);
// If doesn't start with "PowerToys.", try adding it
if (moduleName.find(powerToysPrefix) != 0)
{
moduleNameVariants.push_back(powerToysPrefix + moduleName);
}
// If starts with "PowerToys.", try without it
else
{
moduleNameVariants.push_back(moduleName.substr(powerToysPrefix.length()));
}
// FIRST: Try same directory as the module DLL
if (!moduleDllPath.empty())
{
std::filesystem::path dllPath(moduleDllPath);
std::filesystem::path dllDirectory = dllPath.parent_path();
std::wstring localSettingsPath = (dllDirectory / L"settings.json").wstring();
std::wcout << L"Trying settings path (module directory): " << localSettingsPath << L"\n";
if (std::filesystem::exists(localSettingsPath))
{
std::wstring contents = ReadFileContents(localSettingsPath);
if (!contents.empty())
{
std::wcout << L"Settings file loaded from module directory (" << contents.size() << L" characters)\n";
return contents;
}
}
}
// SECOND: Try standard PowerToys settings locations
for (const auto& variant : moduleNameVariants)
{
std::wstring settingsPath = GetSettingsPath(variant);
std::wcout << L"Trying settings path: " << settingsPath << L"\n";
// Check if file exists (case-sensitive path)
if (std::filesystem::exists(settingsPath))
{
std::wstring contents = ReadFileContents(settingsPath);
if (!contents.empty())
{
std::wcout << L"Settings file loaded (" << contents.size() << L" characters)\n";
return contents;
}
}
else
{
// Try case-insensitive search in the parent directory
std::wstring root = GetPowerToysSettingsRoot();
if (!root.empty() && std::filesystem::exists(root))
{
try
{
// Search for a directory that matches case-insensitively
for (const auto& entry : std::filesystem::directory_iterator(root))
{
if (entry.is_directory())
{
std::wstring dirName = entry.path().filename().wstring();
// Case-insensitive comparison
if (_wcsicmp(dirName.c_str(), variant.c_str()) == 0)
{
std::wstring actualSettingsPath = entry.path().wstring() + L"\\settings.json";
std::wcout << L"Found case-insensitive match: " << actualSettingsPath << L"\n";
if (std::filesystem::exists(actualSettingsPath))
{
std::wstring contents = ReadFileContents(actualSettingsPath);
if (!contents.empty())
{
std::wcout << L"Settings file loaded (" << contents.size() << L" characters)\n";
return contents;
}
}
}
}
}
}
catch (const std::filesystem::filesystem_error& e)
{
std::wcerr << L"Error searching directory: " << e.what() << L"\n";
}
}
}
}
std::wcerr << L"Error: Settings file not found in any expected location:\n";
if (!moduleDllPath.empty())
{
std::filesystem::path dllPath(moduleDllPath);
std::filesystem::path dllDirectory = dllPath.parent_path();
std::wcerr << L" - " << (dllDirectory / L"settings.json").wstring() << L" (module directory)\n";
}
for (const auto& variant : moduleNameVariants)
{
std::wcerr << L" - " << GetSettingsPath(variant) << L"\n";
}
return L"";
}

View File

@@ -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.
#pragma once
#include <Windows.h>
#include <string>
/// <summary>
/// Utility class for discovering and loading PowerToy module settings
/// </summary>
class SettingsLoader
{
public:
SettingsLoader();
~SettingsLoader();
/// <summary>
/// Load settings for a PowerToy module
/// </summary>
/// <param name="moduleName">Name of the module (e.g., "CursorWrap")</param>
/// <param name="moduleDllPath">Full path to the module DLL (for checking local settings.json)</param>
/// <returns>JSON settings string, or empty string if not found</returns>
std::wstring LoadSettings(const std::wstring& moduleName, const std::wstring& moduleDllPath);
/// <summary>
/// Get the settings file path for a module
/// </summary>
/// <param name="moduleName">Name of the module</param>
/// <returns>Full path to the settings.json file</returns>
std::wstring GetSettingsPath(const std::wstring& moduleName) const;
private:
/// <summary>
/// Get the PowerToys root settings directory
/// </summary>
/// <returns>Path to %LOCALAPPDATA%\Microsoft\PowerToys</returns>
std::wstring GetPowerToysSettingsRoot() const;
/// <summary>
/// Read a text file into a string
/// </summary>
/// <param name="filePath">Path to the file</param>
/// <returns>File contents as a string</returns>
std::wstring ReadFileContents(const std::wstring& filePath) const;
};

View File

@@ -0,0 +1,244 @@
// 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.
#include <Windows.h>
#include <Tlhelp32.h>
#include <iostream>
#include <string>
#include <filesystem>
#include "ModuleLoader.h"
#include "SettingsLoader.h"
#include "HotkeyManager.h"
#include "ConsoleHost.h"
namespace
{
void PrintUsage()
{
std::wcout << L"PowerToys Module Loader - Standalone utility for loading and testing PowerToy modules\n\n";
std::wcout << L"Usage: ModuleLoader.exe <module_dll_path>\n\n";
std::wcout << L"Arguments:\n";
std::wcout << L" module_dll_path Path to the PowerToy module DLL (e.g., CursorWrap.dll)\n\n";
std::wcout << L"Behavior:\n";
std::wcout << L" - Automatically discovers settings from %%LOCALAPPDATA%%\\Microsoft\\PowerToys\\<ModuleName>\\settings.json\n";
std::wcout << L" - Loads and enables the module\n";
std::wcout << L" - Registers module hotkeys\n";
std::wcout << L" - Runs until Ctrl+C is pressed\n\n";
std::wcout << L"Examples:\n";
std::wcout << L" ModuleLoader.exe x64\\Debug\\modules\\CursorWrap.dll\n";
std::wcout << L" ModuleLoader.exe \"C:\\Program Files\\PowerToys\\modules\\MouseHighlighter.dll\"\n\n";
std::wcout << L"Notes:\n";
std::wcout << L" - Only non-UI modules are supported\n";
std::wcout << L" - Module must have a valid settings.json file\n";
std::wcout << L" - Debug output is written to module's log directory\n";
}
std::wstring ExtractModuleName(const std::wstring& dllPath)
{
std::filesystem::path path(dllPath);
std::wstring filename = path.stem().wstring();
// Remove "PowerToys." prefix if present (case-insensitive)
const std::wstring powerToysPrefix = L"PowerToys.";
if (filename.length() >= powerToysPrefix.length())
{
// Check if filename starts with "PowerToys." (case-insensitive)
if (_wcsnicmp(filename.c_str(), powerToysPrefix.c_str(), powerToysPrefix.length()) == 0)
{
filename = filename.substr(powerToysPrefix.length());
}
}
// Common PowerToys module naming patterns
// Remove common suffixes if present
const std::wstring suffixes[] = { L"Module", L"ModuleInterface", L"Interface" };
for (const auto& suffix : suffixes)
{
if (filename.size() > suffix.size())
{
size_t pos = filename.rfind(suffix);
if (pos != std::wstring::npos && pos + suffix.size() == filename.size())
{
filename = filename.substr(0, pos);
break;
}
}
}
return filename;
}
}
int wmain(int argc, wchar_t* argv[])
{
std::wcout << L"PowerToys Module Loader v1.0\n";
std::wcout << L"=============================\n\n";
// Check if PowerToys.exe is running
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);
bool powerToysRunning = false;
if (Process32FirstW(hSnapshot, &pe32))
{
do
{
if (_wcsicmp(pe32.szExeFile, L"PowerToys.exe") == 0)
{
powerToysRunning = true;
break;
}
} while (Process32NextW(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
if (powerToysRunning)
{
// Display warning with VT100 colors
// Yellow background (43m), black text (30m), bold (1m)
std::wcout << L"\033[1;43;30m WARNING \033[0m PowerToys.exe is currently running!\n\n";
// Red text for important message
std::wcout << L"\033[1;31m";
std::wcout << L"Running ModuleLoader while PowerToys is active may cause conflicts:\n";
std::wcout << L" - Duplicate hotkey registrations\n";
std::wcout << L" - Conflicting module instances\n";
std::wcout << L" - Unexpected behavior\n";
std::wcout << L"\033[0m\n"; // Reset color
// Cyan text for recommendation
std::wcout << L"\033[1;36m";
std::wcout << L"RECOMMENDATION: Exit PowerToys before continuing.\n";
std::wcout << L"\033[0m\n"; // Reset color
// Yellow text for prompt
std::wcout << L"\033[1;33m";
std::wcout << L"Do you want to continue anyway? (y/N): ";
std::wcout << L"\033[0m"; // Reset color
wchar_t response = L'\0';
std::wcin >> response;
if (response != L'y' && response != L'Y')
{
std::wcout << L"\nExiting. Please close PowerToys and try again.\n";
return 1;
}
std::wcout << L"\n";
}
}
// Parse command-line arguments
if (argc < 2)
{
std::wcerr << L"Error: Missing required argument <module_dll_path>\n\n";
PrintUsage();
return 1;
}
const std::wstring dllPath = argv[1];
// Validate DLL exists
if (!std::filesystem::exists(dllPath))
{
std::wcerr << L"Error: Module DLL not found: " << dllPath << L"\n";
return 1;
}
std::wcout << L"Loading module: " << dllPath << L"\n";
// Extract module name from DLL path
std::wstring moduleName = ExtractModuleName(dllPath);
std::wcout << L"Detected module name: " << moduleName << L"\n\n";
try
{
// Load settings for the module
std::wcout << L"Loading settings...\n";
SettingsLoader settingsLoader;
std::wstring settingsJson = settingsLoader.LoadSettings(moduleName, dllPath);
if (settingsJson.empty())
{
std::wcerr << L"Error: Could not load settings for module '" << moduleName << L"'\n";
std::wcerr << L"Expected location: %LOCALAPPDATA%\\Microsoft\\PowerToys\\" << moduleName << L"\\settings.json\n";
return 1;
}
std::wcout << L"Settings loaded successfully.\n\n";
// Load the module DLL
std::wcout << L"Loading module DLL...\n";
ModuleLoader moduleLoader;
if (!moduleLoader.Load(dllPath))
{
std::wcerr << L"Error: Failed to load module DLL\n";
return 1;
}
std::wcout << L"Module DLL loaded successfully.\n";
std::wcout << L"Module key: " << moduleLoader.GetModuleKey() << L"\n";
std::wcout << L"Module name: " << moduleLoader.GetModuleName() << L"\n\n";
// Apply settings to the module
std::wcout << L"Applying settings to module...\n";
moduleLoader.SetConfig(settingsJson);
std::wcout << L"Settings applied.\n\n";
// Register hotkeys
std::wcout << L"Registering module hotkeys...\n";
HotkeyManager hotkeyManager;
if (!hotkeyManager.RegisterModuleHotkeys(moduleLoader))
{
std::wcerr << L"Warning: Failed to register some hotkeys\n";
}
std::wcout << L"Hotkeys registered: " << hotkeyManager.GetRegisteredCount() << L"\n\n";
// Enable the module
std::wcout << L"Enabling module...\n";
moduleLoader.Enable();
std::wcout << L"Module enabled.\n\n";
// Display status
std::wcout << L"=============================\n";
std::wcout << L"Module is now running!\n";
std::wcout << L"=============================\n\n";
std::wcout << L"Module Status:\n";
std::wcout << L" - Name: " << moduleLoader.GetModuleName() << L"\n";
std::wcout << L" - Key: " << moduleLoader.GetModuleKey() << L"\n";
std::wcout << L" - Enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n";
std::wcout << L" - Hotkeys: " << hotkeyManager.GetRegisteredCount() << L" registered\n\n";
if (hotkeyManager.GetRegisteredCount() > 0)
{
std::wcout << L"Registered Hotkeys:\n";
hotkeyManager.PrintHotkeys();
std::wcout << L"\n";
}
std::wcout << L"Press Ctrl+C to exit.\n";
std::wcout << L"You can press the module's hotkey to toggle its functionality.\n\n";
// Run the message loop
ConsoleHost consoleHost(moduleLoader, hotkeyManager);
consoleHost.Run();
// Cleanup
std::wcout << L"\nShutting down...\n";
moduleLoader.Disable();
hotkeyManager.UnregisterAll();
std::wcout << L"Module unloaded successfully.\n";
return 0;
}
catch (const std::exception& ex)
{
std::wcerr << L"Fatal error: " << ex.what() << L"\n";
return 1;
}
}