Compare commits

..

30 Commits

Author SHA1 Message Date
vanzue
8fe9263d99 develop toolbar 2025-10-26 19:12:13 +08:00
Kai Tao (from Dev Box)
49fd8ac0dd dev 2025-10-09 10:18:01 +08:00
Kai Tao (from Dev Box)
304b069c5c dev 2025-10-04 10:45:55 +08:00
vanzue
5915a5c2fc dev 2025-10-01 00:13:04 +08:00
vanzue
3c6d6c998a toolbar dev 2025-09-30 20:38:18 +08:00
Jiří Polášek
05c700a4cd CmdPal: Fix NavView merge (#42096)
## Summary of the Pull Request

Regression: #42044

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-29 23:01:32 +02:00
Niels Laute
48b70e0861 [CmdPal Settings] Improved NavView behavior (#42044)
## Summary of the Pull Request

The NavView behavior (e.g. when showing the panebutton, collapsing the
menu etc.) was inconsistent with other Settings experiences (like PT
Settings and W11 Settings).

This PR makes use of the TitleBar's PaneToggleButton.

## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2025-09-29 17:26:24 +02:00
Jiří Polášek
08dc3fbcef CmdPal: Fix desynced resmanager files (#42038)
## Summary of the Pull Request

This PR fixes desynced resource manager files introduced by previous
commits.
While Visual Studio would regenerate and correct these files
automatically, applying this fix preemptively reduces unnecessary churn
in unrelated commits and avoids redundant file changes.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-29 10:09:42 -05:00
Jiří Polášek
eeb84cb621 Dependencies: Upgrade WinUIEx to 2.8.0 (#40639)
## Summary of the Pull Request

This change upgrades the WinUIEx NuGet package from version 2.2.0 to
2.8.0.
- Prevents the window itself from taking focus when it should not.
- Removes dead code from Settings.UI (the code triggered error
[WinUIEX1001](https://dotmorten.github.io/WinUIEx/rules/WinUIEx1001.html)
-- Window.Current is always null).
## PR Checklist

- [x] Closes: #40637
- [x] Closes: #7647
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

I've built and run [projects utilizing
WinUIEx](https://github.com/search?q=repo%3Amicrosoft%2FPowerToys+WinUIEx+path%3A*.*proj&type=code):
- Microsoft.CmdPal.UI
- MeasureToolUI
- FileLocksmithUI
- EnvironmentVariables
- AdvancedPaste
- Peek.UI
- RegistryPreview
- PowerToys.Settings
- Hosts
2025-09-29 08:43:57 -05:00
Gordon Lam
5b2388cd58 Add missing sign dll as part of build (#42075)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request updates the `.pipelines/ESRPSigning_core.json` file to
include additional DLLs for code signing. These changes ensure that new
dependencies are properly signed as part of the build and deployment
process.

**Added DLLs for signing:**
* Added `CommunityToolkit.WinUI.Controls.MarkdownTextBlock.dll` and its
`WinUI3Apps` version to the list of files to be signed.
* Added `Markdig.dll` and its `WinUI3Apps` version to the list of files
to be signed.
* Added `RomanNumerals.dll` and its `WinUI3Apps` version to the list of
files to be signed.
2025-09-29 18:17:44 +08:00
Mike Hall
a8596fed3d Add key to cancel gliding cursor (#41985)
## Summary of the Pull Request
Add low level keyboard hook to Gliding Cursor, this checks for 'Esc'
being pressed when gliding is active and cancels gliding, the mouse hook
passed keys down the hook chain.

## PR Checklist

- [x] Closes: #41972
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

## Detailed Description of the Pull Request / Additional comments
No changes to the list of binaries, no new strings for localization. The
Gliding Cursor functionality has 5 stages, these are: fast horizontal,
slow horizontal, fast vertical, slow vertical, and mouse click - adding
the keyboard hook and checking for 'Esc' allows this sequence to be
interrupted and reset to ready state.

## Validation Steps Performed
Validated Mouse Pointer Crosshairs (which Gliding Cursor is based on),
confirmed that Gliding Cursor functionality is unchanged and that the
'Esc' key cancels/resets the gliding cursor state.

---------

Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
2025-09-29 12:14:16 +08:00
Mike Hall
faf7c7f1a1 add horizontal and vertical options for MousePointerCrosshairs (#41789)
## Summary of the Pull Request
This PR addresses two logged issues for MousePointerCrosshairs, these
are:
https://github.com/microsoft/PowerToys/issues/24944 
https://github.com/microsoft/PowerToys/issues/31817

## PR Checklist

- [x] Closes: #24944, #31817
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

## Detailed Description of the Pull Request / Additional comments
The PR adds a new combo box to MousePointerCrosshairs XAML options, this
gives the option for 'both', 'vertical only' or 'horizontal only'. The
default option is 'both' which mirrors the existing behavior.

## Validation Steps Performed
Validation has been completed on two separate PCs, a Surface laptop 7
Pro and a Dell Workstation.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-29 10:32:07 +08:00
Dave Rayment
3b4007d299 [PowerRename] Fix issue where counter does not update if the filename and replacement result matched (#42006)
This PR fixes an issue which has been reported a number of times under
slightly different guises. The bug manifests as a counter "stall" or
"skip" under certain circumstances, not advancing the counter if the
result of the rename operation happens to match the original filename.

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This issue occurs if all the following are true:
- Enumeration features are enabled
- Regular expression matching is enabled
- The replacement string includes a counter, for example `${}` or
`${start=1}` or `${start=10,increment=10}` etc.
- There are one or more original filenames which coincide with the
result of the renaming operations

Previously, the counter was not updated when the renaming operation
result was the same as the original filename. For example, here the
first rename result matches the original filename and the counter
remains at `1` for the second file, whereas it should be `2`:

<img width="1002" height="759" alt="image"
src="https://github.com/user-attachments/assets/2766f448-adc3-4fe7-9c13-f4c5505ae1d9"
/>

This fix increments the counter irrespective of these coincidences.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41950, #31950, #33884
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

**Before discussing the detail of the fix, I'd like to acknowledge the
incredible coincidence that it resolves two issues (31950 and 41950)
_which are exactly 10,000 issues (and 18 months) apart_.**

Now, back to the issue at hand, if you'll forgive the pun...

The original flawed code is here:

```cpp
    bool replacedSomething = false;
    if (m_flags & UseRegularExpressions)
    {
        replaceTerm = regex_replace(replaceTerm, zeroGroupRegex, L"$1$$$0");
        replaceTerm = regex_replace(replaceTerm, otherGroupsRegex, L"$1$0$4");

        res = RegexReplaceDispatch[_useBoostLib](source, m_searchTerm, replaceTerm, m_flags & MatchAllOccurrences, !(m_flags & CaseSensitive));
        replacedSomething = originalSource != res;
    }
```

Here `replacedSomething` controls whether the counter variable is
incremented later in the code. The problem lies in the assumption that
only a replacement result which differs from the original string implies
a match. This is incorrect, as a successful match and replace operation
can still produce a result which coincides with `originalSource` (the
source filename), i.e. `originalSource == res`.

The solution is to separate the regex matching from the replacement
operation, and to advance the counter when the match is successful
irrespective of whether the result is the same as the filename. This is
what the fix does:

```cpp
    bool shouldIncrementCounter = false;
    if (m_flags & UseRegularExpressions)
    {
        replaceTerm = regex_replace(replaceTerm, zeroGroupRegex, L"$1$$$0");
        replaceTerm = regex_replace(replaceTerm, otherGroupsRegex, L"$1$0$4");

        res = RegexReplaceDispatch[_useBoostLib](source, m_searchTerm, replaceTerm, m_flags & MatchAllOccurrences, !(m_flags & CaseSensitive));

        // Use regex search to determine if a match exists. This is the basis for incrementing
        // the counter.
        if (_useBoostLib)
        {
            boost::wregex pattern(m_searchTerm, boost::wregex::ECMAScript | (!(m_flags & CaseSensitive) ? boost::wregex::icase : boost::wregex::normal));
            shouldIncrementCounter = boost::regex_search(sourceToUse, pattern);
        }
        else
        {
            auto regexFlags = std::wregex::ECMAScript;
            if (!(m_flags & CaseSensitive))
            {
                regexFlags |= std::wregex::icase;
            }
            std::wregex pattern(m_searchTerm, regexFlags);
            shouldIncrementCounter = std::regex_search(sourceToUse, pattern);
        }
    }
```

The `regex_search()` call on both the boost and std paths tests whether
the regex pattern can be found in the original filename (`sourceToUse`).
`shouldIncrementCounter` tracks whether the counter will be incremented
later, renamed from `replacedSomething` to reflect the change in
behaviour.

## Validation Steps Performed

The fix includes an additional unit test for both the std and boost
regex paths. Without the fix, the test fails:

<img width="1509" height="506" alt="Screenshot 2025-09-25 063611"
src="https://github.com/user-attachments/assets/14dbf817-b1d3-456d-80f2-abcd28266b8d"
/>

and with the fix, all tests pass:

<img width="897" height="492" alt="Screenshot 2025-09-25 063749"
src="https://github.com/user-attachments/assets/9a587495-d54c-47d3-bc55-ccc64a805a88"
/>
2025-09-29 10:05:21 +08:00
Copilot
296d8f87b6 Fix: Prevent backup directory creation during dry runs (#41460)
## Summary

This PR fixes an issue where the backup folder was being created
unnecessarily when users navigated to the General tab in PowerToys
Settings, even when no actual backup had been triggered.

## Problem

When opening PowerToys Settings and navigating to the General tab, the
backup directory (default: `~/Documents/PowerToys/Backup`) was
automatically created, even though no backup operation had been
performed. This caused confusion for users setting up PowerToys on new
devices, as they would always need to manually clean up the unwanted
default folder when configuring a custom backup path.

## Root Cause

The issue occurred because:
1. Loading the General tab triggers `RefreshBackupRestoreStatus()` 
2. This calls `DryRunBackup()` to check backup status
3. `DryRunBackup()` executes `BackupSettingsInternal()` with
`dryRun=true`
4. However, the directory creation logic (`TryCreateDirectory`) was
running regardless of the dry run flag

## Solution

The fix ensures that directory creation only happens during actual
backup operations:

**Primary Change**: Wrapped backup directory creation in a dry run
check:
```csharp
// Only create the backup directory if this is not a dry run
if (!dryRun)
{
    var dirExists = TryCreateDirectory(settingsBackupAndRestoreDir);
    if (!dirExists)
    {
        Logger.LogError($"Failed to create dir {settingsBackupAndRestoreDir}");
        return (false, $"General_SettingsBackupAndRestore_BackupError", "Error", lastBackupExists, "\n" + settingsBackupAndRestoreDir);
    }
}
```

**Consistency Change**: Also moved temporary directory creation inside
dry run checks to maintain consistent behavior throughout the backup
process.

## Impact

-  **General tab loading**: No longer creates unwanted backup
directories
-  **Actual backup functionality**: Remains completely unchanged
-  **User experience**: Clean setup without unwanted default folders
-  **No breaking changes**: All existing backup/restore features work
as before

## Testing

Created comprehensive tests to validate:
- Dry runs (General tab loading) don't create directories
- Actual backup operations create directories as expected
- No regression in existing backup/restore functionality

Fixes #38620.

> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `i1qvsblobprodcus353.vsblob.vsassets.io`
> - Triggering command: `dotnet build
src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj -c Debug
--nologo` (dns block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/microsoft/PowerToys/settings/copilot/coding_agent)
(admins only)
>
> </details>



<!-- START COPILOT CODING AGENT TIPS -->
---

 Let Copilot coding agent [set things up for
you](https://github.com/microsoft/PowerToys/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yeelam-gordon <73506701+yeelam-gordon@users.noreply.github.com>
2025-09-29 09:11:28 +08:00
Shawn Yuan
8d4ed04f1a ignore holtkey conflict (#41729)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This PR implements functionality to ignore specific hotkey conflicts in
PowerToys settings. The primary purpose is to allow users to suppress
individual shortcut conflict warnings if they find their configurations
work correctly despite the detected conflicts.


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41544
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
- Added hotkey conflict ignore functionality with user-controllable
settings
- Updated shortcut control UI to support ignore states and clearer
conflict messaging
- Enhanced conflict detection to respect ignored shortcuts when counting
conflicts

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
2025-09-29 08:53:07 +08:00
moooyo
b026bf5be2 [CmdPal] Enable AOT by default (#41350)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
It's time to enable AOT by default and clean up the related
configuration.
PublishReadyToRun and PublishTrimmed are unnecessary now.

By clean up these configuration, cmdpal will:
1. Run without AOT in the VS by default.
2. Build with AOT by default in the Pipeline.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
2025-09-29 07:43:29 +08:00
AmirMS
f1367bfa17 Initial DSC v3 support for PowerToys (#41132)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

Tasks checklist
- [X] Implement DSC infra in PowerToys
- [X] Implement Settings DSC resource
  - [X] Implement Get, Set, Test, Export, Schema
  - [X] Generate manifest (DSC resource JSON)
- [X] Added Unit Tests
- [x] Add `NJsonSchema` v11.4.0 to the stream
- [x] Package the manifest files so dsc.exe can discover them
- [x] Add `PowerToys.DSC.exe` to the PATH (maybe?)
- [x] Add `InstallLocation` in the registry key so `winget configue
export` can export the PowerToys DSC resources
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [X] Closes: #37276
- [X] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [X] **Tests:** Added/updated and all pass
- [X] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** Added/updated
- [x] **New binaries:** Added on the required places
- [x] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [x] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [x] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [x] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [x] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: vanzue <vanzue@outlook.com>
Co-authored-by: Kai Tao (from Dev Box) <kaitao@microsoft.com>
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2025-09-28 15:12:51 +08:00
Michael Jolley
3145b39d42 CmdPal: Links in details will now wrap correctly. (#42036)
See title

Closes #39649
2025-09-27 20:07:02 +02:00
Niels Laute
ef131fd73b Fix formatting in README.md (#42045)
Removed extra newline before the Microsoft PowerToys header.

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-27 20:05:15 +02:00
Jiří Polášek
348af2d6fc CmdPal: Update web search extensions icons (#42034)
## Summary of the Pull Request

This PR updates icons in the Web Search extension:
- Replaces the main icon with a new one based on Fluent UI System Color
Icons. The globe’s color shifts slightly from turquoise to bluish to
increase contrast between the Web Search extension icon and the
Clipboard History icon.
- Fixes the cutoff on the right edge of the original extension icon.
- Updates the history item icon to the Fluent UI History icon.
- Adds a search command icon using the Fluent UI Search icon.

![websearch
icons](https://github.com/user-attachments/assets/46d62cfc-c1ac-4634-9f36-caaef09c5370)

<img width="1678" height="1029" alt="image"
src="https://github.com/user-attachments/assets/a1072036-cae5-44da-9666-700df7a4642f"
/>

<img width="1784" height="1101" alt="image"
src="https://github.com/user-attachments/assets/e27fd7f1-0591-45ab-8685-e2629b393ec1"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #42037
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-27 11:41:40 -05:00
Niels Laute
54a1ec2fdd Readme refresh (#41896)
## Summary of the Pull Request
Refresh for the readme, removing deadlinks and making things a bit more
structured and easier to find.

Preview link:
https://github.com/microsoft/PowerToys/tree/niels9001/readme-update

## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-27 17:02:40 +02:00
Niels Laute
9d6891a307 [Regression] Re-add missing button styles (#42027)
## Summary of the Pull Request

By merging #41900, we removed too many styles :). This brings back the
missing styles.

## PR Checklist

- [ ] Closes: #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-27 15:47:04 +02:00
Mike Griese
c3398b0a01 Allow Run to handle commandlines with spaces (#42016)
This better handles cases where commandlines might have embedded spaces.
For something like

```
C:\Program Files\PowerShell\7\pwsh.exe -c write-host dawg
```

we'll now see that `C:\Program` isn't a file, and we'll try to look at
`C:\Program Files\PowerShell\7\pwsh.exe` instead.

This code is pilfered from
https://github.com/microsoft/terminal/pull/12348 which fixed
https://github.com/microsoft/terminal/issues/12345. Terminal has great
code for normalizing a string into an executable and args, so why not
just use it here.

related to #41646
related to #41705 (but much more narrowly scoped)

----

I added some tests too.

drive-by fix: as I was adding tests, I added a helper for "make a change
to a page, and await the page's ItemsChanged". This removes a bunch of
`await 1s` calls, and brings the shell page tests from like, 7s to 500ms
2025-09-26 18:39:00 -05:00
Jiří Polášek
2b6c5d2cdd CmdPal: Hide search box on content pages [experiment] (#41479)
## Summary of the Pull Request

CmdPal now displays the search box only on pages that derive from
ListPage. On ContentPage (forms, etc.), the search box is hidden.

- Moves keyboard shortcut handling from SearchBox to ShellPage so
shortcuts are always handled.
- Keeps the search box hidden/disabled to preserve layout metrics. 
- Refines focus management to prevent focus jumps during navigation. 
- For ContentPage page's content gains focus automatically (not just
form inputs, but now markdown content as well - so you can scroll
immediately, for example).
- Adds accessibility (a11y) tweaks: when navigating to content pages
without a visible search box, sets an explicit focus target so screen
readers announce a meaningful element. Screen reader will now announce
navigation to the page.
- Adds a title to the main list page - "Home".


https://github.com/user-attachments/assets/f60d0826-df1f-468e-8e41-0266cd27878b


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #38967
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-26 16:00:53 -05:00
Michael Jolley
e1681ec08f CmdPal: Add Context Menu command "Show Details" when list item has details, but list view's ShowDetails == false (#40870)
Closes #38270

When a list item's `Details` property is not null, but it's parent
ListViews `ShowDetails` property is false, this PR adds a context menu
item at the bottom of the commands for the list item to show details.
Clicking that command will show the details for the selected item, but
the details pane will hide when a different item is selected.

## Preview


https://github.com/user-attachments/assets/7b5cd3d4-b4ae-433a-ad25-f620590cd261

---------

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-09-26 19:46:09 +02:00
Michael Jolley
744415f20a CmdPal: Limiting length of primary/secondary commands (#41396)
Closes #41365

Limits width of primary/secondary commands to 160 and trims with
ellipsis.
2025-09-26 19:04:47 +02:00
Michael Jolley
3bdb5305ba CmdPal: Linq clean-up (All Apps) (#41551)
Beginning the process of removing System.Linq from CmdPal. This PR
removes it from the All Apps extension.
2025-09-26 11:15:34 -05:00
Jiří Polášek
7b7bae2889 CmdPal: Enable loading local images in MarkdownContent (#41754)
Add a new image provider for `MarkdownTextBlock` that allows loading
images from additional sources:

- **file scheme**  
  - Enables loading images using the `file:` scheme.  
- Intentionally restricts file URIs to absolute paths to ensure correct
resolution when passed through the CmdPal extension/host boundary. (In
most cases, 3rd-party extensions will provide the paths, but the CmdPal
host performs the actual loading and would otherwise resolve paths
relative to itself.)

- **data scheme**  
- Enables loading images from URIs with the `data:` scheme (both Base64
and URL-encoded forms).
- Note: the Markdown control itself cannot handle large input and may
hang before the code introduced in this PR is invoked.

- **ms-appx scheme**  
  - This scheme is now supported for loading images.  
- However, since the Command Palette host performs the loading,
`ms-appx:` resolution applies to the host and not the extensions, which
limits its usefulness.

- **ms-appdata scheme**  
  - This scheme is now supported for loading images.  
- Similar to `ms-appx:`, resolution applies to the host, not the
extensions, limiting its usefulness.

---

Additionally, this PR introduces the concept of **_image source
hints_**, implemented as query string parameters piggy-backed on the
original URI.
These hints allow users to influence the behavior of images within
Markdown content.

- `--x-cmdpal-fit`
  - `none`: no automatic scaling, provides image as is (default)
  - `fit`: scale to fit the available space
- `--x-cmdpal-upscale`
  - `true`: allow upscaling
  - `false`: downscale only (default)
- `--x-cmdpal-width`: desired width in pixels
- `--x-cmdpal-height`: desired height in pixels
- `--x-cmdpal-maxwidth`: max width in pixels
- `--x-cmdpal-maxheight`: max height in pixels 

--- 

Since `MarkdownTextBlock` requires conforming to the `IImageProvider`
interface—which accepts only a raw URI and must return an `Image`
control—this PR also introduces a new class `RtbInlineImageFactory`.

The factory hooks into the root text block upon loading and listens for
events related to **layout** and **DPI changes**, ensuring that images
adapt correctly to the control’s environment.

```csharp
public interface IImageProvider
{
    Task<Image> GetImage(string url);
    bool ShouldUseThisProvider(string url);
}
```

---
Pictures? Videos!

Loading images from new schemes:


https://github.com/user-attachments/assets/e0f4308d-30b2-4c81-86db-353048c708c1

New image source scaling options:


https://github.com/user-attachments/assets/ec5b007d-3140-4f0a-b163-7b278233ad40


<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41752
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Michael Jolley <mike@baldbeardedbuilder.com>
2025-09-26 10:59:00 -05:00
Michael Jolley
a1c8541d8b Materializing lists to prevent rescoring (#42017)
Read title. Making MainListPage materialize search results so
enumerating doesn't rescore the item.
2025-09-26 09:59:23 -05:00
Mike Griese
2e0fe16128 CmdPal: Move core projects into Core/ (#41358)
Couple little things here:

* Makes `Microsoft.CmdPal.Common` a `Core` project
* Moves the `CmdPal.Core` projects into a single `Core/` directory
* Adds the `CoreLogger` which I had stashed in
https://github.com/microsoft/PowerToys/compare/dev/migrie/40113/extension-hosts-try-2...dev/migrie/b/remove-core-managedcommon-dep
a while back
* De-duplicates a bunch of commands that were in both Apps and Common
* moves all the commands into the toolkit, instead of in the Common
project
2025-09-26 08:05:37 -05:00
465 changed files with 27711 additions and 1666 deletions

View File

@@ -131,3 +131,4 @@
ignore$
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$
^src/common/CalculatorEngineCommon/exprtk\.hpp$
src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage.cs

View File

@@ -160,6 +160,7 @@ BUILDARCH
BUILDNUMBER
buildtransitive
builttoroam
BUNDLEINFO
BVal
BValue
byapp
@@ -393,6 +394,7 @@ DNLEN
DONOTROUND
DONTVALIDATEPATH
dotnet
downscale
DPICHANGED
DPIs
DPSAPI
@@ -862,6 +864,7 @@ LOCKTYPE
LOGFONT
LOGFONTW
logon
LOGMSG
LOGPIXELSX
LOGPIXELSY
LOn
@@ -1039,6 +1042,7 @@ MWBEx
MYICON
NAMECHANGE
namespaceanddescendants
Namotion
nao
NCACTIVATE
ncc
@@ -1076,6 +1080,7 @@ NEWPLUSSHELLEXTENSIONWIN
newrow
nicksnettravels
NIF
NJson
NLog
NLSTEXT
NMAKE
@@ -1379,7 +1384,7 @@ QUNS
RAII
RAlt
randi
Rasterization
rasterization
Rasterize
RAWINPUTDEVICE
RAWINPUTHEADER

2
.gitignore vendored
View File

@@ -350,7 +350,9 @@ src/common/Telemetry/*.etl
# Generated installer file for Monaco source files.
/installer/PowerToysSetup/MonacoSRC.wxs
/installer/PowerToysSetup/DscResources.wxs
/installer/PowerToysSetupVNext/MonacoSRC.wxs
/installer/PowerToysSetupVNext/DscResources.wxs
# MSBuildCache
/MSBuildCacheLogs/

View File

@@ -55,7 +55,6 @@
"PowerToys.Awake.exe",
"PowerToys.Awake.dll",
"PowerToys.FancyZonesEditor.exe",
"PowerToys.FancyZonesEditor.dll",
"PowerToys.FancyZonesEditorCommon.dll",
@@ -230,7 +229,10 @@
"PowerToys.CmdPalModuleInterface.dll",
"CmdPalKeyboardService.dll",
"*Microsoft.CmdPal.UI_*.msix"
"*Microsoft.CmdPal.UI_*.msix",
"PowerToys.DSC.dll",
"PowerToys.DSC.exe"
],
"SigningInfo": {
"Operations": [
@@ -297,6 +299,9 @@
"msvcp140_1_app.dll",
"msvcp140_2_app.dll",
"msvcp140_app.dll",
"Namotion.Reflection.dll",
"NJsonSchema.Annotations.dll",
"NJsonSchema.dll",
"vcamp140_app.dll",
"vccorlib140_app.dll",
"vcomp140_app.dll",
@@ -322,6 +327,12 @@
"WinUI3Apps\\ReverseMarkdown.dll",
"WinUI3Apps\\SharpCompress.dll",
"WinUI3Apps\\ZstdSharp.dll",
"CommunityToolkit.WinUI.Controls.MarkdownTextBlock.dll",
"WinUI3Apps\\CommunityToolkit.WinUI.Controls.MarkdownTextBlock.dll",
"Markdig.dll",
"WinUI3Apps\\Markdig.dll",
"RomanNumerals.dll",
"WinUI3Apps\\RomanNumerals.dll",
"TestableIO.System.IO.Abstractions.dll",
"WinUI3Apps\\TestableIO.System.IO.Abstractions.dll",
"TestableIO.System.IO.Abstractions.Wrappers.dll",

View File

@@ -0,0 +1,88 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$BuildPlatform,
[Parameter(Mandatory = $true)]
[string]$BuildConfiguration,
[Parameter()]
[string]$RepoRoot = (Get-Location).Path
)
$ErrorActionPreference = 'Stop'
function Resolve-PlatformDirectory {
param(
[string]$Root,
[string]$Platform
)
$normalized = $Platform.Trim()
$candidates = @()
$candidates += Join-Path $Root $normalized
$candidates += Join-Path $Root ($normalized.ToUpperInvariant())
$candidates += Join-Path $Root ($normalized.ToLowerInvariant())
$candidates = $candidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
foreach ($candidate in $candidates) {
if (Test-Path $candidate) {
return $candidate
}
}
return $candidates[0]
}
Write-Host "Repo root: $RepoRoot"
Write-Host "Requested build platform: $BuildPlatform"
Write-Host "Requested configuration: $BuildConfiguration"
# Always use x64 PowerToys.DSC.exe since CI/CD machines are x64
$exePlatform = 'x64'
$exeRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $exePlatform
$exeOutputDir = Join-Path $exeRoot $BuildConfiguration
$exePath = Join-Path $exeOutputDir 'PowerToys.DSC.exe'
Write-Host "Using x64 PowerToys.DSC.exe to generate DSC manifests for $BuildPlatform build"
if (-not (Test-Path $exePath)) {
throw "PowerToys.DSC.exe not found at '$exePath'. Make sure it has been built first."
}
Write-Host "Using PowerToys.DSC.exe at '$exePath'."
# Output DSC manifests to the target build platform directory (x64, ARM64, etc.)
$outputRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $BuildPlatform
if (-not (Test-Path $outputRoot)) {
Write-Host "Creating missing platform output root at '$outputRoot'."
New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null
}
$outputDir = Join-Path $outputRoot $BuildConfiguration
if (-not (Test-Path $outputDir)) {
Write-Host "Creating missing configuration output directory at '$outputDir'."
New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
}
Write-Host "DSC manifests will be generated to: '$outputDir'"
Write-Host "Cleaning previously generated DSC manifest files from '$outputDir'."
Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force
$arguments = @('manifest', '--resource', 'settings', '--outputDir', $outputDir)
Write-Host "Invoking DSC manifest generator: '$exePath' $($arguments -join ' ')"
& $exePath @arguments
if ($LASTEXITCODE -ne 0) {
throw "PowerToys.DSC.exe exited with code $LASTEXITCODE"
}
$generatedFiles = Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
if ($generatedFiles.Count -eq 0) {
throw "No DSC manifest files were generated in '$outputDir'."
}
Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):"
foreach ($file in $generatedFiles) {
Write-Host " - $($file.FullName)"
}

View File

@@ -43,11 +43,6 @@ parameters:
displayName: "Build Using Visual Studio Preview"
default: false
- name: enableAOT
type: boolean
displayName: "Enable AOT (Ahead-of-Time) Compilation for CmdPal"
default: true
name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr)
variables:
@@ -109,8 +104,8 @@ extends:
useManagedIdentity: $(SigningUseManagedIdentity)
clientId: $(SigningOriginalClientId)
# Have msbuild use the release nuget config profile
additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:EnableCmdPalAOT=${{ parameters.enableAOT }} /p:InstallerSuffix=${{ parameters.installerSuffix }}
installerSuffix: ${{ parameters.installerSuffix }}
additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:InstallerSuffix=${{ parameters.installerSuffix }} /p:EnableCmdPalAOT=true
beforeBuildSteps:
# Sets versions for all PowerToy created DLLs
- pwsh: |-

View File

@@ -271,6 +271,23 @@ jobs:
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
# Build PowerToys.DSC.exe for ARM64 (x64 uses existing binary from previous build)
- task: VSBuild@1
displayName: Build PowerToys.DSC.exe (x64 for generating manifests)
condition: ne(variables['BuildPlatform'], 'x64')
inputs:
solution: src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
msbuildArgs: /t:Build /m /restore
platform: x64
configuration: $(BuildConfiguration)
msbuildArchitecture: x64
maximumCpuCount: true
# Generate DSC manifests using PowerToys.DSC.exe
- pwsh: |-
& '.pipelines/generateDscManifests.ps1' -BuildPlatform '$(BuildPlatform)' -BuildConfiguration '$(BuildConfiguration)' -RepoRoot '$(Build.SourcesDirectory)'
displayName: Generate DSC manifests
- task: CopyFiles@2
displayName: Stage SDK/build
inputs:

View File

@@ -39,6 +39,11 @@ foreach ($csprojFile in $csprojFilesArray) {
if ($csprojFile -like '*TemplateCmdPalExtension.csproj') {
continue
}
# The CmdPal.Core projects use a common shared props file, so skip them
if ($csprojFile -like '*Microsoft.CmdPal.Core.*.csproj') {
continue
}
$importExists = Test-ImportSharedCsWinRTProps -filePath $csprojFile
if (!$importExists) {

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"dotnet.defaultSolution": "disable"
}

View File

@@ -67,6 +67,7 @@
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="MSTest" Version="3.8.3" />
<PackageVersion Include="NJsonSchema" Version="11.4.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NLog" Version="5.2.8" />
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
@@ -100,6 +101,7 @@
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="System.Runtime.Caching" Version="9.0.8" />
<PackageVersion Include="System.Security.Permissions" Version="9.0.9" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.8" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.8" />
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
@@ -107,7 +109,7 @@
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
<PackageVersion Include="UnitsNet" Version="5.56.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="WinUIEx" Version="2.2.0" />
<PackageVersion Include="WinUIEx" Version="2.8.0" />
<PackageVersion Include="WPF-UI" Version="3.0.5" />
<PackageVersion Include="WyHash" Version="1.0.5" />
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />

View File

@@ -1521,6 +1521,7 @@ SOFTWARE.
- ModernWpfUI
- Moq
- MSTest
- NJsonSchema
- NLog
- NLog.Extensions.Logging
- NLog.Schema

View File

@@ -638,7 +638,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.CommandPalette.Ex
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CommandPalette.Extensions.Toolkit", "src\modules\cmdpal\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj", "{CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Common", "src\modules\cmdpal\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj", "{14E62033-58D0-4A7D-8990-52F50A08BBBD}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Core.Common", "src\modules\cmdpal\Core\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj", "{14E62033-58D0-4A7D-8990-52F50A08BBBD}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.UI", "src\modules\cmdpal\Microsoft.Terminal.UI\Microsoft.Terminal.UI.vcxproj", "{6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}"
EndProject
@@ -728,7 +728,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerRename.UITests", "src\
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Core.ViewModels", "src\modules\cmdpal\Microsoft.CmdPal.Core.ViewModels\Microsoft.CmdPal.Core.ViewModels.csproj", "{24133F7F-C1D1-DE04-EFA8-F5D5467FE027}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Core.ViewModels", "src\modules\cmdpal\Core\Microsoft.CmdPal.Core.ViewModels\Microsoft.CmdPal.Core.ViewModels.csproj", "{24133F7F-C1D1-DE04-EFA8-F5D5467FE027}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0E556541-6A45-42CB-AE49-EE5A9BE05E7C}"
EndProject
@@ -797,6 +797,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "v3", "v3", "{9605B84E-FAC4-477B-B9EC-0753177EE6A8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerToys.DSC", "src\dsc\v3\PowerToys.DSC\PowerToys.DSC.csproj", "{94CDC147-6137-45E9-AEDE-17FF809607C0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerToys.DSC.UnitTests", "src\dsc\v3\PowerToys.DSC.UnitTests\PowerToys.DSC.UnitTests.csproj", "{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}"
@@ -2891,6 +2897,22 @@ Global
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.Build.0 = Release|ARM64
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.ActiveCfg = Release|x64
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.Build.0 = Release|x64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|ARM64.ActiveCfg = Debug|ARM64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|ARM64.Build.0 = Debug|ARM64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|x64.ActiveCfg = Debug|x64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|x64.Build.0 = Debug|x64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|ARM64.ActiveCfg = Release|ARM64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|ARM64.Build.0 = Release|ARM64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|x64.ActiveCfg = Release|x64
{94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|x64.Build.0 = Release|x64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|ARM64.ActiveCfg = Debug|ARM64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|ARM64.Build.0 = Debug|ARM64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|x64.ActiveCfg = Debug|x64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|x64.Build.0 = Debug|x64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|ARM64.ActiveCfg = Release|ARM64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|ARM64.Build.0 = Release|ARM64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|x64.ActiveCfg = Release|x64
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|x64.Build.0 = Release|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64
@@ -3160,7 +3182,7 @@ Global
{F3D09629-59A2-4924-A4B9-D6BFAA2C1B49} = {3846508C-77EB-4034-A702-F8BB263C4F79}
{305DD37E-C85D-4B08-AAFE-7381FA890463} = {F3D09629-59A2-4924-A4B9-D6BFAA2C1B49}
{CA4D810F-C8F4-4B61-9DA9-71807E0B9F24} = {F3D09629-59A2-4924-A4B9-D6BFAA2C1B49}
{14E62033-58D0-4A7D-8990-52F50A08BBBD} = {7520A2FE-00A2-49B8-83ED-DB216E874C04}
{14E62033-58D0-4A7D-8990-52F50A08BBBD} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{6515F03F-E56D-4DB4-B23D-AC4FB80DB36F} = {7520A2FE-00A2-49B8-83ED-DB216E874C04}
{071E18A4-A530-46B8-AB7D-B862EE55E24E} = {3846508C-77EB-4034-A702-F8BB263C4F79}
{C846F7A7-792A-47D9-B0CB-417C900EE03D} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
@@ -3238,6 +3260,9 @@ Global
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E11826E1-76DF-42AC-985C-164CC2EE57A1} = {7AC943C9-52E8-44CF-9083-744D8049667B}
{66C069F8-C548-4CA6-8CDE-239104D68E88} = {E11826E1-76DF-42AC-985C-164CC2EE57A1}
{9605B84E-FAC4-477B-B9EC-0753177EE6A8} = {557C4636-D7E1-4838-A504-7D19B725EE95}
{94CDC147-6137-45E9-AEDE-17FF809607C0} = {9605B84E-FAC4-477B-B9EC-0753177EE6A8}
{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78} = {9605B84E-FAC4-477B-B9EC-0753177EE6A8}
{E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}

148
README.md
View File

@@ -1,38 +1,56 @@
# Microsoft PowerToys
<p align="center">
<picture>
<source media="(prefers-color-scheme: light)" srcset="./doc/images/readme/pt-hero.light.png" />
<img src="./doc/images/readme/pt-hero.dark.png" />
</picture>
</p>
<h1 align="center">
<span>Microsoft PowerToys</span>
</h1>
![Hero image for Microsoft PowerToys](doc/images/overview/PT_hero_image.png)
<h3 align="center">
<a href="#-installation">Installation</a>
<span> · </span>
<a href="https://aka.ms/powertoys-docs">Documentation</a>
<span> · </span>
<a href="https://aka.ms/powertoys-releaseblog">Blog</a>
<span> · </span>
<a href="#-whats-new">Release notes</a>
</h3>
<br/><br/>
Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks.
<br/><br/>
[How to use PowerToys][usingPowerToys-docs-link] | [Downloads & Release notes][github-release-link] | [Contributing to PowerToys](#contributing) | [What's Happening](#whats-happening) | [Roadmap](#powertoys-roadmap)
| | | |
|---|---|---|
| [<img src="doc/images/icons/AdvancedPaste.png" alt="Advanced Paste icon" height="16"> Advanced Paste](https://aka.ms/PowerToysOverview_AdvancedPaste) | [<img src="doc/images/icons/Always%20On%20Top.png" alt="Always on Top icon" height="16"> Always on Top](https://aka.ms/PowerToysOverview_AoT) | [<img src="doc/images/icons/Awake.png" alt="Awake icon" height="16"> Awake](https://aka.ms/PowerToysOverview_Awake) |
| [<img src="doc/images/icons/Color%20Picker.png" alt="Color Picker icon" height="16"> Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [<img src="doc/images/icons/Command%20Not%20Found.png" alt="Command Not Found icon" height="16"> Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [<img src="doc/images/icons/Command Palette.png" alt="Command Palette icon" height="16"> Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
| [<img src="doc/images/icons/Crop%20And%20Lock.png" alt="Crop and Lock icon" height="16"> Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [<img src="doc/images/icons/Environment%20Manager.png" alt="Environment Variables icon" height="16"> Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [<img src="doc/images/icons/FancyZones.png" alt="FancyZones icon" height="16"> FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
| [<img src="doc/images/icons/File%20Explorer%20Preview.png" alt="File Explorer Add-ons icon" height="16"> File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [<img src="doc/images/icons/File%20Locksmith.png" alt="File Locksmith icon" height="16"> File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [<img src="doc/images/icons/Host%20File%20Editor.png" alt="Hosts File Editor icon" height="16"> Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
| [<img src="doc/images/icons/Image%20Resizer.png" alt="Image Resizer icon" height="16"> Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [<img src="doc/images/icons/Keyboard%20Manager.png" alt="Keyboard Manager icon" height="16"> Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [<img src="doc/images/icons/Find My Mouse.png" alt="Mouse Utilities icon" height="16"> Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
| [<img src="doc/images/icons/MouseWithoutBorders.png" alt="Mouse Without Borders icon" height="16"> Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [<img src="doc/images/icons/NewPlus.png" alt="New+ icon" height="16"> New+](https://aka.ms/PowerToysOverview_NewPlus) | [<img src="doc/images/icons/Peek.png" alt="Peek icon" height="16"> Peek](https://aka.ms/PowerToysOverview_Peek) |
| [<img src="doc/images/icons/PowerRename.png" alt="PowerRename icon" height="16"> PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [<img src="doc/images/icons/PowerToys%20Run.png" alt="PowerToys Run icon" height="16"> PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [<img src="doc/images/icons/PowerAccent.png" alt="Quick Accent icon" height="16"> Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) |
| [<img src="doc/images/icons/Registry%20Preview.png" alt="Registry Preview icon" height="16"> Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [<img src="doc/images/icons/MeasureTool.png" alt="Screen Ruler icon" height="16"> Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [<img src="doc/images/icons/Shortcut%20Guide.png" alt="Shortcut Guide icon" height="16"> Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) |
| [<img src="doc/images/icons/PowerOCR.png" alt="Text Extractor icon" height="16"> Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [<img src="doc/images/icons/Workspaces.png" alt="Workspaces icon" height="16"> Workspaces](https://aka.ms/PowerToysOverview_Workspaces) | [<img src="doc/images/icons/ZoomIt.png" alt="ZoomIt icon" height="16"> ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) |
## About
Microsoft PowerToys is a set of utilities for power users to tune and streamline their Windows experience for greater productivity. For more info on [PowerToys overviews and how to use the utilities][usingPowerToys-docs-link], or any other tools and resources for [Windows development environments](https://learn.microsoft.com/windows/dev-environment/overview), head over to [learn.microsoft.com][usingPowerToys-docs-link]!
## 📋 Installation
| | Current utilities: | |
|--------------|--------------------|--------------|
| [Advanced Paste](https://aka.ms/PowerToysOverview_AdvancedPaste) | [Always on Top](https://aka.ms/PowerToysOverview_AoT) | [PowerToys Awake](https://aka.ms/PowerToysOverview_Awake) |
| [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
| [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
| [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
| [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
| [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [New+](https://aka.ms/PowerToysOverview_NewPlus) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) |
| [Peek](https://aka.ms/PowerToysOverview_Peek) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) |
| [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) |
| [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [Workspaces](https://aka.ms/PowerToysOverview_Workspaces) |
| [ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) |
For detailed installation instructions, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install).
## Installing and running Microsoft PowerToys
Before you begin, make sure your device meets the system requirements:
### Requirements
> [!NOTE]
> - Windows 11 or Windows 10 version 2004 (20H1 / build 19041) or newer
> - 64-bit processor: x64 or ARM64
> - Latest stable version of [Microsoft Edge WebView2 Runtime](https://go.microsoft.com/fwlink/p/?LinkId=2124703) is installed via the bootstrapper during setup
- Windows 11 or Windows 10 version 2004 (code name 20H1 / build number 19041) or newer.
- x64 or ARM64 processor
- Our installer will install the following items:
- [Microsoft Edge WebView2 Runtime](https://go.microsoft.com/fwlink/p/?LinkId=2124703) bootstrapper. This will install the latest version.
Choose one of the installation methods below:
### Via GitHub with EXE [Recommended]
<details>
<summary>Download .exe from GitHub</summary>
Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
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.95%22
@@ -49,57 +67,49 @@ Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and cl
| Machine wide - x64 | [PowerToysSetup-0.94.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.94.0-arm64.exe][ptMachineArm64] |
This is our preferred method.
</details>
### Via Microsoft Store
<details>
<summary>Microsoft Store</summary>
You can easily install PowerToys from the Microsoft Store:
<p>
<a style="text-decoration:none" href="https://aka.ms/getPowertoys">
<picture>
<source media="(prefers-color-scheme: light)" srcset="doc/images/readme/StoreBadge-dark.png" width="148" />
<img src="doc/images/readme/StoreBadge-light.png" width="148" />
</picture></a>
</p>
</details>
Install from the [Microsoft Store's PowerToys page][microsoft-store-link]. You must be using the [new Microsoft Store](https://blogs.windows.com/windowsExperience/2021/06/24/building-a-new-open-microsoft-store-on-windows-11/), which is available for both Windows 11 and Windows 10.
### Via WinGet
<details>
<summary>WinGet</summary>
Download PowerToys from [WinGet][winget-link]. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
#### User scope installer [default]
*User scope installer [default]*
```powershell
winget install Microsoft.PowerToys -s winget
```
#### Machine-wide scope installer
*Machine-wide scope installer*
```powershell
winget install --scope machine Microsoft.PowerToys -s winget
```
</details>
### Other install methods
<details>
<summary>Other methods</summary>
There are [community driven install methods](./doc/unofficialInstallMethods.md) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
</details>
## Third-Party Run Plugins
## ✨ What's new
**Version 0.94 (September 2025)**
There is a collection of [third-party plugins](./doc/thirdPartyRunPlugins.md) created by the community that aren't distributed with PowerToys.
For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
## Contributing
This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows.
We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort.
Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so.
For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
## What's Happening
### PowerToys Roadmap
Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
### 0.94 - Sep 2025 Update
In this release, we focused on new features, stability, optimization improvements, and automation.
For an in-depth look at the latest changes, visit the [release blog](https://aka.ms/powertoys-releaseblog).
**✨Highlights**
**✨ Highlights**
- PowerToys Settings added a Settings search with fuzzy matching, suggestions, a results page, and UX polish to make finding options faster.
- A comprehensive hotkey conflict detection system was introduced in Settings to surface and help resolve conflicting shortcuts. Note that the default hotkey settings (Win+Ctrl+Shift+T, Win+Ctrl+V, Win+Ctrl+T, Win+Shift+T) may overlap with existing Windows system shortcuts. This is expected. You can resolve the conflict by assigning different hotkeys.
@@ -138,13 +148,13 @@ For an in-depth look at the latest changes, visit the [release blog](https://aka
- Allowed providers to override Dispose with a virtual method.
- Fixed memory leaks by cleaning up removed or cancelled list items.
- Sorted DateTime extension results by relevance for better usability.
- Reduced search text jiggling by avoiding redundant change notifications.
- Reduced search text "jiggling" by avoiding redundant change notifications.
- Centralized automation notifications in a UIHelper for better accessibility. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- Preserved Adaptive Card action types during trimming via DynamicDependency.
- Added an acrylic backdrop and refined styling to the context menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Prevented disposed pages and Settings windows from handling stale messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made the extension API easier to evolve without breaking clients.
- Added evil sample pages to help reproduce tricky bugs.
- Added "evil" sample pages to help reproduce tricky bugs.
- Fixed WinGet trim-safety issues by replacing LINQ with manual iteration.
- Cancelled stale list fetches to avoid older results overwriting newer ones in CmdPal.
@@ -220,10 +230,10 @@ For an in-depth look at the latest changes, visit the [release blog](https://aka
- Rewrote system command tests with a new test base and cleaner patterns.
- Added unit tests for WebSearch and Shell extensions with mockable settings.
- Added unit tests and abstractions for Apps and Bookmarks extensions.
- Cleans up AIgenerated tests; adds meaningful query tests across extensions.
- Cleans up AI-generated tests; adds meaningful query tests across extensions.
- Removed the obsolete debug dialog from Settings for a smoother developer loop.
### What is being planned over the next few releases
## 🛣️ Roadmap
For [v0.95][github-next-release-work], we'll work on the items below:
@@ -235,9 +245,19 @@ For [v0.95][github-next-release-work], we'll work on the items below:
- New UI automation tests
- Stability, bug fixes
## PowerToys Community
## ❤️ PowerToys Community
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldnt be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Month by month, you directly help make PowerToys a better piece of software.
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!
## Contributing
This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows.
We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort.
Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so.
For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
## Code of Conduct

83
doc/dsc/Settings.md Normal file
View File

@@ -0,0 +1,83 @@
# Settings resource
Manage the settings for PowerToys modules
## Commands
### ✨ Modules
List all the modules supported by the settings resource.
```shell
PS C:\> PowerToys.DSC.exe modules --resource 'settings'
AdvancedPaste
AlwaysOnTop
App
Awake
ColorPicker
CropAndLock
EnvironmentVariables
FancyZones
FileLocksmith
FindMyMouse
Hosts
ImageResizer
KeyboardManager
MeasureTool
MouseHighlighter
MouseJump
MousePointerCrosshairs
Peek
PowerAccent
PowerOCR
PowerRename
RegistryPreview
ShortcutGuide
Workspaces
ZoomIt
```
### 📄 Get
Get the settings for a specific module.
```shell
PS C:\> PowerToys.DSC.exe get --resource 'settings' --module EnvironmentVariables
{"settings":{"properties":{"LaunchAdministrator":{"value":true}},"name":"EnvironmentVariables","version":"1.0"}}
```
### 🖨️ Export
Export the settings for a specific module.
Settings resource Get and Export operation output states are identical.
```shell
PS C:\> PowerToys.DSC.exe get --resource 'settings' --module EnvironmentVariables
{"settings":{"properties":{"LaunchAdministrator":{"value":true}},"name":"EnvironmentVariables","version":"1.0"}}
```
### 📝 Set
Set the settings for a specific module. This command will update the settings to the specified values.
```shell
PS C:\> PowerToys.DSC.exe set --resource 'settings' --module Awake --input '{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}'
{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}
["settings"]
```
### 🧪 Test
Test the settings for a specific module. This command will check if the current settings match the desired state.
```shell
PS C:\> PowerToys.DSC.exe test --resource 'settings' --module Awake --input '{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000002-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}'
{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"},"_inDesiredState":false}
["settings"]
```
### 🛠️ Schema
Generates the JSON schema for the settings resource of a specific module.
```shell
PS C:\> PowerToys.DSC.exe schema --resource 'settings' --module Awake
{"$schema":"http://json-schema.org/draft-04/schema#","title":"SettingsResourceObjectOfAwakeSettings","type":"object","additionalProperties":false,"required":["settings"],"properties":{"_inDesiredState":{"type":["boolean","null"],"description":"Indicates whether an instance is in the desired state"},"settings":{"description":"The settings content for the module."}}}
PS E:\src\powertoys> PowerToys.DSC.exe schema --resource 'settings' --module Awake | Format-Json
```
### 📦 Manifest
Generates a manifest dsc resource JSON file for the specified module.
- If the module is not specified, it will generate a manifest for all modules.
- If the output directory is not specified, it will print the manifest to the console.
```shell
PS C:\> PowerToys.DSC.exe manifest --resource settings --module 'Awake' --outputDir "C:\manifests"
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
doc/images/icons/ZoomIt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

View File

@@ -16,6 +16,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" x64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs"
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup Condition="'$(Platform)' != 'x64'">
@@ -26,6 +27,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" arm64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs"
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup>
@@ -121,6 +123,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
<Compile Include="Hosts.wxs" />
<Compile Include="ImageResizer.wxs" />
<Compile Include="KeyboardManager.wxs" />
<Compile Include="DscResources.wxs" />
<Compile Include="Peek.wxs" />
<Compile Include="PowerRename.wxs" />
<Compile Include="RegistryPreview.wxs" />

View File

@@ -75,6 +75,7 @@
<ComponentGroupRef Id="NewPlusComponentGroup" />
<ComponentGroupRef Id="NewPlusTemplatesComponentGroup" />
<ComponentGroupRef Id="ResourcesComponentGroup" />
<ComponentGroupRef Id="DscResourcesComponentGroup" />
<ComponentGroupRef Id="WindowsAppSDKComponentGroup" />
<ComponentGroupRef Id="ToolComponentGroup" />
<ComponentGroupRef Id="MonacoSRCHeatGenerated" />
@@ -324,7 +325,6 @@
BinaryKey="PTCustomActions"
DllEntry="UninstallDSCModuleCA"
/>
<CustomAction Id="UninstallServicesTask"
Return="ignore"
Impersonate="yes"

View File

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

View File

@@ -3,6 +3,7 @@
#include "RcResource.h"
#include <ProjectTelemetry.h>
#include <spdlog/sinks/base_sink.h>
#include <filesystem>
#include "../../src/common/logger/logger.h"
#include "../../src/common/utils/gpo.h"
@@ -232,7 +233,9 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall)
auto action = [&commandLine](HANDLE userToken)
{
STARTUPINFO startupInfo{.cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL};
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(STARTUPINFO);
startupInfo.wShowWindow = SW_SHOWNORMAL;
PROCESS_INFORMATION processInformation;
PVOID lpEnvironment = NULL;
@@ -271,7 +274,9 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall)
}
else
{
STARTUPINFO startupInfo{.cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL};
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(STARTUPINFO);
startupInfo.wShowWindow = SW_SHOWNORMAL;
PROCESS_INFORMATION processInformation;
@@ -424,7 +429,7 @@ UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall)
const auto modulesPath = baseModulesPath / L"Microsoft.PowerToys.Configure" / (get_product_version(false) + L".0");
std::error_code errorCode;
fs::create_directories(modulesPath, errorCode);
std::filesystem::create_directories(modulesPath, errorCode);
if (errorCode)
{
hr = E_FAIL;
@@ -433,7 +438,7 @@ UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall)
for (const auto *filename : {DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME})
{
fs::copy_file(fs::path(installationFolder) / "DSCModules" / filename, modulesPath / filename, fs::copy_options::overwrite_existing, errorCode);
std::filesystem::copy_file(std::filesystem::path(installationFolder) / "DSCModules" / filename, modulesPath / filename, std::filesystem::copy_options::overwrite_existing, errorCode);
if (errorCode)
{
@@ -481,7 +486,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall)
for (const auto *filename : {DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME})
{
fs::remove(versionedModulePath / filename, errorCode);
std::filesystem::remove(versionedModulePath / filename, errorCode);
if (errorCode)
{
@@ -492,7 +497,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall)
for (const auto *modulePath : {&versionedModulePath, &powerToysModulePath})
{
fs::remove(*modulePath, errorCode);
std::filesystem::remove(*modulePath, errorCode);
if (errorCode)
{
@@ -1375,6 +1380,120 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
return WcaFinalize(er);
}
UINT __stdcall SetBundleInstallLocationCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
// Declare all variables at the beginning to avoid goto issues
std::wstring customActionData;
std::wstring installationFolder;
std::wstring bundleUpgradeCode;
std::wstring installScope;
bool isPerUser = false;
size_t pos1 = std::wstring::npos;
size_t pos2 = std::wstring::npos;
std::vector<HKEY> keysToTry;
hr = WcaInitialize(hInstall, "SetBundleInstallLocationCA");
ExitOnFailure(hr, "Failed to initialize");
// Parse CustomActionData: "installFolder;upgradeCode;installScope"
hr = getInstallFolder(hInstall, customActionData);
ExitOnFailure(hr, "Failed to get CustomActionData.");
pos1 = customActionData.find(L';');
if (pos1 == std::wstring::npos)
{
hr = E_INVALIDARG;
ExitOnFailure(hr, "Invalid CustomActionData format - missing first semicolon");
}
pos2 = customActionData.find(L';', pos1 + 1);
if (pos2 == std::wstring::npos)
{
hr = E_INVALIDARG;
ExitOnFailure(hr, "Invalid CustomActionData format - missing second semicolon");
}
installationFolder = customActionData.substr(0, pos1);
bundleUpgradeCode = customActionData.substr(pos1 + 1, pos2 - pos1 - 1);
installScope = customActionData.substr(pos2 + 1);
isPerUser = (installScope == L"perUser");
// Use the appropriate registry based on install scope
HKEY targetKey = isPerUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
const wchar_t* keyName = isPerUser ? L"HKCU" : L"HKLM";
WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Searching for Bundle in %ls registry", keyName);
HKEY uninstallKey;
LONG openResult = RegOpenKeyExW(targetKey, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", 0, KEY_READ | KEY_ENUMERATE_SUB_KEYS, &uninstallKey);
if (openResult != ERROR_SUCCESS)
{
WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Failed to open uninstall key, error: %ld", openResult);
goto LExit;
}
DWORD index = 0;
wchar_t subKeyName[256];
DWORD subKeyNameSize = sizeof(subKeyName) / sizeof(wchar_t);
while (RegEnumKeyExW(uninstallKey, index, subKeyName, &subKeyNameSize, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS)
{
HKEY productKey;
if (RegOpenKeyExW(uninstallKey, subKeyName, 0, KEY_READ | KEY_WRITE, &productKey) == ERROR_SUCCESS)
{
wchar_t upgradeCode[256];
DWORD upgradeCodeSize = sizeof(upgradeCode);
DWORD valueType;
if (RegQueryValueExW(productKey, L"BundleUpgradeCode", nullptr, &valueType,
reinterpret_cast<LPBYTE>(upgradeCode), &upgradeCodeSize) == ERROR_SUCCESS)
{
// Remove brackets from registry upgradeCode for comparison (bundleUpgradeCode doesn't have brackets)
std::wstring regUpgradeCode = upgradeCode;
if (!regUpgradeCode.empty() && regUpgradeCode.front() == L'{' && regUpgradeCode.back() == L'}')
{
regUpgradeCode = regUpgradeCode.substr(1, regUpgradeCode.length() - 2);
}
if (_wcsicmp(regUpgradeCode.c_str(), bundleUpgradeCode.c_str()) == 0)
{
// Found matching Bundle, set InstallLocation
LONG setResult = RegSetValueExW(productKey, L"InstallLocation", 0, REG_SZ,
reinterpret_cast<const BYTE*>(installationFolder.c_str()),
static_cast<DWORD>((installationFolder.length() + 1) * sizeof(wchar_t)));
if (setResult == ERROR_SUCCESS)
{
WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: InstallLocation set successfully");
}
else
{
WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Failed to set InstallLocation, error: %ld", setResult);
}
RegCloseKey(productKey);
RegCloseKey(uninstallKey);
goto LExit;
}
}
RegCloseKey(productKey);
}
index++;
subKeyNameSize = sizeof(subKeyName) / sizeof(wchar_t);
}
RegCloseKey(uninstallKey);
LExit:
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
return WcaFinalize(er);
}
void initSystemLogger()
{
static std::once_flag initLoggerFlag;

View File

@@ -32,4 +32,4 @@ EXPORTS
CleanFileLocksmithRuntimeRegistryCA
CleanPowerRenameRuntimeRegistryCA
CleanNewPlusRuntimeRegistryCA
SetBundleInstallLocationCA

View File

@@ -9,6 +9,25 @@
<RegistryValue Type="string" Name="InstallScope" Value="$(var.InstallScope)" />
</RegistryKey>
</Component>
<?if $(var.PerUser) = "true" ?>
<Component Id="powertoys_env_path_user" Bitness="always64">
<!-- Anchor registry for component key path -->
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="powertoys_env_path_user" Value="" KeyPath="yes" />
</RegistryKey>
<!-- Append install folder to current user's PATH -->
<Environment Id="AddPowerToysToUserPath" Name="PATH" Action="set" Part="last" System="no" Value="[INSTALLFOLDER]" />
</Component>
<?else?>
<Component Id="powertoys_env_path_machine" Bitness="always64">
<!-- Anchor registry for component key path -->
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="powertoys_env_path_machine" Value="" KeyPath="yes" />
</RegistryKey>
<!-- Append install folder to machine PATH -->
<Environment Id="AddPowerToysToMachinePath" Name="PATH" Action="set" Part="last" System="yes" Value="[INSTALLFOLDER]" />
</Component>
<?endif?>
<Component Id="powertoys_toast_clsid" Bitness="always64">
<RemoveFolder Id="Remove_powertoys_toast_clsid" On="uninstall" />
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{DD5CACDA-7C2E-4997-A62A-04A597B58F76}">
@@ -109,6 +128,11 @@
<ComponentRef Id="powertoys_exe" />
<ComponentRef Id="PowerToysStartMenuShortcut" />
<ComponentRef Id="powertoys_per_machine_comp" />
<?if $(var.PerUser) = "true" ?>
<ComponentRef Id="powertoys_env_path_user" />
<?else?>
<ComponentRef Id="powertoys_env_path_machine" />
<?endif?>
<ComponentRef Id="powertoys_toast_clsid" />
<ComponentRef Id="License_rtf" />
<ComponentRef Id="Notice_md" />

View File

@@ -28,6 +28,9 @@
<Log Disable="no" Prefix="powertoys-bootstrapper-msi-$(var.Version)" Extension=".log" />
<!-- Store Bundle UpgradeCode for CustomAction -->
<Variable Name="BundleUpgradeCode" Type="string" Value="$(var.UpgradeCode)" />
<!-- Only install/upgrade if the version is greater or equal than the currently installed version of PowerToys, to handle the case in which PowerToys was installed from old MSI (before WiX bootstrapper was used) -->
<!-- If the previous installation is a bundle installation, just let WiX run its logic. -->
<Variable Name="MinimumVersion" Type="version" Value="0.0.0.0" />
@@ -58,6 +61,7 @@
<MsiPackage DisplayName="PowerToys MSI" SourceFile="$(var.PowerToysPlatform)\Release\$(var.MSIPath)\$(var.MSIName)" Compressed="yes" bal:DisplayInternalUICondition="false">
<MsiProperty Name="BOOTSTRAPPERINSTALLFOLDER" Value="[InstallFolder]" />
<MsiProperty Name="MSIRESTARTMANAGERCONTROL" Value="Disable" />
<MsiProperty Name="BUNDLEINFO" Value="[BundleUpgradeCode]" />
</MsiPackage>
</Chain>
</Bundle>

View File

@@ -14,6 +14,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" x64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2"
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup Condition="'$(Platform)' != 'x64'">
@@ -24,6 +25,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" arm64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2"
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup>
@@ -115,6 +117,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
<Compile Include="KeyboardManager.wxs" />
<Compile Include="Peek.wxs" />
<Compile Include="PowerRename.wxs" />
<Compile Include="DscResources.wxs" />
<Compile Include="RegistryPreview.wxs" />
<Compile Include="Run.wxs" />
<Compile Include="Settings.wxs" />

View File

@@ -62,6 +62,7 @@
<ComponentGroupRef Id="NewPlusComponentGroup" />
<ComponentGroupRef Id="NewPlusTemplatesComponentGroup" />
<ComponentGroupRef Id="ResourcesComponentGroup" />
<ComponentGroupRef Id="DscResourcesComponentGroup" />
<ComponentGroupRef Id="WindowsAppSDKComponentGroup" />
<ComponentGroupRef Id="ToolComponentGroup" />
<ComponentGroupRef Id="MonacoSRCHeatGenerated" />
@@ -69,8 +70,8 @@
<ComponentGroupRef Id="CmdPalComponentGroup" />
</Feature>
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" Sequence="execute" />
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
<UI>
@@ -117,6 +118,8 @@
<Custom Action="SetUnApplyModulesRegistryChangeSetsParam" Before="UnApplyModulesRegistryChangeSets" />
<Custom Action="CheckGPO" After="InstallInitialize" Condition="NOT Installed" />
<Custom Action="SetBundleInstallLocationData" Before="SetBundleInstallLocation" Condition="NOT Installed" />
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" />
@@ -160,6 +163,9 @@
<CustomAction Id="SetInstallCmdPalPackageParam" Property="InstallCmdPalPackage" Value="[INSTALLFOLDER]" />
<!-- Set InstallLocation for Bundle entry as well -->
<CustomAction Id="SetBundleInstallLocationData" Property="SetBundleInstallLocation" Value="[INSTALLFOLDER];[BUNDLEINFO];[InstallScope]" />
<CustomAction Id="LaunchPowerToys" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="LaunchPowerToysCA" BinaryRef="PTCustomActions" />
<CustomAction Id="TerminateProcesses" Return="ignore" Execute="immediate" DllEntry="TerminateProcessesCA" BinaryRef="PTCustomActions" />
@@ -244,6 +250,8 @@
<CustomAction Id="InstallCmdPalPackage" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="InstallCmdPalPackageCA" BinaryRef="PTCustomActions" />
<CustomAction Id="SetBundleInstallLocation" Return="ignore" Impersonate="no" Execute="deferred" DllEntry="SetBundleInstallLocationCA" BinaryRef="PTCustomActions" />
<!-- Close 'PowerToys.exe' before uninstall-->
<Property Id="MSIRESTARTMANAGERCONTROL" Value="DisableShutdown" />
<Property Id="MSIFASTINSTALL" Value="DisableShutdown" />

View File

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

View File

@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Globalization;
using System.IO;
using System.Resources;
using PowerToys.DSC.UnitTests.Models;
namespace PowerToys.DSC.UnitTests;
public class BaseDscTest
{
private readonly ResourceManager _resourceManager;
public BaseDscTest()
{
_resourceManager = new ResourceManager("PowerToys.DSC.Properties.Resources", typeof(PowerToys.DSC.Program).Assembly);
}
/// <summary>
/// Returns the string resource for the given name, formatted with the provided arguments.
/// </summary>
/// <param name="name">The name of the resource string.</param>
/// <param name="args">The arguments to format the resource string with.</param>
/// <returns></returns>
public string GetResourceString(string name, params string[] args)
{
return string.Format(CultureInfo.InvariantCulture, _resourceManager.GetString(name, CultureInfo.InvariantCulture), args);
}
/// <summary>
/// Execute a dsc command with the provided arguments.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="args"></param>
/// <returns></returns>
protected DscExecuteResult ExecuteDscCommand<T>(params string[] args)
where T : Command, new()
{
var originalOut = Console.Out;
var originalErr = Console.Error;
var outSw = new StringWriter();
var errSw = new StringWriter();
try
{
Console.SetOut(outSw);
Console.SetError(errSw);
var executeResult = new T().Invoke(args);
var output = outSw.ToString();
var errorOutput = errSw.ToString();
return new(executeResult == 0, output, errorOutput);
}
finally
{
Console.SetOut(originalOut);
Console.SetError(originalErr);
outSw.Dispose();
errSw.Dispose();
}
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerToys.DSC.Commands;
using PowerToys.DSC.DSCResources;
namespace PowerToys.DSC.UnitTests;
[TestClass]
public sealed class CommandTest : BaseDscTest
{
[TestMethod]
public void GetResource_Found_Success()
{
// Act
var result = ExecuteDscCommand<GetCommand>("--resource", SettingsResource.ResourceName);
// Assert
Assert.IsTrue(result.Success);
}
[TestMethod]
public void GetResource_NotFound_Fail()
{
// Arrange
var availableResources = string.Join(", ", BaseCommand.AvailableResources);
// Act
var result = ExecuteDscCommand<GetCommand>("--resource", "ResourceNotFound");
// Assert
Assert.IsFalse(result.Success);
Assert.Contains(GetResourceString("InvalidResourceNameError", availableResources), result.Error);
}
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using PowerToys.DSC.Models;
namespace PowerToys.DSC.UnitTests.Models;
/// <summary>
/// Result of executing a DSC command.
/// </summary>
public class DscExecuteResult
{
/// <summary>
/// Initializes a new instance of the <see cref="DscExecuteResult"/> class.
/// </summary>
/// <param name="success">Value indicating whether the command execution was successful.</param>
/// <param name="output">Output stream content.</param>
/// <param name="error">Error stream content.</param>
public DscExecuteResult(bool success, string output, string error)
{
Success = success;
Output = output;
Error = error;
}
/// <summary>
/// Gets a value indicating whether the command execution was successful.
/// </summary>
public bool Success { get; }
/// <summary>
/// Gets the output stream content of the operation.
/// </summary>
public string Output { get; }
/// <summary>
/// Gets the error stream content of the operation.
/// </summary>
public string Error { get; }
/// <summary>
/// Gets the messages from the error stream.
/// </summary>
/// <returns>List of messages with their levels.</returns>
public List<(DscMessageLevel Level, string Message)> Messages()
{
var lines = Error.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
return lines.SelectMany(line =>
{
var map = JsonSerializer.Deserialize<Dictionary<string, string>>(line);
return map.Select(v => (GetMessageLevel(v.Key), v.Value)).ToList();
}).ToList();
}
/// <summary>
/// Gets the output as state.
/// </summary>
/// <returns>State.</returns>
public T OutputState<T>()
{
var lines = Output.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
Debug.Assert(lines.Length == 1, "Output should contain exactly one line.");
return JsonSerializer.Deserialize<T>(lines[0]);
}
/// <summary>
/// Gets the output as state and diff.
/// </summary>
/// <returns>State and diff.</returns>
public (T State, List<string> Diff) OutputStateAndDiff<T>()
{
var lines = Output.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
Debug.Assert(lines.Length == 2, "Output should contain exactly two lines.");
var obj = JsonSerializer.Deserialize<T>(lines[0]);
var diff = JsonSerializer.Deserialize<List<string>>(lines[1]);
return (obj, diff);
}
/// <summary>
/// Gets the message level from a string representation.
/// </summary>
/// <param name="level">The string representation of the message level.</param>
/// <returns>The level as <see cref="DscMessageLevel"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the level is unknown.</exception>
private DscMessageLevel GetMessageLevel(string level)
{
return level switch
{
"error" => DscMessageLevel.Error,
"warn" => DscMessageLevel.Warning,
"info" => DscMessageLevel.Info,
"debug" => DscMessageLevel.Debug,
"trace" => DscMessageLevel.Trace,
_ => throw new ArgumentOutOfRangeException(nameof(level), level, "Unknown message level"),
};
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<OutputPath>..\..\..\..\$(Configuration)\$(Platform)\tests\PowerToys.DSC.Tests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PowerToys.DSC\PowerToys.DSC.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceAdvancedPasteModuleTest : SettingsResourceModuleTest<AdvancedPasteSettings>
{
public SettingsResourceAdvancedPasteModuleTest()
: base(nameof(ModuleType.AdvancedPaste))
{
}
protected override Action<AdvancedPasteSettings> GetSettingsModifier()
{
return s =>
{
s.Properties.ShowCustomPreview = !s.Properties.ShowCustomPreview;
s.Properties.CloseAfterLosingFocus = !s.Properties.CloseAfterLosingFocus;
s.Properties.IsAdvancedAIEnabled = !s.Properties.IsAdvancedAIEnabled;
s.Properties.AdvancedPasteUIShortcut = new HotkeySettings
{
Key = "mock",
Alt = true,
};
};
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceAlwaysOnTopModuleTest : SettingsResourceModuleTest<AlwaysOnTopSettings>
{
public SettingsResourceAlwaysOnTopModuleTest()
: base(nameof(ModuleType.AlwaysOnTop))
{
}
protected override Action<AlwaysOnTopSettings> GetSettingsModifier()
{
return s =>
{
s.Properties.RoundCornersEnabled.Value = !s.Properties.RoundCornersEnabled.Value;
s.Properties.FrameEnabled.Value = !s.Properties.FrameEnabled.Value;
};
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerToys.DSC.DSCResources;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceAppModuleTest : SettingsResourceModuleTest<GeneralSettings>
{
public SettingsResourceAppModuleTest()
: base(SettingsResource.AppModule)
{
}
protected override Action<GeneralSettings> GetSettingsModifier()
{
return s =>
{
s.Startup = !s.Startup;
s.ShowSysTrayIcon = !s.ShowSysTrayIcon;
s.Enabled.Awake = !s.Enabled.Awake;
s.Enabled.ColorPicker = !s.Enabled.ColorPicker;
};
}
}

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.
using System;
using System.Collections.Generic;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceAwakeModuleTest : SettingsResourceModuleTest<AwakeSettings>
{
public SettingsResourceAwakeModuleTest()
: base(nameof(ModuleType.Awake))
{
}
protected override Action<AwakeSettings> GetSettingsModifier()
{
return s =>
{
s.Properties.ExpirationDateTime = DateTimeOffset.MinValue;
s.Properties.IntervalHours = DefaultSettings.Properties.IntervalHours + 1;
s.Properties.IntervalMinutes = DefaultSettings.Properties.IntervalMinutes + 1;
s.Properties.Mode = s.Properties.Mode == AwakeMode.PASSIVE ? AwakeMode.TIMED : AwakeMode.PASSIVE;
s.Properties.KeepDisplayOn = !s.Properties.KeepDisplayOn;
s.Properties.CustomTrayTimes = new Dictionary<string, uint>
{
{ "08:00", 1 },
{ "12:00", 2 },
{ "16:00", 3 },
};
};
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceColorPickerModuleTest : SettingsResourceModuleTest<ColorPickerSettings>
{
public SettingsResourceColorPickerModuleTest()
: base(nameof(ModuleType.ColorPicker))
{
}
protected override Action<ColorPickerSettings> GetSettingsModifier()
{
return s =>
{
s.Properties.ShowColorName = !s.Properties.ShowColorName;
s.Properties.ColorHistoryLimit = s.Properties.ColorHistoryLimit == 0 ? 10 : 0;
};
}
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using ManagedCommon;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerToys.DSC.Commands;
using PowerToys.DSC.DSCResources;
using PowerToys.DSC.Models;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceCommandTest : BaseDscTest
{
[TestMethod]
public void Modules_ListAllSupportedModules()
{
// Arrange
var expectedModules = new List<string>()
{
SettingsResource.AppModule,
nameof(ModuleType.AdvancedPaste),
nameof(ModuleType.AlwaysOnTop),
nameof(ModuleType.Awake),
nameof(ModuleType.ColorPicker),
nameof(ModuleType.CropAndLock),
nameof(ModuleType.EnvironmentVariables),
nameof(ModuleType.FancyZones),
nameof(ModuleType.FileLocksmith),
nameof(ModuleType.FindMyMouse),
nameof(ModuleType.Hosts),
nameof(ModuleType.ImageResizer),
nameof(ModuleType.KeyboardManager),
nameof(ModuleType.MouseHighlighter),
nameof(ModuleType.MouseJump),
nameof(ModuleType.MousePointerCrosshairs),
nameof(ModuleType.Peek),
nameof(ModuleType.PowerRename),
nameof(ModuleType.PowerAccent),
nameof(ModuleType.RegistryPreview),
nameof(ModuleType.MeasureTool),
nameof(ModuleType.ShortcutGuide),
nameof(ModuleType.PowerOCR),
nameof(ModuleType.Workspaces),
nameof(ModuleType.ZoomIt),
};
// Act
var result = ExecuteDscCommand<ModulesCommand>("--resource", SettingsResource.ResourceName);
// Assert
Assert.IsTrue(result.Success);
Assert.AreEqual(string.Join(Environment.NewLine, expectedModules.Order()), result.Output.Trim());
}
[TestMethod]
public void Set_EmptyInput_Fail()
{
// Act
var result = ExecuteDscCommand<SetCommand>("--resource", SettingsResource.ResourceName, "--module", "Awake");
var messages = result.Messages();
// Assert
Assert.IsFalse(result.Success);
Assert.AreEqual(1, messages.Count);
Assert.AreEqual(DscMessageLevel.Error, messages[0].Level);
Assert.AreEqual(GetResourceString("InputEmptyOrNullError"), messages[0].Message);
}
[TestMethod]
public void Test_EmptyInput_Fail()
{
// Act
var result = ExecuteDscCommand<TestCommand>("--resource", SettingsResource.ResourceName, "--module", "Awake");
var messages = result.Messages();
// Assert
Assert.IsFalse(result.Success);
Assert.AreEqual(1, messages.Count);
Assert.AreEqual(DscMessageLevel.Error, messages[0].Level);
Assert.AreEqual(GetResourceString("InputEmptyOrNullError"), messages[0].Message);
}
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
[TestClass]
public sealed class SettingsResourceCropAndLockModuleTest : SettingsResourceModuleTest<CropAndLockSettings>
{
public SettingsResourceCropAndLockModuleTest()
: base(nameof(ModuleType.CropAndLock))
{
}
protected override Action<CropAndLockSettings> GetSettingsModifier()
{
return s =>
{
s.Properties.ThumbnailHotkey = new KeyboardKeysProperty()
{
Value = new HotkeySettings
{
Key = "mock",
Alt = true,
},
};
};
}
}

View File

@@ -0,0 +1,267 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerToys.DSC.Commands;
using PowerToys.DSC.DSCResources;
using PowerToys.DSC.Models.ResourceObjects;
namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
public abstract class SettingsResourceModuleTest<TSettingsConfig> : BaseDscTest
where TSettingsConfig : ISettingsConfig, new()
{
private readonly SettingsUtils _settingsUtils = new();
private TSettingsConfig _originalSettings;
protected TSettingsConfig DefaultSettings => new();
protected string Module { get; }
protected List<string> DiffSettings { get; } = [SettingsResourceObject<AwakeSettings>.SettingsJsonPropertyName];
protected List<string> DiffEmpty { get; } = [];
public SettingsResourceModuleTest(string module)
{
Module = module;
}
[TestInitialize]
public void TestInitialize()
{
_originalSettings = GetSettings();
ResetSettingsToDefaultValues();
}
[TestCleanup]
public void TestCleanup()
{
SaveSettings(_originalSettings);
}
[TestMethod]
public void Get_Success()
{
// Arrange
var settingsBeforeExecute = GetSettings();
// Act
var result = ExecuteDscCommand<GetCommand>("--resource", SettingsResource.ResourceName, "--module", Module);
var state = result.OutputState<SettingsResourceObject<TSettingsConfig>>();
// Assert
Assert.IsTrue(result.Success);
AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
}
[TestMethod]
public void Export_Success()
{
// Arrange
var settingsBeforeExecute = GetSettings();
// Act
var result = ExecuteDscCommand<ExportCommand>("--resource", SettingsResource.ResourceName, "--module", Module);
var state = result.OutputState<SettingsResourceObject<TSettingsConfig>>();
// Assert
Assert.IsTrue(result.Success);
AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
}
[TestMethod]
public void SetWithDiff_Success()
{
// Arrange
var settingsModifier = GetSettingsModifier();
var input = CreateInputResourceObject(settingsModifier);
// Act
var result = ExecuteDscCommand<SetCommand>("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input);
var (state, diff) = result.OutputStateAndDiff<SettingsResourceObject<TSettingsConfig>>();
// Assert
Assert.IsTrue(result.Success);
AssertSettingsHasChanged(settingsModifier);
AssertStateAndSettingsAreEqual(GetSettings(), state);
CollectionAssert.AreEqual(DiffSettings, diff);
}
[TestMethod]
public void SetWithoutDiff_Success()
{
// Arrange
var settingsModifier = GetSettingsModifier();
UpdateSettings(settingsModifier);
var settingsBeforeExecute = GetSettings();
var input = CreateInputResourceObject(settingsModifier);
// Act
var result = ExecuteDscCommand<SetCommand>("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input);
var (state, diff) = result.OutputStateAndDiff<SettingsResourceObject<TSettingsConfig>>();
// Assert
Assert.IsTrue(result.Success);
AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
CollectionAssert.AreEqual(DiffEmpty, diff);
}
[TestMethod]
public void TestWithDiff_Success()
{
// Arrange
var settingsModifier = GetSettingsModifier();
var settingsBeforeExecute = GetSettings();
var input = CreateInputResourceObject(settingsModifier);
// Act
var result = ExecuteDscCommand<TestCommand>("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input);
var (state, diff) = result.OutputStateAndDiff<SettingsResourceObject<TSettingsConfig>>();
// Assert
Assert.IsTrue(result.Success);
AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
CollectionAssert.AreEqual(DiffSettings, diff);
Assert.IsFalse(state.InDesiredState);
}
[TestMethod]
public void TestWithoutDiff_Success()
{
// Arrange
var settingsModifier = GetSettingsModifier();
UpdateSettings(settingsModifier);
var settingsBeforeExecute = GetSettings();
var input = CreateInputResourceObject(settingsModifier);
// Act
var result = ExecuteDscCommand<TestCommand>("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input);
var (state, diff) = result.OutputStateAndDiff<SettingsResourceObject<TSettingsConfig>>();
// Assert
Assert.IsTrue(result.Success);
AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
CollectionAssert.AreEqual(DiffEmpty, diff);
Assert.IsTrue(state.InDesiredState);
}
/// <summary>
/// Gets the settings modifier action for the specific settings configuration.
/// </summary>
/// <returns>An action that modifies the settings configuration.</returns>
protected abstract Action<TSettingsConfig> GetSettingsModifier();
/// <summary>
/// Resets the settings to default values.
/// </summary>
private void ResetSettingsToDefaultValues()
{
SaveSettings(DefaultSettings);
}
/// <summary>
/// Get the settings for the specified module.
/// </summary>
/// <returns>An instance of the settings type with the current configuration.</returns>
private TSettingsConfig GetSettings()
{
return _settingsUtils.GetSettingsOrDefault<TSettingsConfig>(DefaultSettings.GetModuleName());
}
/// <summary>
/// Saves the settings for the specified module.
/// </summary>
/// <param name="settings">Settings to save.</param>
private void SaveSettings(TSettingsConfig settings)
{
_settingsUtils.SaveSettings(JsonSerializer.Serialize(settings), DefaultSettings.GetModuleName());
}
/// <summary>
/// Create the resource object for the operation.
/// </summary>
/// <param name="settings">Settings to include in the resource object.</param>
/// <returns>A JSON string representing the resource object.</returns>
private string CreateResourceObject(TSettingsConfig settings)
{
var resourceObject = new SettingsResourceObject<TSettingsConfig>
{
Settings = settings,
};
return JsonSerializer.Serialize(resourceObject);
}
private string CreateInputResourceObject(Action<TSettingsConfig> settingsModifier)
{
var settings = DefaultSettings;
settingsModifier(settings);
return CreateResourceObject(settings);
}
/// <summary>
/// Create the response for the Get operation.
/// </summary>
/// <returns>A JSON string representing the response.</returns>
private string CreateGetResponse()
{
return CreateResourceObject(GetSettings());
}
/// <summary>
/// Asserts that the state and settings are equal.
/// </summary>
/// <param name="settings">Settings manifest to compare against.</param>
/// <param name="state">Output state to compare.</param>
private void AssertStateAndSettingsAreEqual(TSettingsConfig settings, SettingsResourceObject<TSettingsConfig> state)
{
AssertSettingsAreEqual(settings, state.Settings);
}
/// <summary>
/// Asserts that two settings manifests are equal.
/// </summary>
/// <param name="expected">Expected settings.</param>
/// <param name="actual">Actual settings.</param>
private void AssertSettingsAreEqual(TSettingsConfig expected, TSettingsConfig actual)
{
var expectedJson = JsonSerializer.SerializeToNode(expected) as JsonObject;
var actualJson = JsonSerializer.SerializeToNode(actual) as JsonObject;
Assert.IsTrue(JsonNode.DeepEquals(expectedJson, actualJson));
}
/// <summary>
/// Asserts that the current settings have changed.
/// </summary>
/// <param name="action">Action to prepare the default settings.</param>
private void AssertSettingsHasChanged(Action<TSettingsConfig> action)
{
var currentSettings = GetSettings();
var defaultSettings = DefaultSettings;
action(defaultSettings);
AssertSettingsAreEqual(defaultSettings, currentSettings);
}
/// <summary>
/// Updates the settings.
/// </summary>
/// <param name="action">Action to modify the settings.</param>
private void UpdateSettings(Action<TSettingsConfig> action)
{
var settings = GetSettings();
action(settings);
SaveSettings(settings);
}
}

View File

@@ -0,0 +1,120 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using PowerToys.DSC.DSCResources;
using PowerToys.DSC.Options;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Base class for all DSC commands.
/// </summary>
public abstract class BaseCommand : Command
{
private static readonly CompositeFormat ModuleNotSupportedByResource = CompositeFormat.Parse(Resources.ModuleNotSupportedByResource);
// Shared options for all commands
private readonly ModuleOption _moduleOption;
private readonly ResourceOption _resourceOption;
private readonly InputOption _inputOption;
// The dictionary of available resources and their factories.
private static readonly Dictionary<string, Func<string?, BaseResource>> _resourceFactories = new()
{
{ SettingsResource.ResourceName, module => new SettingsResource(module) },
// Add other resources here
};
/// <summary>
/// Gets the list of available DSC resources that can be used with the command.
/// </summary>
public static List<string> AvailableResources => [.._resourceFactories.Keys];
/// <summary>
/// Gets the DSC resource to be used by the command.
/// </summary>
protected BaseResource? Resource { get; private set; }
/// <summary>
/// Gets the input JSON provided by the user.
/// </summary>
protected string? Input { get; private set; }
/// <summary>
/// Gets the PowerToys module to be used by the command.
/// </summary>
protected string? Module { get; private set; }
public BaseCommand(string name, string description)
: base(name, description)
{
// Register the common options for all commands
_moduleOption = new ModuleOption();
AddOption(_moduleOption);
_resourceOption = new ResourceOption(AvailableResources);
AddOption(_resourceOption);
_inputOption = new InputOption();
AddOption(_inputOption);
// Register the command handler
this.SetHandler(CommandHandler);
}
/// <summary>
/// Handles the command invocation.
/// </summary>
/// <param name="context">The invocation context containing the parsed command options.</param>
public void CommandHandler(InvocationContext context)
{
Input = context.ParseResult.GetValueForOption(_inputOption);
Module = context.ParseResult.GetValueForOption(_moduleOption);
Resource = ResolvedResource(context);
// Validate the module against the resource's supported modules
var supportedModules = Resource.GetSupportedModules();
if (!string.IsNullOrEmpty(Module) && !supportedModules.Contains(Module))
{
var errorMessage = string.Format(CultureInfo.InvariantCulture, ModuleNotSupportedByResource, Module, Resource.Name);
context.Console.Error.WriteLine(errorMessage);
context.ExitCode = 1;
return;
}
// Continue with the command handler logic
CommandHandlerInternal(context);
}
/// <summary>
/// Handles the command logic internally.
/// </summary>
/// <param name="context">Invocation context containing the parsed command options.</param>
public abstract void CommandHandlerInternal(InvocationContext context);
/// <summary>
/// Resolves the resource from the provided resource name in the context.
/// </summary>
/// <param name="context">Invocation context containing the parsed command options.</param>
/// <returns>The resolved <see cref="BaseResource"/> instance.</returns>
private BaseResource ResolvedResource(InvocationContext context)
{
// Resource option has already been validated before the command
// handler is invoked.
var resourceName = context.ParseResult.GetValueForOption(_resourceOption);
Debug.Assert(!string.IsNullOrEmpty(resourceName), "Resource name must not be null or empty.");
Debug.Assert(_resourceFactories.ContainsKey(resourceName), $"Resource '{resourceName}' is not registered.");
return _resourceFactories[resourceName](Module);
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine.Invocation;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to export all state instances.
/// </summary>
public sealed class ExportCommand : BaseCommand
{
public ExportCommand()
: base("export", Resources.ExportCommandDescription)
{
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
context.ExitCode = Resource!.ExportState(Input) ? 0 : 1;
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine.Invocation;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to get the resource state.
/// </summary>
public sealed class GetCommand : BaseCommand
{
public GetCommand()
: base("get", Resources.GetCommandDescription)
{
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
context.ExitCode = Resource!.GetState(Input) ? 0 : 1;
}
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine.Invocation;
using PowerToys.DSC.Options;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to get the manifest of the DSC resource.
/// </summary>
public sealed class ManifestCommand : BaseCommand
{
/// <summary>
/// Option to specify the output directory for the manifest.
/// </summary>
private readonly OutputDirectoryOption _outputDirectoryOption;
public ManifestCommand()
: base("manifest", Resources.ManifestCommandDescription)
{
_outputDirectoryOption = new OutputDirectoryOption();
AddOption(_outputDirectoryOption);
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
var outputDir = context.ParseResult.GetValueForOption(_outputDirectoryOption);
context.ExitCode = Resource!.Manifest(outputDir) ? 0 : 1;
}
}

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.
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Diagnostics;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to get all supported modules for a specific resource.
/// </summary>
/// <remarks>
/// This class is primarily used for debugging purposes and for build scripts.
/// </remarks>
public sealed class ModulesCommand : BaseCommand
{
public ModulesCommand()
: base("modules", Resources.ModulesCommandDescription)
{
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
// Module is optional, if not provided, all supported modules for the
// resource will be printed. If provided, it must be one of the
// supported modules since it has been validated before this command is
// executed.
if (!string.IsNullOrEmpty(Module))
{
Debug.Assert(Resource!.GetSupportedModules().Contains(Module), "Module must be present in the list of supported modules.");
context.Console.WriteLine(Module);
}
else
{
// Print the supported modules for the specified resource
foreach (var module in Resource!.GetSupportedModules())
{
context.Console.WriteLine(module);
}
}
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine.Invocation;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to output the schema of the resource.
/// </summary>
public sealed class SchemaCommand : BaseCommand
{
public SchemaCommand()
: base("schema", Resources.SchemaCommandDescription)
{
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
context.ExitCode = Resource!.Schema() ? 0 : 1;
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine.Invocation;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to set the resource state.
/// </summary>
public sealed class SetCommand : BaseCommand
{
public SetCommand()
: base("set", Resources.SetCommandDescription)
{
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
context.ExitCode = Resource!.SetState(Input) ? 0 : 1;
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine.Invocation;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Commands;
/// <summary>
/// Command to test the resource state.
/// </summary>
public sealed class TestCommand : BaseCommand
{
public TestCommand()
: base("test", Resources.TestCommandDescription)
{
}
/// <inheritdoc/>
public override void CommandHandlerInternal(InvocationContext context)
{
context.ExitCode = Resource!.TestState(Input) ? 0 : 1;
}
}

View File

@@ -0,0 +1,134 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json.Nodes;
using PowerToys.DSC.Models;
namespace PowerToys.DSC.DSCResources;
/// <summary>
/// Base class for all DSC resources.
/// </summary>
public abstract class BaseResource
{
/// <summary>
/// Gets the name of the resource.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the module being used by the resource, if provided.
/// </summary>
public string? Module { get; }
public BaseResource(string name, string? module)
{
Name = name;
Module = module;
}
/// <summary>
/// Calls the get method on the resource.
/// </summary>
/// <param name="input">The input string, if any.</param>
/// <returns>True if the operation was successful; otherwise false.</returns>
public abstract bool GetState(string? input);
/// <summary>
/// Calls the set method on the resource.
/// </summary>
/// <param name="input">The input string, if any.</param>
/// <returns>True if the operation was successful; otherwise false.</returns>
public abstract bool SetState(string? input);
/// <summary>
/// Calls the test method on the resource.
/// </summary>
/// <param name="input">The input string, if any.</param>
/// <returns>True if the operation was successful; otherwise false.</returns>
public abstract bool TestState(string? input);
/// <summary>
/// Calls the export method on the resource.
/// </summary>
/// <param name="input"> The input string, if any.</param>
/// <returns>True if the operation was successful; otherwise false.</returns>
public abstract bool ExportState(string? input);
/// <summary>
/// Calls the schema method on the resource.
/// </summary>
/// <returns>True if the operation was successful; otherwise false.</returns>
public abstract bool Schema();
/// <summary>
/// Generates a DSC resource JSON manifest for the resource. If the
/// outputDir is not provided, the manifest will be printed to the console.
/// </summary>
/// <param name="outputDir"> The directory where the manifest should be
/// saved. If null, the manifest will be printed to the console.</param>
/// <returns>True if the manifest was successfully generated and saved,otherwise false.</returns>
public abstract bool Manifest(string? outputDir);
/// <summary>
/// Gets the list of supported modules for the resource.
/// </summary>
/// <returns>Gets a list of supported modules.</returns>
public abstract IList<string> GetSupportedModules();
/// <summary>
/// Writes a JSON output line to the console.
/// </summary>
/// <param name="output">The JSON output to write.</param>
protected void WriteJsonOutputLine(JsonNode output)
{
var json = output.ToJsonString(new() { WriteIndented = false });
WriteJsonOutputLine(json);
}
/// <summary>
/// Writes a JSON output line to the console.
/// </summary>
/// <param name="output">The JSON output to write.</param>
protected void WriteJsonOutputLine(string output)
{
Console.WriteLine(output);
}
/// <summary>
/// Writes a message output line to the console with the specified message level.
/// </summary>
/// <param name="level">The level of the message.</param>
/// <param name="message">The message to write.</param>
protected void WriteMessageOutputLine(DscMessageLevel level, string message)
{
var messageObj = new Dictionary<string, string>
{
[GetMessageLevel(level)] = message,
};
var messageJson = System.Text.Json.JsonSerializer.Serialize(messageObj);
Console.Error.WriteLine(messageJson);
}
/// <summary>
/// Gets the message level as a string based on the provided dsc message level enum value.
/// </summary>
/// <param name="level">The dsc message level.</param>
/// <returns>A string representation of the message level.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the provided message level is not recognized.</exception>
private static string GetMessageLevel(DscMessageLevel level)
{
return level switch
{
DscMessageLevel.Error => "error",
DscMessageLevel.Warning => "warn",
DscMessageLevel.Info => "info",
DscMessageLevel.Debug => "debug",
DscMessageLevel.Trace => "trace",
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null),
};
}
}

View File

@@ -0,0 +1,248 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using PowerToys.DSC.Models;
using PowerToys.DSC.Models.FunctionData;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.DSCResources;
/// <summary>
/// Represents the DSC resource for managing PowerToys settings.
/// </summary>
public sealed class SettingsResource : BaseResource
{
private static readonly CompositeFormat FailedToWriteManifests = CompositeFormat.Parse(Resources.FailedToWriteManifests);
public const string AppModule = "App";
public const string ResourceName = "settings";
private readonly Dictionary<string, Func<string?, ISettingsFunctionData>> _moduleFunctionData;
public string ModuleOrDefault => string.IsNullOrEmpty(Module) ? AppModule : Module;
public SettingsResource(string? module)
: base(ResourceName, module)
{
_moduleFunctionData = new()
{
{ AppModule, CreateModuleFunctionData<GeneralSettings> },
{ nameof(ModuleType.AdvancedPaste), CreateModuleFunctionData<AdvancedPasteSettings> },
{ nameof(ModuleType.AlwaysOnTop), CreateModuleFunctionData<AlwaysOnTopSettings> },
{ nameof(ModuleType.Awake), CreateModuleFunctionData<AwakeSettings> },
{ nameof(ModuleType.ColorPicker), CreateModuleFunctionData<ColorPickerSettings> },
{ nameof(ModuleType.CropAndLock), CreateModuleFunctionData<CropAndLockSettings> },
{ nameof(ModuleType.EnvironmentVariables), CreateModuleFunctionData<EnvironmentVariablesSettings> },
{ nameof(ModuleType.FancyZones), CreateModuleFunctionData<FancyZonesSettings> },
{ nameof(ModuleType.FileLocksmith), CreateModuleFunctionData<FileLocksmithSettings> },
{ nameof(ModuleType.FindMyMouse), CreateModuleFunctionData<FindMyMouseSettings> },
{ nameof(ModuleType.Hosts), CreateModuleFunctionData<HostsSettings> },
{ nameof(ModuleType.ImageResizer), CreateModuleFunctionData<ImageResizerSettings> },
{ nameof(ModuleType.KeyboardManager), CreateModuleFunctionData<KeyboardManagerSettings> },
{ nameof(ModuleType.MouseHighlighter), CreateModuleFunctionData<MouseHighlighterSettings> },
{ nameof(ModuleType.MouseJump), CreateModuleFunctionData<MouseJumpSettings> },
{ nameof(ModuleType.MousePointerCrosshairs), CreateModuleFunctionData<MousePointerCrosshairsSettings> },
{ nameof(ModuleType.Peek), CreateModuleFunctionData<PeekSettings> },
{ nameof(ModuleType.PowerRename), CreateModuleFunctionData<PowerRenameSettings> },
{ nameof(ModuleType.PowerAccent), CreateModuleFunctionData<PowerAccentSettings> },
{ nameof(ModuleType.RegistryPreview), CreateModuleFunctionData<RegistryPreviewSettings> },
{ nameof(ModuleType.MeasureTool), CreateModuleFunctionData<MeasureToolSettings> },
{ nameof(ModuleType.ShortcutGuide), CreateModuleFunctionData<ShortcutGuideSettings> },
{ nameof(ModuleType.PowerOCR), CreateModuleFunctionData<PowerOcrSettings> },
{ nameof(ModuleType.Workspaces), CreateModuleFunctionData<WorkspacesSettings> },
{ nameof(ModuleType.ZoomIt), CreateModuleFunctionData<ZoomItSettings> },
// The following modules are not currently supported:
// - MouseWithoutBorders Contains sensitive configuration values, making export/import potentially insecure.
// - PowerLauncher Uses absolute file paths in its settings, which are not portable across systems.
// - NewPlus Uses absolute file paths in its settings, which are not portable across systems.
};
}
/// <inheritdoc/>
public override bool ExportState(string? input)
{
var data = CreateFunctionData();
data.GetState();
WriteJsonOutputLine(data.Output.ToJson());
return true;
}
/// <inheritdoc/>
public override bool GetState(string? input)
{
return ExportState(input);
}
/// <inheritdoc/>
public override bool SetState(string? input)
{
if (string.IsNullOrEmpty(input))
{
WriteMessageOutputLine(DscMessageLevel.Error, Resources.InputEmptyOrNullError);
return false;
}
var data = CreateFunctionData(input);
data.GetState();
// Capture the diff before updating the output
var diff = data.GetDiffJson();
// Only call Set if the desired state is different from the current state
if (!data.TestState())
{
var inputSettings = data.Input.SettingsInternal;
data.Output.SettingsInternal = inputSettings;
data.SetState();
}
WriteJsonOutputLine(data.Output.ToJson());
WriteJsonOutputLine(diff);
return true;
}
/// <inheritdoc/>
public override bool TestState(string? input)
{
if (string.IsNullOrEmpty(input))
{
WriteMessageOutputLine(DscMessageLevel.Error, Resources.InputEmptyOrNullError);
return false;
}
var data = CreateFunctionData(input);
data.GetState();
data.Output.InDesiredState = data.TestState();
WriteJsonOutputLine(data.Output.ToJson());
WriteJsonOutputLine(data.GetDiffJson());
return true;
}
/// <inheritdoc/>
public override bool Schema()
{
var data = CreateFunctionData();
WriteJsonOutputLine(data.Schema());
return true;
}
/// <inheritdoc/>
/// <remarks>
/// If an output directory is specified, write the manifests to files,
/// otherwise output them to the console.
/// </remarks>
public override bool Manifest(string? outputDir)
{
var manifests = GenerateManifests();
if (!string.IsNullOrEmpty(outputDir))
{
try
{
foreach (var (name, manifest) in manifests)
{
File.WriteAllText(Path.Combine(outputDir, $"microsoft.powertoys.{name}.settings.dsc.resource.json"), manifest);
}
}
catch (Exception ex)
{
var errorMessage = string.Format(CultureInfo.InvariantCulture, FailedToWriteManifests, outputDir, ex.Message);
WriteMessageOutputLine(DscMessageLevel.Error, errorMessage);
return false;
}
}
else
{
foreach (var (_, manifest) in manifests)
{
WriteJsonOutputLine(manifest);
}
}
return true;
}
/// <summary>
/// Generates manifests for the specified module or all supported modules
/// if no module is specified.
/// </summary>
/// <returns>A list of tuples containing the module name and its corresponding manifest JSON.</returns>
private List<(string Name, string Manifest)> GenerateManifests()
{
List<(string Name, string Manifest)> manifests = [];
if (!string.IsNullOrEmpty(Module))
{
manifests.Add((Module, GenerateManifest(Module)));
}
else
{
foreach (var module in GetSupportedModules())
{
manifests.Add((module, GenerateManifest(module)));
}
}
return manifests;
}
/// <summary>
/// Generate a DSC resource JSON manifest for the specified module.
/// </summary>
/// <param name="module">The name of the module for which to generate the manifest.</param>
/// <returns>A JSON string representing the DSC resource manifest.</returns>
private string GenerateManifest(string module)
{
// Note: The description is not localized because the generated
// manifest file will be part of the package
return new DscManifest($"{module}Settings", "0.1.0")
.AddDescription($"Allows management of {module} settings state via the DSC v3 command line interface protocol.")
.AddStdinMethod("export", ["export", "--module", module, "--resource", "settings"])
.AddStdinMethod("get", ["get", "--module", module, "--resource", "settings"])
.AddJsonInputMethod("set", "--input", ["set", "--module", module, "--resource", "settings"], implementsPretest: true, stateAndDiff: true)
.AddJsonInputMethod("test", "--input", ["test", "--module", module, "--resource", "settings"], stateAndDiff: true)
.AddCommandMethod("schema", ["schema", "--module", module, "--resource", "settings"])
.ToJson();
}
/// <inheritdoc/>
public override IList<string> GetSupportedModules()
{
return [.. _moduleFunctionData.Keys.Order()];
}
/// <summary>
/// Creates the function data for the specified module or the default module if none is specified.
/// </summary>
/// <param name="input">The input string, if any.</param>
/// <returns>An instance of <see cref="ISettingsFunctionData"/> for the specified module.</returns>
public ISettingsFunctionData CreateFunctionData(string? input = null)
{
Debug.Assert(_moduleFunctionData.ContainsKey(ModuleOrDefault), "Module should be supported by the resource.");
return _moduleFunctionData[ModuleOrDefault](input);
}
/// <summary>
/// Creates the function data for a specific settings configuration type.
/// </summary>
/// <typeparam name="TSettingsConfig">The type of settings configuration to create function data for.</typeparam>
/// <param name="input">The input string, if any.</param>
/// <returns>An instance of <see cref="ISettingsFunctionData"/> for the specified settings configuration type.</returns>
private ISettingsFunctionData CreateModuleFunctionData<TSettingsConfig>(string? input)
where TSettingsConfig : ISettingsConfig, new()
{
return new SettingsFunctionData<TSettingsConfig>(input);
}
}

View File

@@ -0,0 +1,152 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace PowerToys.DSC.Models;
/// <summary>
/// Class for building a DSC manifest for PowerToys resources.
/// </summary>
public sealed class DscManifest
{
private const string Schema = "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.vscode.json";
private const string Executable = @"PowerToys.DSC.exe";
private readonly string _type;
private readonly string _version;
private readonly JsonObject _manifest;
public DscManifest(string type, string version)
{
_type = type;
_version = version;
_manifest = new JsonObject
{
["$schema"] = Schema,
["type"] = $"Microsoft.PowerToys/{_type}",
["version"] = _version,
["tags"] = new JsonArray("PowerToys"),
};
}
/// <summary>
/// Adds a description to the manifest.
/// </summary>
/// <param name="description">The description to add.</param>
/// <returns>Returns the current instance of <see cref="DscManifest"/>.</returns>
public DscManifest AddDescription(string description)
{
_manifest["description"] = description;
return this;
}
/// <summary>
/// Adds a method to the manifest with the specified executable and arguments.
/// </summary>
/// <param name="method">The name of the method to add.</param>
/// <param name="inputArg">The input argument for the method</param>
/// <param name="args">The list of arguments for the method.</param>
/// <param name="implementsPretest">Whether the method implements a pretest.</param>
/// <param name="stateAndDiff">Whether the method returns state and diff.</param>
/// <returns>Returns the current instance of <see cref="DscManifest"/>.</returns>
public DscManifest AddJsonInputMethod(string method, string inputArg, List<string> args, bool? implementsPretest = null, bool? stateAndDiff = null)
{
var argsJson = CreateJsonArray(args);
argsJson.Add(new JsonObject
{
["jsonInputArg"] = inputArg,
["mandatory"] = true,
});
var methodObject = AddMethod(argsJson, implementsPretest, stateAndDiff);
_manifest[method] = methodObject;
return this;
}
/// <summary>
/// Adds a method to the manifest that reads from standard input (stdin).
/// </summary>
/// <param name="method">The name of the method to add.</param>
/// <param name="args">The list of arguments for the method.</param>
/// <param name="implementsPretest">Whether the method implements a pretest.</param>
/// <param name="stateAndDiff">Whether the method returns state and diff.</param>
/// <returns>Returns the current instance of <see cref="DscManifest"/>.</returns>
public DscManifest AddStdinMethod(string method, List<string> args, bool? implementsPretest = null, bool? stateAndDiff = null)
{
var argsJson = CreateJsonArray(args);
var methodObject = AddMethod(argsJson, implementsPretest, stateAndDiff);
methodObject["input"] = "stdin";
_manifest[method] = methodObject;
return this;
}
/// <summary>
/// Adds a command method to the manifest.
/// </summary>
/// <param name="method">The name of the method to add.</param>
/// <param name="args">The list of arguments for the method.</param>
/// <returns>Returns the current instance of <see cref="DscManifest"/>.</returns>
public DscManifest AddCommandMethod(string method, List<string> args)
{
_manifest[method] = new JsonObject
{
["command"] = AddMethod(CreateJsonArray(args)),
};
return this;
}
/// <summary>
/// Gets the JSON representation of the manifest.
/// </summary>
/// <returns>Returns the JSON string of the manifest.</returns>
public string ToJson()
{
return _manifest.ToJsonString(new() { WriteIndented = true });
}
/// <summary>
/// Add a method to the manifest with the specified arguments.
/// </summary>
/// <param name="args">The list of arguments for the method.</param>
/// <param name="implementsPretest">Whether the method implements a pretest.</param>
/// <param name="stateAndDiff">Whether the method returns state and diff.</param>
/// <returns>Returns the method object.</returns>
private JsonObject AddMethod(JsonArray args, bool? implementsPretest = null, bool? stateAndDiff = null)
{
var methodObject = new JsonObject
{
["executable"] = Executable,
["args"] = args,
};
if (implementsPretest.HasValue)
{
methodObject["implementsPretest"] = implementsPretest.Value;
}
if (stateAndDiff.HasValue)
{
methodObject["return"] = stateAndDiff.Value ? "stateAndDiff" : "state";
}
return methodObject;
}
/// <summary>
/// Creates a JSON array from a list of strings.
/// </summary>
/// <param name="args">The list of strings to convert.</param>
/// <returns>Returns the JSON array.</returns>
private JsonArray CreateJsonArray(List<string> args)
{
var jsonArray = new JsonArray();
foreach (var arg in args)
{
jsonArray.Add(arg);
}
return jsonArray;
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace PowerToys.DSC.Models;
/// <summary>
/// Specifies the severity level of a message.
/// </summary>
public enum DscMessageLevel
{
/// <summary>
/// Represents an error message.
/// </summary>
Error,
/// <summary>
/// Represents a warning message.
/// </summary>
Warning,
/// <summary>
/// Represents an informational message.
/// </summary>
Info,
/// <summary>
/// Represents a debug message.
/// </summary>
Debug,
/// <summary>
/// Represents a trace message.
/// </summary>
Trace,
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Newtonsoft.Json;
using NJsonSchema.Generation;
using PowerToys.DSC.Models.ResourceObjects;
namespace PowerToys.DSC.Models.FunctionData;
/// <summary>
/// Base class for function data objects.
/// </summary>
public class BaseFunctionData
{
/// <summary>
/// Generates a JSON schema for the specified resource object type.
/// </summary>
/// <typeparam name="T">The type of the resource object.</typeparam>
/// <returns>A JSON schema string.</returns>
protected static string GenerateSchema<T>()
where T : BaseResourceObject
{
var settings = new SystemTextJsonSchemaGeneratorSettings()
{
FlattenInheritanceHierarchy = true,
SerializerOptions =
{
IgnoreReadOnlyFields = true,
},
};
var generator = new JsonSchemaGenerator(settings);
var schema = generator.Generate(typeof(T));
return schema.ToJson(Formatting.None);
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Nodes;
using PowerToys.DSC.Models.ResourceObjects;
namespace PowerToys.DSC.Models.FunctionData;
/// <summary>
/// Interface for function data related to settings.
/// </summary>
public interface ISettingsFunctionData
{
/// <summary>
/// Gets the input settings resource object.
/// </summary>
public ISettingsResourceObject Input { get; }
/// <summary>
/// Gets the output settings resource object.
/// </summary>
public ISettingsResourceObject Output { get; }
/// <summary>
/// Gets the current settings.
/// </summary>
public void GetState();
/// <summary>
/// Sets the current settings.
/// </summary>
public void SetState();
/// <summary>
/// Tests if the current settings and the desired state are valid.
/// </summary>
/// <returns>True if the current settings match the desired state; otherwise false.</returns>
public bool TestState();
/// <summary>
/// Gets the difference between the current settings and the desired state in JSON format.
/// </summary>
/// <returns>A JSON array representing the differences.</returns>
public JsonArray GetDiffJson();
/// <summary>
/// Gets the schema for the settings resource object.
/// </summary>
/// <returns></returns>
public string Schema();
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using PowerToys.DSC.Models.ResourceObjects;
namespace PowerToys.DSC.Models.FunctionData;
/// <summary>
/// Represents function data for the settings DSC resource.
/// </summary>
/// <typeparam name="TSettingsConfig">The module settings configuration type.</typeparam>
public sealed class SettingsFunctionData<TSettingsConfig> : BaseFunctionData, ISettingsFunctionData
where TSettingsConfig : ISettingsConfig, new()
{
private static readonly SettingsUtils _settingsUtils = new();
private static readonly TSettingsConfig _settingsConfig = new();
private readonly SettingsResourceObject<TSettingsConfig> _input;
private readonly SettingsResourceObject<TSettingsConfig> _output;
/// <inheritdoc/>
public ISettingsResourceObject Input => _input;
/// <inheritdoc/>
public ISettingsResourceObject Output => _output;
public SettingsFunctionData(string? input = null)
{
_output = new();
_input = string.IsNullOrEmpty(input) ? new() : JsonSerializer.Deserialize<SettingsResourceObject<TSettingsConfig>>(input) ?? new();
}
/// <inheritdoc/>
public void GetState()
{
_output.Settings = GetSettings();
}
/// <inheritdoc/>
public void SetState()
{
Debug.Assert(_output.Settings != null, "Output settings should not be null");
SaveSettings(_output.Settings);
}
/// <inheritdoc/>
public bool TestState()
{
var input = JsonSerializer.SerializeToNode(_input.Settings);
var output = JsonSerializer.SerializeToNode(_output.Settings);
return JsonNode.DeepEquals(input, output);
}
/// <inheritdoc/>
public JsonArray GetDiffJson()
{
var diff = new JsonArray();
if (!TestState())
{
diff.Add(SettingsResourceObject<TSettingsConfig>.SettingsJsonPropertyName);
}
return diff;
}
/// <inheritdoc/>
public string Schema()
{
return GenerateSchema<SettingsResourceObject<TSettingsConfig>>();
}
/// <summary>
/// Gets the settings configuration from the settings utils for a specific module.
/// </summary>
/// <returns>The settings configuration for the module.</returns>
private static TSettingsConfig GetSettings()
{
return _settingsUtils.GetSettingsOrDefault<TSettingsConfig>(_settingsConfig.GetModuleName());
}
/// <summary>
/// Saves the settings configuration to the settings utils for a specific module.
/// </summary>
/// <param name="settings">Settings of a specific module</param>
private static void SaveSettings(TSettingsConfig settings)
{
var inputJson = JsonSerializer.Serialize(settings);
_settingsUtils.SaveSettings(inputJson, _settingsConfig.GetModuleName());
}
}

View File

@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace PowerToys.DSC.Models.ResourceObjects;
/// <summary>
/// Base class for all resource objects.
/// </summary>
public class BaseResourceObject
{
private readonly JsonSerializerOptions _options;
public BaseResourceObject()
{
_options = new()
{
WriteIndented = false,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
};
}
/// <summary>
/// Gets or sets whether an instance is in the desired state.
/// </summary>
[JsonPropertyName("_inDesiredState")]
[Description("Indicates whether an instance is in the desired state")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? InDesiredState { get; set; }
/// <summary>
/// Generates a JSON representation of the resource object.
/// </summary>
/// <returns></returns>
public JsonNode ToJson()
{
return JsonSerializer.SerializeToNode(this, GetType(), _options) ?? new JsonObject();
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Nodes;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace PowerToys.DSC.Models.ResourceObjects;
/// <summary>
/// Interface for settings resource objects.
/// </summary>
public interface ISettingsResourceObject
{
/// <summary>
/// Gets or sets the settings configuration.
/// </summary>
public ISettingsConfig SettingsInternal { get; set; }
/// <summary>
/// Gets or sets whether an instance is in the desired state.
/// </summary>
public bool? InDesiredState { get; set; }
/// <summary>
/// Generates a JSON representation of the resource object.
/// </summary>
/// <returns>String representation of the resource object in JSON format.</returns>
public JsonNode ToJson();
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using NJsonSchema.Annotations;
namespace PowerToys.DSC.Models.ResourceObjects;
/// <summary>
/// Represents a settings resource object for a module's settings configuration.
/// </summary>
/// <typeparam name="TSettingsConfig">The type of the settings configuration.</typeparam>
public sealed class SettingsResourceObject<TSettingsConfig> : BaseResourceObject, ISettingsResourceObject
where TSettingsConfig : ISettingsConfig, new()
{
public const string SettingsJsonPropertyName = "settings";
/// <summary>
/// Gets or sets the settings content for the module.
/// </summary>
[JsonPropertyName(SettingsJsonPropertyName)]
[Required]
[Description("The settings content for the module.")]
[JsonSchemaType(typeof(object))]
public TSettingsConfig Settings { get; set; } = new();
/// <inheritdoc/>
[JsonIgnore]
public ISettingsConfig SettingsInternal { get => Settings; set => Settings = (TSettingsConfig)value; }
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Globalization;
using System.Text;
using System.Text.Json;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Options;
/// <summary>
/// Represents an option for specifying JSON input for the dsc command.
/// </summary>
public sealed class InputOption : Option<string>
{
private static readonly CompositeFormat InvalidJsonInputError = CompositeFormat.Parse(Resources.InvalidJsonInputError);
public InputOption()
: base("--input", Resources.InputOptionDescription)
{
AddValidator(OptionValidator);
}
/// <summary>
/// Validates the JSON input provided to the option.
/// </summary>
/// <param name="result">The option result to validate.</param>
private void OptionValidator(OptionResult result)
{
var value = result.GetValueOrDefault<string>() ?? string.Empty;
if (string.IsNullOrEmpty(value))
{
result.ErrorMessage = Resources.InputEmptyOrNullError;
}
else
{
try
{
JsonDocument.Parse(value);
}
catch (Exception e)
{
result.ErrorMessage = string.Format(CultureInfo.InvariantCulture, InvalidJsonInputError, e.Message);
}
}
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Options;
/// <summary>
/// Represents an option for specifying the module name for the dsc command.
/// </summary>
public sealed class ModuleOption : Option<string?>
{
public ModuleOption()
: base("--module", Resources.ModuleOptionDescription)
{
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Globalization;
using System.IO;
using System.Text;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Options;
/// <summary>
/// Represents an option for specifying the output directory for the dsc command.
/// </summary>
public sealed class OutputDirectoryOption : Option<string>
{
private static readonly CompositeFormat InvalidOutputDirectoryError = CompositeFormat.Parse(Resources.InvalidOutputDirectoryError);
public OutputDirectoryOption()
: base("--outputDir", Resources.OutputDirectoryOptionDescription)
{
AddValidator(OptionValidator);
}
/// <summary>
/// Validates the output directory option.
/// </summary>
/// <param name="result">The option result to validate.</param>
private void OptionValidator(OptionResult result)
{
var value = result.GetValueOrDefault<string>() ?? string.Empty;
if (string.IsNullOrEmpty(value))
{
result.ErrorMessage = Resources.OutputDirectoryEmptyOrNullError;
}
else if (!Directory.Exists(value))
{
result.ErrorMessage = string.Format(CultureInfo.InvariantCulture, InvalidOutputDirectoryError, value);
}
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Globalization;
using System.Text;
using PowerToys.DSC.Properties;
namespace PowerToys.DSC.Options;
/// <summary>
/// Represents an option for specifying the resource name for the dsc command.
/// </summary>
public sealed class ResourceOption : Option<string>
{
private static readonly CompositeFormat InvalidResourceNameError = CompositeFormat.Parse(Resources.InvalidResourceNameError);
private readonly IList<string> _resources = [];
public ResourceOption(IList<string> resources)
: base("--resource", Resources.ResourceOptionDescription)
{
_resources = resources;
IsRequired = true;
AddValidator(OptionValidator);
}
/// <summary>
/// Validates the resource option to ensure that the specified resource name is valid.
/// </summary>
/// <param name="result">The option result to validate.</param>
private void OptionValidator(OptionResult result)
{
var value = result.GetValueOrDefault<string>() ?? string.Empty;
if (!_resources.Contains(value))
{
result.ErrorMessage = string.Format(CultureInfo.InvariantCulture, InvalidResourceNameError, string.Join(", ", _resources));
}
}
}

View File

@@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AssemblyName>PowerToys.DSC</AssemblyName>
<AssemblyDescription>PowerToys DSC</AssemblyDescription>
<RootNamespace>PowerToys.DSC</RootNamespace>
<Nullable>enable</Nullable>
<!-- Ensure WindowsDesktop runtime pack is included for consistent WindowsBase.dll version -->
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NJsonSchema" />
<PackageReference Include="System.CommandLine" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<!-- In debug mode, generate the DSC resource JSON files -->
<Target Name="GenerateDscResourceJsonFiles" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
<Message Text="Generating DSC resource JSON files inside ..." Importance="high" />
<Exec Command="dotnet &quot;$(TargetPath)&quot; manifest --resource settings --outputDir &quot;$(TargetDir)\&quot;" />
</Target>
</Project>

View File

@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Threading.Tasks;
using PowerToys.DSC.Commands;
namespace PowerToys.DSC;
/// <summary>
/// Main entry point for the PowerToys Desired State Configuration CLI application.
/// </summary>
public class Program
{
public static async Task<int> Main(string[] args)
{
var rootCommand = new RootCommand(Properties.Resources.PowerToysDSC);
rootCommand.AddCommand(new GetCommand());
rootCommand.AddCommand(new SetCommand());
rootCommand.AddCommand(new ExportCommand());
rootCommand.AddCommand(new TestCommand());
rootCommand.AddCommand(new SchemaCommand());
rootCommand.AddCommand(new ManifestCommand());
rootCommand.AddCommand(new ModulesCommand());
return await rootCommand.InvokeAsync(args);
}
}

View File

@@ -0,0 +1,234 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PowerToys.DSC.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PowerToys.DSC.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Get all state instances.
/// </summary>
internal static string ExportCommandDescription {
get {
return ResourceManager.GetString("ExportCommandDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to write manifests to directory &apos;{0}&apos;: {1}.
/// </summary>
internal static string FailedToWriteManifests {
get {
return ResourceManager.GetString("FailedToWriteManifests", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Get the resource state.
/// </summary>
internal static string GetCommandDescription {
get {
return ResourceManager.GetString("GetCommandDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Input cannot be empty or null.
/// </summary>
internal static string InputEmptyOrNullError {
get {
return ResourceManager.GetString("InputEmptyOrNullError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The JSON input.
/// </summary>
internal static string InputOptionDescription {
get {
return ResourceManager.GetString("InputOptionDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid JSON input: {0}.
/// </summary>
internal static string InvalidJsonInputError {
get {
return ResourceManager.GetString("InvalidJsonInputError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid output directory: {0}.
/// </summary>
internal static string InvalidOutputDirectoryError {
get {
return ResourceManager.GetString("InvalidOutputDirectoryError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid resource name. Valid resource names are: {0}.
/// </summary>
internal static string InvalidResourceNameError {
get {
return ResourceManager.GetString("InvalidResourceNameError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Get the manifest of the dsc resource.
/// </summary>
internal static string ManifestCommandDescription {
get {
return ResourceManager.GetString("ManifestCommandDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Module &apos;{0}&apos; is not supported for the resource {1}. Use the &apos;module&apos; command to list available modules..
/// </summary>
internal static string ModuleNotSupportedByResource {
get {
return ResourceManager.GetString("ModuleNotSupportedByResource", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The module name.
/// </summary>
internal static string ModuleOptionDescription {
get {
return ResourceManager.GetString("ModuleOptionDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Get all supported modules for a specific resource.
/// </summary>
internal static string ModulesCommandDescription {
get {
return ResourceManager.GetString("ModulesCommandDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output directory cannot be empty or null.
/// </summary>
internal static string OutputDirectoryEmptyOrNullError {
get {
return ResourceManager.GetString("OutputDirectoryEmptyOrNullError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The output directory.
/// </summary>
internal static string OutputDirectoryOptionDescription {
get {
return ResourceManager.GetString("OutputDirectoryOptionDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to PowerToys Desired State Configuration commands.
/// </summary>
internal static string PowerToysDSC {
get {
return ResourceManager.GetString("PowerToysDSC", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The resource name.
/// </summary>
internal static string ResourceOptionDescription {
get {
return ResourceManager.GetString("ResourceOptionDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Outputs schema of the resource.
/// </summary>
internal static string SchemaCommandDescription {
get {
return ResourceManager.GetString("SchemaCommandDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Set the resource state.
/// </summary>
internal static string SetCommandDescription {
get {
return ResourceManager.GetString("SetCommandDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Test the resource state.
/// </summary>
internal static string TestCommandDescription {
get {
return ResourceManager.GetString("TestCommandDescription", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="PowerToysDSC" xml:space="preserve">
<value>PowerToys Desired State Configuration commands</value>
<comment>{Locked="PowerToys Desired State Configuration"}</comment>
</data>
<data name="ModuleNotSupportedByResource" xml:space="preserve">
<value>Module '{0}' is not supported for the resource {1}. Use the 'module' command to list available modules.</value>
<comment>{Locked="'module'","{0}","{1}"}</comment>
</data>
<data name="ExportCommandDescription" xml:space="preserve">
<value>Get all state instances</value>
</data>
<data name="GetCommandDescription" xml:space="preserve">
<value>Get the resource state</value>
</data>
<data name="ManifestCommandDescription" xml:space="preserve">
<value>Get the manifest of the dsc resource</value>
</data>
<data name="ModulesCommandDescription" xml:space="preserve">
<value>Get all supported modules for a specific resource</value>
</data>
<data name="SchemaCommandDescription" xml:space="preserve">
<value>Outputs schema of the resource</value>
</data>
<data name="SetCommandDescription" xml:space="preserve">
<value>Set the resource state</value>
</data>
<data name="TestCommandDescription" xml:space="preserve">
<value>Test the resource state</value>
</data>
<data name="InputEmptyOrNullError" xml:space="preserve">
<value>Input cannot be empty or null</value>
</data>
<data name="FailedToWriteManifests" xml:space="preserve">
<value>Failed to write manifests to directory '{0}': {1}</value>
<comment>{Locked="{0}","{1}"}</comment>
</data>
<data name="InputOptionDescription" xml:space="preserve">
<value>The JSON input</value>
</data>
<data name="ModuleOptionDescription" xml:space="preserve">
<value>The module name</value>
</data>
<data name="OutputDirectoryOptionDescription" xml:space="preserve">
<value>The output directory</value>
</data>
<data name="ResourceOptionDescription" xml:space="preserve">
<value>The resource name</value>
</data>
<data name="InvalidJsonInputError" xml:space="preserve">
<value>Invalid JSON input: {0}</value>
<comment>{Locked="{0}"}</comment>
</data>
<data name="OutputDirectoryEmptyOrNullError" xml:space="preserve">
<value>Output directory cannot be empty or null</value>
</data>
<data name="InvalidOutputDirectoryError" xml:space="preserve">
<value>Invalid output directory: {0}</value>
<comment>{Locked="{0}"}</comment>
</data>
<data name="InvalidResourceNameError" xml:space="preserve">
<value>Invalid resource name. Valid resource names are: {0}</value>
<comment>{Locked="{0}"}</comment>
</data>
</root>

View File

@@ -94,6 +94,21 @@ public:
}
}
static void SetCrosshairsOrientation(CrosshairsOrientation orientation)
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([orientation]() {
if (instance != nullptr)
{
instance->m_crosshairs_orientation = orientation;
instance->UpdateCrosshairsPosition();
}
});
}
}
private:
enum class MouseButton
{
@@ -147,6 +162,7 @@ private:
int m_crosshairs_border_size = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_BORDER_SIZE;
bool m_crosshairs_is_fixed_length_enabled = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED;
int m_crosshairs_fixed_length = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_FIXED_LENGTH;
CrosshairsOrientation m_crosshairs_orientation = static_cast<CrosshairsOrientation>(INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_ORIENTATION);
float m_crosshairs_opacity = max(0.f, min(1.f, (float)INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_OPACITY / 100.0f));
bool m_crosshairs_auto_hide = INCLUSIVE_MOUSE_DEFAULT_AUTO_HIDE;
};
@@ -286,6 +302,8 @@ void InclusiveCrosshairs::UpdateCrosshairsPosition()
float halfPixelAdjustment = m_crosshairs_thickness % 2 == 1 ? 0.5f : 0.0f;
float borderSizePadding = m_crosshairs_border_size * 2.f;
// Left and Right crosshairs (horizontal line)
if (m_crosshairs_orientation == CrosshairsOrientation::Both || m_crosshairs_orientation == CrosshairsOrientation::HorizontalOnly)
{
float leftCrosshairsFullScreenLength = ptCursor.x - ptMonitorUpperLeft.x - m_crosshairs_radius + halfPixelAdjustment * 2.f;
float leftCrosshairsLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length : leftCrosshairsFullScreenLength;
@@ -294,9 +312,7 @@ void InclusiveCrosshairs::UpdateCrosshairsPosition()
m_left_crosshairs_border.Size({ leftCrosshairsBorderLength, m_crosshairs_thickness + borderSizePadding });
m_left_crosshairs.Offset({ ptCursor.x - m_crosshairs_radius + halfPixelAdjustment * 2.f, ptCursor.y + halfPixelAdjustment, .0f });
m_left_crosshairs.Size({ leftCrosshairsLength, static_cast<float>(m_crosshairs_thickness) });
}
{
float rightCrosshairsFullScreenLength = static_cast<float>(ptMonitorBottomRight.x) - ptCursor.x - m_crosshairs_radius;
float rightCrosshairsLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length : rightCrosshairsFullScreenLength;
float rightCrosshairsBorderLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length + borderSizePadding : rightCrosshairsFullScreenLength + m_crosshairs_border_size;
@@ -305,7 +321,17 @@ void InclusiveCrosshairs::UpdateCrosshairsPosition()
m_right_crosshairs.Offset({ static_cast<float>(ptCursor.x) + m_crosshairs_radius, ptCursor.y + halfPixelAdjustment, .0f });
m_right_crosshairs.Size({ rightCrosshairsLength, static_cast<float>(m_crosshairs_thickness) });
}
else
{
// Hide horizontal crosshairs by setting size to 0
m_left_crosshairs_border.Size({ 0.0f, 0.0f });
m_left_crosshairs.Size({ 0.0f, 0.0f });
m_right_crosshairs_border.Size({ 0.0f, 0.0f });
m_right_crosshairs.Size({ 0.0f, 0.0f });
}
// Top and Bottom crosshairs (vertical line)
if (m_crosshairs_orientation == CrosshairsOrientation::Both || m_crosshairs_orientation == CrosshairsOrientation::VerticalOnly)
{
float topCrosshairsFullScreenLength = ptCursor.y - ptMonitorUpperLeft.y - m_crosshairs_radius + halfPixelAdjustment * 2.f;
float topCrosshairsLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length : topCrosshairsFullScreenLength;
@@ -314,9 +340,7 @@ void InclusiveCrosshairs::UpdateCrosshairsPosition()
m_top_crosshairs_border.Size({ m_crosshairs_thickness + borderSizePadding, topCrosshairsBorderLength });
m_top_crosshairs.Offset({ ptCursor.x + halfPixelAdjustment, ptCursor.y - m_crosshairs_radius + halfPixelAdjustment * 2.f, .0f });
m_top_crosshairs.Size({ static_cast<float>(m_crosshairs_thickness), topCrosshairsLength });
}
{
float bottomCrosshairsFullScreenLength = static_cast<float>(ptMonitorBottomRight.y) - ptCursor.y - m_crosshairs_radius;
float bottomCrosshairsLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length : bottomCrosshairsFullScreenLength;
float bottomCrosshairsBorderLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length + borderSizePadding : bottomCrosshairsFullScreenLength + m_crosshairs_border_size;
@@ -325,6 +349,14 @@ void InclusiveCrosshairs::UpdateCrosshairsPosition()
m_bottom_crosshairs.Offset({ ptCursor.x + halfPixelAdjustment, static_cast<float>(ptCursor.y) + m_crosshairs_radius, .0f });
m_bottom_crosshairs.Size({ static_cast<float>(m_crosshairs_thickness), bottomCrosshairsLength });
}
else
{
// Hide vertical crosshairs by setting size to 0
m_top_crosshairs_border.Size({ 0.0f, 0.0f });
m_top_crosshairs.Size({ 0.0f, 0.0f });
m_bottom_crosshairs_border.Size({ 0.0f, 0.0f });
m_bottom_crosshairs.Size({ 0.0f, 0.0f });
}
}
LRESULT CALLBACK InclusiveCrosshairs::MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept
@@ -398,6 +430,7 @@ void InclusiveCrosshairs::ApplySettings(InclusiveCrosshairsSettings& settings, b
m_crosshairs_auto_hide = settings.crosshairsAutoHide;
m_crosshairs_is_fixed_length_enabled = settings.crosshairsIsFixedLengthEnabled;
m_crosshairs_fixed_length = settings.crosshairsFixedLength;
m_crosshairs_orientation = settings.crosshairsOrientation;
if (applyToRunTimeObjects)
{
@@ -618,6 +651,11 @@ void InclusiveCrosshairsSetExternalControl(bool enabled)
InclusiveCrosshairs::SetExternalControl(enabled);
}
void InclusiveCrosshairsSetOrientation(CrosshairsOrientation orientation)
{
InclusiveCrosshairs::SetCrosshairsOrientation(orientation);
}
int InclusiveCrosshairsMain(HINSTANCE hInstance, InclusiveCrosshairsSettings& settings)
{
Logger::info("Starting a crosshairs instance.");

View File

@@ -10,8 +10,16 @@ constexpr int INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_BORDER_SIZE = 1;
constexpr bool INCLUSIVE_MOUSE_DEFAULT_AUTO_HIDE = false;
constexpr bool INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED = false;
constexpr int INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_FIXED_LENGTH = 1;
constexpr int INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_ORIENTATION = 0; // 0=Both, 1=Vertical, 2=Horizontal
constexpr bool INCLUSIVE_MOUSE_DEFAULT_AUTO_ACTIVATE = false;
enum struct CrosshairsOrientation : int
{
Both = 0,
VerticalOnly = 1,
HorizontalOnly = 2,
};
struct InclusiveCrosshairsSettings
{
winrt::Windows::UI::Color crosshairsColor = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_COLOR;
@@ -23,6 +31,7 @@ struct InclusiveCrosshairsSettings
bool crosshairsAutoHide = INCLUSIVE_MOUSE_DEFAULT_AUTO_HIDE;
bool crosshairsIsFixedLengthEnabled = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED;
int crosshairsFixedLength = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_FIXED_LENGTH;
CrosshairsOrientation crosshairsOrientation = static_cast<CrosshairsOrientation>(INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_ORIENTATION);
bool autoActivate = INCLUSIVE_MOUSE_DEFAULT_AUTO_ACTIVATE;
};
@@ -35,3 +44,4 @@ void InclusiveCrosshairsRequestUpdatePosition();
void InclusiveCrosshairsEnsureOn();
void InclusiveCrosshairsEnsureOff();
void InclusiveCrosshairsSetExternalControl(bool enabled);
void InclusiveCrosshairsSetOrientation(CrosshairsOrientation orientation);

View File

@@ -80,7 +80,7 @@
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>..\..\..\;..\..\..\modules;..\..\..\common\Telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>

View File

@@ -8,6 +8,7 @@
#include <thread>
#include <chrono>
#include <memory>
#include <algorithm>
extern void InclusiveCrosshairsRequestUpdatePosition();
extern void InclusiveCrosshairsEnsureOn();
@@ -30,6 +31,7 @@ namespace
const wchar_t JSON_KEY_CROSSHAIRS_AUTO_HIDE[] = L"crosshairs_auto_hide";
const wchar_t JSON_KEY_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED[] = L"crosshairs_is_fixed_length_enabled";
const wchar_t JSON_KEY_CROSSHAIRS_FIXED_LENGTH[] = L"crosshairs_fixed_length";
const wchar_t JSON_KEY_CROSSHAIRS_ORIENTATION[] = L"crosshairs_orientation";
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
const wchar_t JSON_KEY_GLIDE_TRAVEL_SPEED[] = L"gliding_travel_speed";
const wchar_t JSON_KEY_GLIDE_DELAY_SPEED[] = L"gliding_delay_speed";
@@ -62,6 +64,9 @@ const static wchar_t* MODULE_NAME = L"MousePointerCrosshairs";
// Add a description that will we shown in the module settings page.
const static wchar_t* MODULE_DESC = L"<no description>";
class MousePointerCrosshairs; // fwd
static std::atomic<MousePointerCrosshairs*> g_instance{ nullptr }; // for hook callback
// Implement the PowerToy Module Interface and all the required methods.
class MousePointerCrosshairs : public PowertoyModuleIface
{
@@ -70,8 +75,11 @@ private:
bool m_enabled = false;
// Additional hotkeys (legacy API) to support multiple shortcuts
Hotkey m_activationHotkey{}; // Crosshairs toggle
Hotkey m_glidingHotkey{}; // Gliding cursor state machine
Hotkey m_activationHotkey{}; // Crosshairs toggle
Hotkey m_glidingHotkey{}; // Gliding cursor state machine
// Low-level keyboard hook (Escape to cancel gliding)
HHOOK m_keyboardHook = nullptr;
// Shared state for worker threads (decoupled from this lifetime)
struct State
@@ -84,7 +92,7 @@ private:
int currentYPos{ 0 };
int currentXSpeed{ 0 }; // pixels per base window
int currentYSpeed{ 0 }; // pixels per base window
int xPosSnapshot{ 0 }; // xPos captured at end of horizontal scan
int xPosSnapshot{ 0 }; // xPos captured at end of horizontal scan
// Fractional accumulators to spread movement across 10ms ticks
double xFraction{ 0.0 };
@@ -92,9 +100,9 @@ private:
// Speeds represent pixels per 200ms (min 5, max 60 enforced by UI/settings)
int fastHSpeed{ 30 }; // pixels per base window
int slowHSpeed{ 5 }; // pixels per base window
int slowHSpeed{ 5 }; // pixels per base window
int fastVSpeed{ 30 }; // pixels per base window
int slowVSpeed{ 5 }; // pixels per base window
int slowVSpeed{ 5 }; // pixels per base window
};
std::shared_ptr<State> m_state;
@@ -120,13 +128,16 @@ public:
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mousePointerCrosshairsLoggerName);
m_state = std::make_shared<State>();
init_settings();
g_instance.store(this, std::memory_order_release);
};
// Destroy the powertoy and free memory
virtual void destroy() override
{
UninstallKeyboardHook();
StopXTimer();
StopYTimer();
g_instance.store(nullptr, std::memory_order_release);
// Release shared state so worker threads (if any) exit when weak_ptr lock fails
m_state.reset();
delete this;
@@ -196,6 +207,7 @@ public:
{
m_enabled = false;
Trace::EnableMousePointerCrosshairs(false);
UninstallKeyboardHook();
StopXTimer();
StopYTimer();
m_glideState = 0;
@@ -220,7 +232,7 @@ public:
if (buffer && buffer_size >= 2)
{
buffer[0] = m_activationHotkey; // Crosshairs toggle
buffer[1] = m_glidingHotkey; // Gliding cursor toggle
buffer[1] = m_glidingHotkey; // Gliding cursor toggle
}
return 2;
}
@@ -256,6 +268,27 @@ private:
SendInput(2, inputs, sizeof(INPUT));
}
// Cancel gliding without performing the final click (Escape handling)
void CancelGliding()
{
int state = m_glideState.load();
if (state == 0)
{
return; // nothing to cancel
}
StopXTimer();
StopYTimer();
m_glideState = 0;
InclusiveCrosshairsEnsureOff();
InclusiveCrosshairsSetExternalControl(false);
if (auto s = m_state)
{
s->xFraction = 0.0;
s->yFraction = 0.0;
}
Logger::debug("Gliding cursor cancelled via Escape key");
}
// Stateless helpers operating on shared State
static void PositionCursorX(const std::shared_ptr<State>& s)
{
@@ -398,10 +431,14 @@ private:
{
case 0:
{
// For detect for cancel key
InstallKeyboardHook();
// Ensure crosshairs on (do not toggle off if already on)
InclusiveCrosshairsEnsureOn();
// Disable internal mouse hook so we control position updates explicitly
InclusiveCrosshairsSetExternalControl(true);
// Override crosshairs to show both for Gliding Cursor
InclusiveCrosshairsSetOrientation(CrosshairsOrientation::Both);
s->currentXPos = 0;
s->currentXSpeed = s->fastHSpeed;
@@ -444,12 +481,15 @@ private:
case 4:
default:
{
UninstallKeyboardHook();
// Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state
StopYTimer();
m_glideState = 0;
LeftClick();
InclusiveCrosshairsEnsureOff();
InclusiveCrosshairsSetExternalControl(false);
// Restore original crosshairs orientation setting
InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation);
s->xFraction = 0.0;
s->yFraction = 0.0;
break;
@@ -457,6 +497,51 @@ private:
}
}
// Low-level keyboard hook procedures
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HC_ACTION)
{
const KBDLLHOOKSTRUCT* kb = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
if (kb && kb->vkCode == VK_ESCAPE && (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN))
{
if (auto inst = g_instance.load(std::memory_order_acquire))
{
if (inst->m_enabled && inst->m_glideState.load() != 0)
{
inst->UninstallKeyboardHook();
inst->CancelGliding();
}
}
}
}
// Do not swallow Escape; pass it through
return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
void InstallKeyboardHook()
{
if (m_keyboardHook)
{
return; // already installed
}
m_keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, m_hModule, 0);
if (!m_keyboardHook)
{
Logger::error("Failed to install low-level keyboard hook for MousePointerCrosshairs (Escape cancel). GetLastError={}.", GetLastError());
}
}
void UninstallKeyboardHook()
{
if (m_keyboardHook)
{
UnhookWindowsHookEx(m_keyboardHook);
m_keyboardHook = nullptr;
}
}
// Load the settings file.
void init_settings()
{
@@ -475,264 +560,287 @@ private:
void parse_settings(PowerToysSettings::PowerToyValues& settings)
{
// TODO: refactor to use common/utils/json.h instead
// Refactored JSON parsing: uses inline try-catch blocks for each property for clarity and error handling
auto settingsObject = settings.get_raw_json();
InclusiveCrosshairsSettings inclusiveCrosshairsSettings;
if (settingsObject.GetView().Size())
{
try
{
// Parse primary activation HotKey (for centralized hook)
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
// Parse activation hotkey
try
{
auto jsonHotkeyObject = propertiesObject.GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonHotkeyObject);
m_activationHotkey.win = hotkey.win_pressed();
m_activationHotkey.ctrl = hotkey.ctrl_pressed();
m_activationHotkey.shift = hotkey.shift_pressed();
m_activationHotkey.alt = hotkey.alt_pressed();
m_activationHotkey.key = static_cast<unsigned char>(hotkey.get_code());
}
catch (...)
{
Logger::warn("Failed to initialize Mouse Pointer Crosshairs activation shortcut");
}
// Map to legacy Hotkey for multi-hotkey API
m_activationHotkey.win = hotkey.win_pressed();
m_activationHotkey.ctrl = hotkey.ctrl_pressed();
m_activationHotkey.shift = hotkey.shift_pressed();
m_activationHotkey.alt = hotkey.alt_pressed();
m_activationHotkey.key = static_cast<unsigned char>(hotkey.get_code());
}
catch (...)
{
Logger::warn("Failed to initialize Mouse Pointer Crosshairs activation shortcut");
}
try
{
// Parse Gliding Cursor HotKey
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDING_ACTIVATION_SHORTCUT);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
m_glidingHotkey.win = hotkey.win_pressed();
m_glidingHotkey.ctrl = hotkey.ctrl_pressed();
m_glidingHotkey.shift = hotkey.shift_pressed();
m_glidingHotkey.alt = hotkey.alt_pressed();
m_glidingHotkey.key = static_cast<unsigned char>(hotkey.get_code());
}
catch (...)
{
// note that this is also defined in src\settings-ui\Settings.UI.Library\MousePointerCrosshairsProperties.cs, DefaultGlidingCursorActivationShortcut
// both need to be kept in sync!
Logger::warn("Failed to initialize Gliding Cursor activation shortcut. Using default Win+Alt+.");
m_glidingHotkey.win = true;
m_glidingHotkey.alt = true;
m_glidingHotkey.ctrl = false;
m_glidingHotkey.shift = false;
m_glidingHotkey.key = VK_OEM_PERIOD;
}
try
{
// Parse Opacity
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_OPACITY);
int value = static_cast<uint8_t>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
// Parse gliding cursor hotkey
try
{
inclusiveCrosshairsSettings.crosshairsOpacity = value;
auto jsonHotkeyObject = propertiesObject.GetNamedObject(JSON_KEY_GLIDING_ACTIVATION_SHORTCUT);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonHotkeyObject);
m_glidingHotkey.win = hotkey.win_pressed();
m_glidingHotkey.ctrl = hotkey.ctrl_pressed();
m_glidingHotkey.shift = hotkey.shift_pressed();
m_glidingHotkey.alt = hotkey.alt_pressed();
m_glidingHotkey.key = static_cast<unsigned char>(hotkey.get_code());
}
else
catch (...)
{
throw std::runtime_error("Invalid Opacity value");
Logger::warn("Failed to initialize Gliding Cursor activation shortcut. Using default Win+Alt+.");
m_glidingHotkey.win = true;
m_glidingHotkey.alt = true;
m_glidingHotkey.ctrl = false;
m_glidingHotkey.shift = false;
m_glidingHotkey.key = VK_OEM_PERIOD;
}
// Parse individual properties with error handling and defaults
try
{
if (propertiesObject.HasKey(L"crosshairs_opacity"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_opacity");
if (propertyObj.HasKey(L"value"))
{
inclusiveCrosshairsSettings.crosshairsOpacity = static_cast<int>(propertyObj.GetNamedNumber(L"value"));
}
}
}
catch (...) { /* Use default value */ }
try
{
if (propertiesObject.HasKey(L"crosshairs_radius"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_radius");
if (propertyObj.HasKey(L"value"))
{
inclusiveCrosshairsSettings.crosshairsRadius = static_cast<int>(propertyObj.GetNamedNumber(L"value"));
}
}
}
catch (...) { /* Use default value */ }
try
{
if (propertiesObject.HasKey(L"crosshairs_thickness"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_thickness");
if (propertyObj.HasKey(L"value"))
{
inclusiveCrosshairsSettings.crosshairsThickness = static_cast<int>(propertyObj.GetNamedNumber(L"value"));
}
}
}
catch (...) { /* Use default value */ }
try
{
if (propertiesObject.HasKey(L"crosshairs_border_size"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_border_size");
if (propertyObj.HasKey(L"value"))
{
inclusiveCrosshairsSettings.crosshairsBorderSize = static_cast<int>(propertyObj.GetNamedNumber(L"value"));
}
}
}
catch (...) { /* Use default value */ }
try
{
if (propertiesObject.HasKey(L"crosshairs_fixed_length"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_fixed_length");
if (propertyObj.HasKey(L"value"))
{
inclusiveCrosshairsSettings.crosshairsFixedLength = static_cast<int>(propertyObj.GetNamedNumber(L"value"));
}
}
}
catch (...) { /* Use default value */ }
try
{
if (propertiesObject.HasKey(L"crosshairs_auto_hide"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_auto_hide");
if (propertyObj.HasKey(L"value"))
{
inclusiveCrosshairsSettings.crosshairsAutoHide = propertyObj.GetNamedBoolean(L"value");
}
}
}
catch (...) { /* Use default value */ }
try
{
if (propertiesObject.HasKey(L"crosshairs_is_fixed_length_enabled"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_is_fixed_length_enabled");
if (propertyObj.HasKey(L"value"))
{
inclusiveCrosshairsSettings.crosshairsIsFixedLengthEnabled = propertyObj.GetNamedBoolean(L"value");
}
}
}
catch (...) { /* Use default value */ }
try
{
if (propertiesObject.HasKey(L"auto_activate"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"auto_activate");
if (propertyObj.HasKey(L"value"))
{
inclusiveCrosshairsSettings.autoActivate = propertyObj.GetNamedBoolean(L"value");
}
}
}
catch (...) { /* Use default value */ }
// Parse orientation with validation - this fixes the original issue!
try
{
if (propertiesObject.HasKey(L"crosshairs_orientation"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_orientation");
if (propertyObj.HasKey(L"value"))
{
int orientationValue = static_cast<int>(propertyObj.GetNamedNumber(L"value"));
if (orientationValue >= 0 && orientationValue <= 2)
{
inclusiveCrosshairsSettings.crosshairsOrientation = static_cast<CrosshairsOrientation>(orientationValue);
}
}
}
}
catch (...) { /* Use default value (Both = 0) */ }
// Parse colors with validation
try
{
if (propertiesObject.HasKey(L"crosshairs_color"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_color");
if (propertyObj.HasKey(L"value"))
{
std::wstring crosshairsColorValue = std::wstring(propertyObj.GetNamedString(L"value").c_str());
uint8_t r, g, b;
if (checkValidRGB(crosshairsColorValue, &r, &g, &b))
{
inclusiveCrosshairsSettings.crosshairsColor = winrt::Windows::UI::ColorHelper::FromArgb(255, r, g, b);
}
}
}
}
catch (...) { /* Use default color */ }
try
{
if (propertiesObject.HasKey(L"crosshairs_border_color"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_border_color");
if (propertyObj.HasKey(L"value"))
{
std::wstring borderColorValue = std::wstring(propertyObj.GetNamedString(L"value").c_str());
uint8_t r, g, b;
if (checkValidRGB(borderColorValue, &r, &g, &b))
{
inclusiveCrosshairsSettings.crosshairsBorderColor = winrt::Windows::UI::ColorHelper::FromArgb(255, r, g, b);
}
}
}
}
catch (...) { /* Use default border color */ }
// Parse speed settings with validation
try
{
if (propertiesObject.HasKey(L"gliding_travel_speed"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"gliding_travel_speed");
if (propertyObj.HasKey(L"value") && m_state)
{
int travelSpeedValue = static_cast<int>(propertyObj.GetNamedNumber(L"value"));
if (travelSpeedValue >= 5 && travelSpeedValue <= 60)
{
m_state->fastHSpeed = travelSpeedValue;
m_state->fastVSpeed = travelSpeedValue;
}
else
{
// Clamp to valid range
int clampedValue = travelSpeedValue;
if (clampedValue < 5) clampedValue = 5;
if (clampedValue > 60) clampedValue = 60;
m_state->fastHSpeed = clampedValue;
m_state->fastVSpeed = clampedValue;
Logger::warn("Travel speed value out of range, clamped to valid range");
}
}
}
}
catch (...)
{
if (m_state)
{
m_state->fastHSpeed = 25;
m_state->fastVSpeed = 25;
}
}
try
{
if (propertiesObject.HasKey(L"gliding_delay_speed"))
{
auto propertyObj = propertiesObject.GetNamedObject(L"gliding_delay_speed");
if (propertyObj.HasKey(L"value") && m_state)
{
int delaySpeedValue = static_cast<int>(propertyObj.GetNamedNumber(L"value"));
if (delaySpeedValue >= 5 && delaySpeedValue <= 60)
{
m_state->slowHSpeed = delaySpeedValue;
m_state->slowVSpeed = delaySpeedValue;
}
else
{
// Clamp to valid range
int clampedValue = delaySpeedValue;
if (clampedValue < 5) clampedValue = 5;
if (clampedValue > 60) clampedValue = 60;
m_state->slowHSpeed = clampedValue;
m_state->slowVSpeed = clampedValue;
Logger::warn("Delay speed value out of range, clamped to valid range");
}
}
}
}
catch (...)
{
if (m_state)
{
m_state->slowHSpeed = 5;
m_state->slowVSpeed = 5;
}
}
}
catch (...)
{
Logger::warn("Failed to initialize Opacity from settings. Will use default value");
}
try
{
// Parse crosshairs color
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_COLOR);
auto crosshairsColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
uint8_t r, g, b;
if (!checkValidRGB(crosshairsColor, &r, &g, &b))
{
Logger::error("Crosshairs color RGB value is invalid. Will use default value");
}
else
{
inclusiveCrosshairsSettings.crosshairsColor = winrt::Windows::UI::ColorHelper::FromArgb(255, r, g, b);
}
}
catch (...)
{
Logger::warn("Failed to initialize crosshairs color from settings. Will use default value");
}
try
{
// Parse Radius
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_RADIUS);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
inclusiveCrosshairsSettings.crosshairsRadius = value;
}
else
{
throw std::runtime_error("Invalid Radius value");
}
}
catch (...)
{
Logger::warn("Failed to initialize Radius from settings. Will use default value");
}
try
{
// Parse Thickness
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_THICKNESS);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
inclusiveCrosshairsSettings.crosshairsThickness = value;
}
else
{
throw std::runtime_error("Invalid Thickness value");
}
}
catch (...)
{
Logger::warn("Failed to initialize Thickness from settings. Will use default value");
}
try
{
// Parse crosshairs border color
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_BORDER_COLOR);
auto crosshairsBorderColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
uint8_t r, g, b;
if (!checkValidRGB(crosshairsBorderColor, &r, &g, &b))
{
Logger::error("Crosshairs border color RGB value is invalid. Will use default value");
}
else
{
inclusiveCrosshairsSettings.crosshairsBorderColor = winrt::Windows::UI::ColorHelper::FromArgb(255, r, g, b);
}
}
catch (...)
{
Logger::warn("Failed to initialize crosshairs border color from settings. Will use default value");
}
try
{
// Parse border size
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_BORDER_SIZE);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
inclusiveCrosshairsSettings.crosshairsBorderSize = value;
}
else
{
throw std::runtime_error("Invalid Border Color value");
}
}
catch (...)
{
Logger::warn("Failed to initialize border color from settings. Will use default value");
}
try
{
// Parse auto hide
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_AUTO_HIDE);
inclusiveCrosshairsSettings.crosshairsAutoHide = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to initialize auto hide from settings. Will use default value");
}
try
{
// Parse whether the fixed length is enabled
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED);
bool value = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
inclusiveCrosshairsSettings.crosshairsIsFixedLengthEnabled = value;
}
catch (...)
{
Logger::warn("Failed to initialize fixed length enabled from settings. Will use default value");
}
try
{
// Parse fixed length
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_FIXED_LENGTH);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
inclusiveCrosshairsSettings.crosshairsFixedLength = value;
}
else
{
throw std::runtime_error("Invalid Fixed Length value");
}
}
catch (...)
{
Logger::warn("Failed to initialize fixed length from settings. Will use default value");
}
try
{
// Parse auto activate
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_AUTO_ACTIVATE);
inclusiveCrosshairsSettings.autoActivate = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to initialize auto activate from settings. Will use default value");
}
try
{
// Parse Travel speed (fast speed mapping)
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_TRAVEL_SPEED);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 5 && value <= 60)
{
m_state->fastHSpeed = value;
m_state->fastVSpeed = value;
}
else if (value < 5)
{
m_state->fastHSpeed = 5; m_state->fastVSpeed = 5;
}
else
{
m_state->fastHSpeed = 60; m_state->fastVSpeed = 60;
}
}
catch (...)
{
Logger::warn("Failed to initialize gliding travel speed from settings. Using default 25.");
if (m_state)
{
m_state->fastHSpeed = 25;
m_state->fastVSpeed = 25;
}
}
try
{
// Parse Delay speed (slow speed mapping)
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_DELAY_SPEED);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 5 && value <= 60)
{
m_state->slowHSpeed = value;
m_state->slowVSpeed = value;
}
else if (value < 5)
{
m_state->slowHSpeed = 5; m_state->slowVSpeed = 5;
}
else
{
m_state->slowHSpeed = 60; m_state->slowVSpeed = 60;
}
}
catch (...)
{
Logger::warn("Failed to initialize gliding delay speed from settings. Using default 5.");
if (m_state)
{
m_state->slowHSpeed = 5;
m_state->slowVSpeed = 5;
}
Logger::warn("Error parsing some MousePointerCrosshairs properties. Using defaults for failed properties.");
}
}
else
@@ -740,6 +848,7 @@ private:
Logger::info("Mouse Pointer Crosshairs settings are empty");
}
// Set default hotkeys if not configured
if (m_activationHotkey.key == 0)
{
m_activationHotkey.win = true;
@@ -756,6 +865,7 @@ private:
m_glidingHotkey.shift = false;
m_glidingHotkey.key = VK_OEM_PERIOD;
}
m_inclusiveCrosshairsSettings = inclusiveCrosshairsSettings;
}
};

View File

@@ -0,0 +1,166 @@
# VSCode Debug Setup for TopToolbar
This directory contains VSCode debug configuration for the TopToolbar project.
## Quick Start
### Prerequisites
1. **Install Required Extensions** (VSCode will prompt you)
- C# Dev Kit (ms-dotnettools.csharp)
- .NET Runtime Install Tool (ms-dotnettools.vscode-dotnet-runtime)
2. **Install .NET SDK**
- .NET 8.0 or later
- Download from: https://dotnet.microsoft.com/download
3. **Required Tools**
- Visual Studio 2022 (Insiders or higher for WinUI 3 support)
- Windows SDK for XAML tools
### Setup Steps
1. **Open the TopToolbar folder in VSCode**
```bash
code c:\PowerToys\src\modules\TopToolbar\TopToolbar
```
2. **Install recommended extensions** when prompted
3. **Configure launch settings**
- Press `Ctrl+Shift+D` to open Debug view
- Select configuration from the dropdown:
- "Launch TopToolbar (x64 Debug)" - for x64 architecture
- "Launch TopToolbar (ARM64 Debug)" - for ARM64 architecture
- "Attach to TopToolbar Process" - to attach to running process
## Debug Configurations
### Launch TopToolbar (x64 Debug)
- Builds TopToolbar in Debug mode for x64 architecture
- Runs the executable from `bin/x64/Debug/TopToolbar.exe`
- Places breakpoints and inspects variables
- Recommended for initial debugging
### Attach to TopToolbar Process
- Attaches debugger to running TopToolbar process
- Useful for debugging already-running applications
- VSCode shows list of processes to select from
### Launch TopToolbar (ARM64 Debug)
- Builds TopToolbar in Debug mode for ARM64 architecture
- Runs the executable from `bin/ARM64/Debug/TopToolbar.exe`
- For ARM-based Windows devices
## Build Tasks
Available build tasks (press `Ctrl+Shift+B` to run):
### Default Build Task
- **build-debug-x64**: Debug build for x64 platform
### Other Tasks
- **build-debug-arm64**: Debug build for ARM64 platform
- **build-release-x64**: Release build for x64 platform
- **clean**: Clean build artifacts
- **run-debug-x64**: Run debug build for x64
### Running a Task
1. Press `Ctrl+Shift+B` (default build) or
2. Press `Ctrl+Shift+P` and select "Tasks: Run Task"
3. Choose from available tasks
## Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| `F5` | Start Debugging (Launch) |
| `Ctrl+Shift+D` | Open Debug View |
| `Ctrl+Shift+B` | Run Default Build Task |
| `Ctrl+Shift+P` | Command Palette (run other tasks) |
| `F9` | Toggle Breakpoint |
| `F10` | Step Over |
| `F11` | Step Into |
| `Shift+F11` | Step Out |
| `Ctrl+K Ctrl+I` | Show Hover (Debug) |
## Debugging Workflow
### Setting Breakpoints
1. Click in the gutter (left margin) of code editor
2. Red circle appears indicating breakpoint
3. Launch debugger with F5
4. Execution stops at breakpoint
### Inspecting Variables
- Hover over variables to see current values
- Use Debug Console to evaluate expressions
- Watch panel for complex expressions
### Debug Console
- Press `Ctrl+Shift+Y` or click Debug Console tab
- Execute C# expressions
- View debug output and logs
## Troubleshooting
### "Program not found" error
- Ensure you've built the project successfully
- Check if binary exists in `bin/x64/Debug/` or `bin/ARM64/Debug/`
- Run build task before debugging
### "Cannot attach to process" error
- Ensure TopToolbar process is running
- Check permissions (may need Administrator rights)
- Verify process architecture matches (x64 vs ARM64)
### Breakpoints not hit
- Ensure code is built in Debug mode (not Release)
- Check symbols are loaded (Debug Console shows debug info)
- Verify breakpoint is in actual code path
### Extension not found error
- Install C# Dev Kit from Extensions marketplace
- Reload VSCode window
- Verify installation in Extensions view
## Configuration Files
### launch.json
Defines debug launch configurations:
- Program paths for different architectures
- Pre-launch tasks
- Debug console behavior
### tasks.json
Defines build and run tasks:
- Build commands for different configurations
- Clean task
- Run tasks
### settings.json
VSCode settings specific to this workspace:
- C# analyzer settings
- Code formatting options
- OmniSharp configuration
### extensions.json
Recommended extensions for this project:
- C# extension
- .NET runtime tools
- XAML tools
- Git integration
## Additional Resources
- [VSCode Debugging Guide](https://code.visualstudio.com/docs/editor/debugging)
- [C# in VSCode](https://code.visualstudio.com/docs/languages/csharp)
- [Debug C# Applications](https://code.visualstudio.com/docs/csharp/debugging)
- [TopToolbar Documentation](../README.md)
## Notes
- Ensure Windows Defender or antivirus excludes VSCode and dotnet tools
- For WinUI 3 development, Visual Studio 2022 Insiders recommended
- Debug builds are larger; use Release builds for distribution
- Some features may require Administrator privileges

View File

@@ -0,0 +1,232 @@
# Getting Started with VSCode Debug for TopToolbar
## Step-by-Step Setup
### Step 1: Open TopToolbar in VSCode
```bash
# Navigate to TopToolbar folder
cd c:\PowerToys\src\modules\TopToolbar\TopToolbar
# Open in VSCode
code .
```
### Step 2: Install Recommended Extensions
When VSCode opens, you'll see a notification about recommended extensions:
1. Click "Show Recommendations" or go to Extensions (`Ctrl+Shift+X`)
2. Install the following:
- **C# Dev Kit** by Microsoft (ms-dotnettools.csharp)
- **.NET Runtime Install Tool** (ms-dotnettools.vscode-dotnet-runtime)
- *Optional:* GitLens for version control (eamodio.gitlens)
3. Reload VSCode window when prompted
### Step 3: Verify .NET SDK Installation
```powershell
# Check installed .NET versions
dotnet --list-sdks
# Should show .NET 8.0 or later
# Example output:
# 8.0.x [C:\Program Files\dotnet\sdk\8.0.x]
```
If .NET SDK is not installed:
1. Visit https://dotnet.microsoft.com/download
2. Download .NET 8.0 SDK or later
3. Run installer and follow instructions
4. Restart terminal and verify installation
### Step 4: Build TopToolbar
**Option A: Using VSCode UI**
1. Press `Ctrl+Shift+B` (default build task)
2. Select `build-debug-x64`
3. Wait for compilation to complete
**Option B: Using Terminal**
```powershell
# In VSCode Terminal (Ctrl+`)
dotnet build -c Debug -p:Platform=x64 TopToolbar.csproj
```
### Step 5: Start Debugging
1. Press `F5` or go to Debug view (`Ctrl+Shift+D`)
2. Select debug configuration:
- **"Launch TopToolbar (x64 Debug)"** (recommended for first time)
- Press `F5` or click the green play button
3. App should launch with debugger attached
### Step 6: Set a Breakpoint
1. Open a source file (e.g., `TopToolbarWindow.xaml.cs`)
2. Click in the gutter (left margin of line numbers)
3. Red dot indicates breakpoint is set
4. Interact with the application to trigger breakpoint
5. Execution pauses at breakpoint
6. Inspect variables in Debug panel
## Common Debug Tasks
### Debug Single File
```powershell
# Build only
dotnet build -c Debug TopToolbar.csproj
# Or via UI: Ctrl+Shift+B
```
### Debug Specific Architecture
```powershell
# ARM64 debug
dotnet build -c Debug -p:Platform=ARM64 TopToolbar.csproj
# x64 debug
dotnet build -c Debug -p:Platform=x64 TopToolbar.csproj
```
### Attach to Running Process
1. Start TopToolbar normally
2. Press `F5` in VSCode
3. Select "Attach to TopToolbar Process"
4. VSCode displays list of running processes
5. Select TopToolbar process
6. Debugger attaches
### Clean Build
```powershell
# Clear build artifacts
dotnet clean TopToolbar.csproj
# Or via UI: Ctrl+Shift+P → "Tasks: Run Task" → "clean"
```
## Debugging Features
### Breakpoints
- **Toggle**: Click gutter or press `F9`
- **View**: Debug view → Breakpoints panel
- **Conditional**: Right-click breakpoint → Add breakpoint condition
### Watch Variables
1. Debug view → Watch panel
2. Click "+" to add expression
3. Type variable name or expression
4. Watch updates as execution continues
### Call Stack
- Debug view → Call Stack panel
- Shows execution flow
- Click frame to navigate to code
### Debug Console
1. Press `Ctrl+Shift+Y`
2. Evaluate C# expressions
3. View debug output
4. Exception details displayed here
### Step Operations
- **F10**: Step Over (next line)
- **F11**: Step Into (enter function)
- **Shift+F11**: Step Out (exit function)
- **F5**: Continue execution
## Keyboard Shortcuts Reference
| Shortcut | Action |
|----------|--------|
| `F5` | Start/Continue Debug |
| `Shift+F5` | Stop Debug |
| `Ctrl+Shift+D` | Open Debug View |
| `F9` | Toggle Breakpoint |
| `F10` | Step Over |
| `F11` | Step Into |
| `Shift+F11` | Step Out |
| `Ctrl+Shift+B` | Run Build Task |
| `Ctrl+Shift+P` | Command Palette |
| `Ctrl+\`` | Toggle Terminal |
| `Ctrl+Shift+Y` | Debug Console |
## Troubleshooting
### Issue: "Program not found"
**Solution:**
1. Verify build succeeded (check `bin/x64/Debug/` folder)
2. Run clean build: `dotnet clean && dotnet build -c Debug`
3. Check file path in launch.json
### Issue: "Cannot attach to process"
**Solution:**
1. Ensure application is running
2. May need Administrator privileges
3. Verify architecture matches (x64 vs ARM64)
### Issue: "Breakpoint not hit"
**Solution:**
1. Ensure Debug build (not Release)
2. Check symbols are loaded
3. Verify code path is executed
### Issue: Extension not loading
**Solution:**
1. Go to Extensions (`Ctrl+Shift+X`)
2. Search for "C# Dev Kit"
3. Click Install
4. Reload VSCode (`Ctrl+Shift+P` → "Reload Window")
### Issue: Build fails with errors
**Solution:**
1. Check error output in Terminal
2. Verify .NET SDK version: `dotnet --version`
3. Clean build: `dotnet clean`
4. Check TopToolbar.csproj syntax
## Project Structure
```
TopToolbar/
├── .vscode/ # VSCode configuration
│ ├── launch.json # Debug configurations
│ ├── tasks.json # Build tasks
│ ├── settings.json # Editor settings
│ ├── extensions.json # Recommended extensions
│ ├── DEBUG_SETUP.md # Detailed setup guide
│ └── VSCODE_CONFIG_SUMMARY.md # Configuration summary
├── TopToolbar.csproj # Project file
├── TopToolbarXAML/ # XAML UI code
│ └── ToolbarWindow.xaml.cs
├── Services/ # Services layer
├── Models/ # Data models
├── Providers/ # Provider implementations
└── bin/ # Build output
├── x64/Debug/ # x64 debug build
└── ARM64/Debug/ # ARM64 debug build
```
## Next Steps
1. ✅ Set up VSCode with debug configurations
2. ✅ Build TopToolbar successfully
3. ✅ Set breakpoints and debug code
4. 📖 Read DEBUG_SETUP.md for advanced topics
5. 🚀 Start developing with VSCode
## Additional Resources
- [VSCode Debugging Guide](https://code.visualstudio.com/docs/editor/debugging)
- [C# Development in VSCode](https://code.visualstudio.com/docs/csharp/intro-to-cs)
- [.NET Development Guide](https://learn.microsoft.com/en-us/dotnet/)
- [WinUI 3 Documentation](https://learn.microsoft.com/en-us/windows/apps/winui/)
## Support
For issues or questions:
1. Check DEBUG_SETUP.md troubleshooting section
2. Review VSCode documentation
3. Check TopToolbar project README
4. Consult .NET and C# documentation

View File

@@ -0,0 +1,147 @@
# Installing C# Extension for VSCode Debugging
## What You Need
The error "Configured debug type 'coreclr' is not supported" means the C# debugger extension is not installed.
## Installation Options
### Option 1: Quick Install (Recommended)
Click the button in VSCode error dialog:
1. Click "Install coreclr Extension" button
2. Wait for installation to complete
3. VSCode reloads automatically
4. Press F5 again to start debugging
### Option 2: Install via Extensions Menu
1. Press `Ctrl+Shift+X` to open Extensions
2. Search for: `ms-dotnettools.csharp`
3. Click "Install" button
4. Wait for installation (takes 1-2 minutes)
5. Click "Reload Window" when prompted
### Option 3: Install via Command Palette
1. Press `Ctrl+Shift+P`
2. Type: `Extensions: Install Extensions`
3. Press Enter
4. Search for: `C# Dev Kit`
5. Click Install
## What Gets Installed
Installing C# extension provides:
- ✓ coreclr debugger (for .NET debugging)
- ✓ Code completion and IntelliSense
- ✓ Code formatting
- ✓ Syntax highlighting
- ✓ Error detection
- ✓ Test explorer
- ✓ XAML support
## After Installation
1. **Reload Window** (if not auto-reloaded)
- Press `Ctrl+Shift+P`
- Type: `Developer: Reload Window`
- Press Enter
2. **Start Debugging Again**
- Press `F5`
- Select debug configuration
- Application should launch with debugger
3. **Verify Installation**
- Check Extensions view
- Search for "C#"
- Should show "C# Dev Kit" with checkmark
## Full Extension Recommendations
Install these for complete development experience:
| Extension | Command |
|-----------|---------|
| C# Dev Kit | `ms-dotnettools.csharp` |
| .NET Runtime | `ms-dotnettools.vscode-dotnet-runtime` |
| XAML Tools | `ms-dotnettools.vscode-xaml-tools` |
| GitHub Copilot | `GitHub.copilot` |
| GitLens | `eamodio.gitlens` |
## Troubleshooting Installation
### Slow Installation
- Normal for first install (large download)
- Can take 2-5 minutes
- Don't close VSCode during installation
### Installation Failed
- Check internet connection
- Restart VSCode
- Try manual installation:
1. Go to https://marketplace.visualstudio.com
2. Search "C# Dev Kit"
3. Click "Install" button in browser
4. Select "Open with Visual Studio Code"
### Extension Not Working After Install
- Reload Window: `Ctrl+Shift+P` → "Reload Window"
- Restart VSCode completely
- Check extension is enabled in Extensions view
## After Successful Installation
### Now You Can:
- ✓ Launch debug sessions with F5
- ✓ Set breakpoints with F9
- ✓ Inspect variables
- ✓ Step through code
- ✓ Evaluate expressions in debug console
### Next Steps:
1. Press F5 to start debugging
2. Set breakpoint by clicking gutter
3. Application launches with debugger
4. Breakpoint pauses execution
5. Inspect variables in Debug panel
## Getting Help
If installation still doesn't work:
1. **Check VSCode Version**
- Should be latest stable or insiders
- Update from Help menu if needed
2. **Check .NET SDK**
```powershell
dotnet --version
# Should show 8.0 or later
```
3. **Check Firewall**
- VSCode may need internet access for extensions
- Add VSCode to firewall exceptions if needed
4. **Manual Marketplace Link**
- Direct link: https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp
## Quick Reference
| Action | Steps |
|--------|-------|
| Install C# | Ctrl+Shift+X → Search "C#" → Install |
| Reload | Ctrl+Shift+P → "Reload Window" |
| Debug | F5 → Select config |
| Breakpoint | Click line gutter |
| Step | F10 (over) or F11 (into) |
---
**Status**: After C# extension installation, coreclr debugging will be available.
**Time to Install**: 2-5 minutes
**Next**: Press F5 to start debugging TopToolbar

View File

@@ -0,0 +1,149 @@
# VSCode Debug Configuration Summary
## Overview
Successfully added comprehensive VSCode debug support to the TopToolbar project with proper launch configurations, build tasks, and documentation.
## Files Created
### 1. `.vscode/launch.json`
VSCode debug launch configurations including:
- **Launch TopToolbar (x64 Debug)**: Debug x64 build
- **Attach to TopToolbar Process**: Attach to running process
- **Launch TopToolbar (ARM64 Debug)**: Debug ARM64 build
Features:
- Pre-launch task automation
- Console output to internal console
- Process attachment with picker
### 2. `.vscode/tasks.json`
Build and run tasks for development:
**Build Tasks:**
- `build-debug-x64`: Debug build for x64 (default)
- `build-debug-arm64`: Debug build for ARM64
- `build-release-x64`: Release build for x64
**Utility Tasks:**
- `clean`: Clean build artifacts
- `run-debug-x64`: Build and run debug x64
**Features:**
- Problem matcher for error detection
- Default build task configured
- Full dotnet CLI integration
### 3. `.vscode/settings.json`
VSCode workspace settings:
- C# analyzer configuration
- Code formatting on save
- OmniSharp analyzer settings
- EditorConfig support enabled
### 4. `.vscode/extensions.json`
Recommended extensions:
- C# Dev Kit (ms-dotnettools.csharp)
- .NET Runtime (ms-dotnettools.vscode-dotnet-runtime)
- C# Extension (ms-vscode.csharp)
- XAML Tools (ms-dotnettools.vscode-xaml-tools)
- GitHub Copilot (for AI assistance)
- GitLens (for version control)
### 5. `.vscode/DEBUG_SETUP.md`
Comprehensive debug setup guide including:
- Prerequisites and installation
- Quick start instructions
- Configuration descriptions
- Keyboard shortcuts
- Debugging workflow
- Troubleshooting guide
- Additional resources
## Quick Start
1. **Open folder in VSCode:**
```bash
code .
```
2. **Install recommended extensions** (prompted by VSCode)
3. **Start debugging:**
- Press `F5` to launch
- Select configuration from dropdown
4. **Build project:**
- Press `Ctrl+Shift+B` for default build
- Or use Task menu for specific tasks
## Key Features
✅ Multiple debug configurations (x64, ARM64, attach)
✅ Automated build tasks before debugging
✅ Problem matcher for error detection
✅ Clean code formatting settings
✅ Extension recommendations
✅ Comprehensive documentation
✅ Keyboard shortcuts reference
✅ Troubleshooting guide
## Architecture Support
- **x64 Platform**: Native development platform
- **ARM64 Platform**: For ARM-based Windows devices
- **Debug Configuration**: Full symbols and debug info
- **Release Configuration**: Optimized builds
## Usage Patterns
### Simple Debug Session
```
F5 (Launch) → Code runs → Hit breakpoint → Inspect variables
```
### Build Then Debug
```
Ctrl+Shift+B (Build) → F5 (Launch) → Debug session
```
### Attach to Process
```
F5 → Select "Attach" config → Choose process → Debug
```
### Custom Task
```
Ctrl+Shift+P → "Run Task" → Select task → Executes
```
## Troubleshooting Integration
- Problem Matcher: Captures build errors
- Debug Console: Inspect variables and expressions
- Watch Panel: Track specific variables
- Call Stack: View execution flow
- Breakpoints: Line-by-line debugging
## Integration Points
- Integrates with existing TopToolbar.csproj
- Works with current build structure
- Compatible with x64 and ARM64 binaries
- Supports WinUI 3 XAML debugging
## Next Steps
1. Open the TopToolbar folder in VSCode
2. Extensions will be recommended automatically
3. Install recommended C# development tools
4. Press F5 to start debugging
5. See DEBUG_SETUP.md for detailed instructions
## Notes
- All configurations use dotnet CLI
- Requires .NET 8.0 SDK or later
- WinUI 3 support requires Visual Studio 2022 Insiders
- Administrator rights may be needed for debugging
- Debug builds stored in respective bin directories

View File

@@ -0,0 +1,10 @@
{
"recommendations": [
"ms-dotnettools.csharp",
"ms-dotnettools.vscode-dotnet-runtime",
"ms-vscode.csharp",
"ms-dotnettools.vscode-xaml-tools",
"GitHub.copilot",
"eamodio.gitlens"
]
}

View File

@@ -0,0 +1,42 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch TopToolbar (x64 Debug)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-debug-x64",
"program": "${workspaceFolder}/bin/x64/Debug/${config:dotnet.targetFramework}/TopToolbar.exe",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "integratedTerminal",
"requireExactSource": false,
"env": {
"DOTNET_ENVIRONMENT": "Development"
}
},
{
"name": "Attach to TopToolbar Process",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}",
"requireExactSource": false
},
{
"name": "Launch TopToolbar (ARM64 Debug)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-debug-arm64",
"program": "${workspaceFolder}/bin/ARM64/Debug/${config:dotnet.targetFramework}/TopToolbar.exe",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "integratedTerminal",
"requireExactSource": false,
"env": {
"DOTNET_ENVIRONMENT": "Development"
}
}
]
}

View File

@@ -0,0 +1,19 @@
{
"omnisharp": {
"msbuildProperties": {
"Platform": "x64",
"RuntimeIdentifier": "win-x64"
},
"enableRoslynAnalyzers": true,
"analyzersPath": null,
"organizeImportsOnFormat": true,
"enableEditorConfigSupport": true,
"enableRoslynAnalyzers": true,
"showOmnisharpLogOnError": true,
"loggingLevel": "information"
},
"omnisharp.defaultLaunchSolution": "TopToolbar.csproj",
"omnisharp.useGlobalMono": "never",
"omnisharp.useModernNet": true,
"omnisharp.projectLoadTimeout": 120
}

View File

@@ -0,0 +1,24 @@
{
"dotnet.defaultSolutionOrFolder": "${workspaceFolder}",
"dotnet.targetFramework": "net9.0-windows10.0.19041.0",
"[csharp]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.analyzers": "explicit"
}
},
"omnisharp.path": "latest",
"omnisharp.useGlobalMono": "never",
"omnisharp.useModernNet": true,
"omnisharp.enableEditorConfigSupport": true,
"omnisharp.enableRoslynAnalyzers": true,
"omnisharp.analysisLevel": "latest",
"omnisharp.projectLoadTimeout": 120,
"omnisharp.maxProjectResults": 250,
"omnisharp.showOmnisharpLogOnError": true,
"omnisharp.msbuildProperties": {
"Platform": "x64",
"RuntimeIdentifier": "win-x64"
},
"extensions.ignoreRecommendations": false
}

View File

@@ -0,0 +1,76 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build-debug-x64",
"command": "dotnet",
"type": "process",
"args": [
"build",
"-c",
"Debug",
"-p:Platform=x64",
"${workspaceFolder}/TopToolbar.csproj"
],
"problemMatcher": "$msCompile",
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "build-debug-arm64",
"command": "dotnet",
"type": "process",
"args": [
"build",
"-c",
"Debug",
"-p:Platform=ARM64",
"${workspaceFolder}/TopToolbar.csproj"
],
"problemMatcher": "$msCompile"
},
{
"label": "build-release-x64",
"command": "dotnet",
"type": "process",
"args": [
"build",
"-c",
"Release",
"-p:Platform=x64",
"${workspaceFolder}/TopToolbar.csproj"
],
"problemMatcher": "$msCompile"
},
{
"label": "clean",
"command": "dotnet",
"type": "process",
"args": [
"clean",
"${workspaceFolder}/TopToolbar.csproj"
],
"problemMatcher": "$msCompile"
},
{
"label": "run-debug-x64",
"command": "dotnet",
"type": "process",
"args": [
"run",
"-c",
"Debug",
"-p:Platform=x64",
"--project",
"${workspaceFolder}/TopToolbar.csproj"
],
"problemMatcher": "$msCompile",
"group": {
"kind": "test",
"isDefault": true
}
}
]
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
namespace TopToolbar.Actions
{
public sealed class ActionContext
{
public string FocusedApp { get; set; } = string.Empty;
public string Selection { get; set; } = string.Empty;
public string WorkspaceId { get; set; } = string.Empty;
public string Zone { get; set; } = string.Empty;
public IDictionary<string, string> EnvironmentVariables { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public string Locale { get; set; } = string.Empty;
public string NowUtcIso { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace TopToolbar.Actions
{
public sealed class ActionDescriptor
{
public string Id { get; set; } = string.Empty;
public string ProviderId { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Subtitle { get; set; } = string.Empty;
public ActionKind Kind { get; set; } = ActionKind.Command;
public ActionIcon Icon { get; set; } = new ActionIcon();
public string GroupHint { get; set; } = string.Empty;
public double? Order { get; set; }
public IList<string> Keywords { get; } = new List<string>();
public JsonNode ArgsSchema { get; set; }
public JsonNode Preview { get; set; }
public bool? CanExecute { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace TopToolbar.Actions
{
public sealed class ActionIcon
{
public ActionIconType Type { get; set; } = ActionIconType.Glyph;
public string Value { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace TopToolbar.Actions
{
public enum ActionIconType
{
Glyph,
Bitmap,
Emoji,
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace TopToolbar.Actions
{
public enum ActionKind
{
Launch,
Toggle,
Command,
Workflow,
Menu,
View,
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json;
namespace TopToolbar.Actions
{
public sealed class ActionOutput
{
public string Type { get; set; } = "text";
public JsonElement Data { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace TopToolbar.Actions
{
public sealed class ActionProgress
{
public double? Percent { get; set; }
public string Note { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace TopToolbar.Actions
{
public sealed class ActionResult
{
public bool Ok { get; set; }
public string Message { get; set; } = string.Empty;
public ActionOutput Output { get; set; } = new ActionOutput();
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
namespace TopToolbar;
internal static class AppPaths
{
private const string RootFolderName = "TopToolbar";
public static string Root => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), RootFolderName);
public static string Logs => Path.Combine(Root, "Logs");
public static string ConfigFile => Path.Combine(Root, "toolbar.config.json");
public static string ProfilesDirectory => Path.Combine(Root, "Profiles");
public static string ProvidersDirectory => Path.Combine(Root, "Providers");
public static string ConfigDirectory => Path.Combine(Root, "config");
public static string ProviderDefinitionsDirectory => Path.Combine(ConfigDirectory, "providers");
public static string IconsDirectory => Path.Combine(Root, "icons");
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2 L5 14 H11 L9 22 L19 10 H13 L15 2 Z" />
</svg>

After

Width:  |  Height:  |  Size: 143 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect x="4" y="5" width="16" height="15" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2" />
<line x1="4" y1="9" x2="20" y2="9" stroke="currentColor" stroke-width="2" />
<line x1="9" y1="3" x2="9" y2="7" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<line x1="15" y1="3" x2="15" y2="7" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<rect x="8" y="12" width="3" height="3" fill="currentColor" rx="0.5" />
<rect x="13" y="12" width="3" height="3" fill="currentColor" rx="0.5" />
<rect x="8" y="16" width="3" height="3" fill="currentColor" rx="0.5" />
<rect x="13" y="16" width="3" height="3" fill="currentColor" rx="0.5" />
</svg>

After

Width:  |  Height:  |  Size: 759 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2" />
<path d="M8 12 L11 15 L16 9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect x="6" y="5" width="12" height="16" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2" />
<rect x="9" y="3" width="6" height="4" rx="1" ry="1" fill="none" stroke="currentColor" stroke-width="2" />
<line x1="9" y1="11" x2="15" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<line x1="9" y1="15" x2="15" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>

After

Width:  |  Height:  |  Size: 495 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M17 19H8C5.8 19 4 17.2 4 15C4 13.3 5.1 11.8 6.7 11.3C7.2 8.5 9.7 6.4 12.6 6.4C15.6 6.4 18 8.6 18.3 11.4C20 11.7 21.2 13.2 21.2 14.9C21.2 17 19.5 19 17 19Z" />
</svg>

After

Width:  |  Height:  |  Size: 257 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect x="3" y="4" width="18" height="12" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2" />
<rect x="9" y="18" width="6" height="2" rx="1" fill="currentColor" />
<line x1="5" y1="15" x2="19" y2="15" stroke="currentColor" stroke-width="2" />
</svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect x="3" y="3" width="8" height="8" rx="1.5" fill="currentColor" />
<rect x="13" y="3" width="8" height="8" rx="1.5" fill="currentColor" />
<rect x="3" y="13" width="8" height="8" rx="1.5" fill="currentColor" />
<rect x="13" y="13" width="8" height="8" rx="1.5" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 363 B

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