Compare commits

...

162 Commits

Author SHA1 Message Date
Jaylyn Barbee
237d1d5537 Updated UI tests for Light Switch to match new UI 2025-10-07 12:36:39 -04:00
Jaylyn Barbee
7524dc9a79 Reverting changes from swapping name incorrectly from dark mode to light switch 2025-10-02 09:56:24 -04:00
Jaylyn Barbee
7d91089180 Reverting changes from swapping name incorrectly from dark mode to light switch 2025-10-02 09:50:43 -04:00
Jaylyn Barbee
8f691c99d0 another push for the Icon 2025-10-01 14:31:17 -04:00
Jaylyn Barbee
59cdd6e359 Adding icon for Light Switch exe 2025-10-01 14:20:27 -04:00
Jaylyn Barbee
2c8fe39458 Merge branch 'jay/lsv2' of https://github.com/microsoft/PowerToys into jay/lsv2 2025-10-01 14:10:38 -04:00
Jaylyn Barbee
c51e69a8f2 Updating JSON for signing new binaries 2025-10-01 14:06:10 -04:00
Jaylyn Barbee
ef7d154f79 Merge branch 'main' into jay/lsv2 2025-10-01 13:58:54 -04:00
Jaylyn Barbee
551f92ef63 Added Light Switch to the Bug Report Tool 2025-10-01 09:42:07 -04:00
Mike Griese
0b9b91c060 CmdPal/Clipboard History: Ctrl+O to open links (#42115)
Basically #42109, but with tests added, and no duplicated OpenUrl
command.
Closes #42108.

Tests pass. 

Tested with both copy as default and paste as default, and things show
up as expected.
2025-10-01 05:50:53 -05:00
Gordon Lam
fae466887c Add back build cache, which will use the nightly build one (#42106)
<!-- 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
Related to this PR https://github.com/microsoft/PowerToys/pull/41968.
Now, we can enabled back the ci build with the nightly build cache (Just
kick off one today, and it will run periodically daily).
2025-10-01 09:32:13 +08:00
Dustin L. Howett
de53c81d75 build: switch Touchdown to Federated Identity (#42119)
This is required as part of offboarding our non-user service account.
2025-09-30 16:09:56 -05:00
Jaylyn Barbee
0e26f43df9 XAML Formatting 2025-09-30 15:55:16 -04:00
Jaylyn Barbee
329a84942c new defaults, clock mode respects user settings 2025-09-30 15:32:15 -04:00
Jaylyn Barbee
2995659e8a fixed location info warning appearing in incorrect scenarios 2025-09-30 14:32:22 -04:00
Mike Griese
8318a40dd4 CmdPal: Bump version to 0.6 (#42097)
This bumps the CmdPal version to 0.6.

It also moves the template project to consume the 0.5 SDK. 

It also removes the WASDK dependency, because we only need the MSIX
tooling.
2025-09-30 12:55:06 -05:00
ruslanlap
f1f00475d1 Add CheatSheets plugin to third-party Run plugins documentation (#41952)
This PR adds the CheatSheets plugin to the third-party plugins
documentation in the General plugins section.

## CheatSheets Plugin
📚 CheatSheets for PowerToys Run - Find cheat sheets and command examples
instantly always at your fingertips with PowerToys Run plugin

![CheatSheets
Demo](https://github.com/ruslanlap/PowerToysRun-CheatSheets/blob/master/assets/demo-cheatsheets.gif)

This plugin enables users to instantly find cheat sheets and command
examples for various tools and programming languages without leaving
PowerToys Run.

### Features
- 🔍 Instant Search - Find commands and cheat sheets with fuzzy matching
- 📚 Multiple Sources - Integrates with tldr, cheat.sh, and offline cheat
sheets
-  Favorites System - Save and quickly access your most-used commands
- 📂 Categories - Browse commands by tool/language (git, docker, python,
etc.)
- 📊 Usage History - Tracks popular commands for quick access
- 💾 Smart Caching - Fast offline access with configurable cache duration
- 🎨 Modern UI - Beautiful WPF interface with theme adaptation
- 🔧 Offline Mode - Works without internet connection using cached data

## Link to plugin
- https://github.com/ruslanlap/PowerToysRun-CheatSheets
2025-09-30 12:53:22 -05:00
Jiří Polášek
0d3db48ab1 CmdPal: Properly quote arguments when rebuilding normalized path (#42071)
## Summary of the Pull Request

This PR ensures proper quoting of arguments after normalization. When
joining arguments back into a single string, any argument containing
whitespace or double quotes must be quoted (because parsing unquoted
them). Adjusts unit tests to reflect the correct expected results.

Ref: 42016

<!-- 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-30 11:03:52 -05:00
Jiří Polášek
c8486087d8 CmdPal: Update visual style of details panel elements (#42102)
## Summary of the Pull Request

This PR updates the details panel formatting:

- Hides the empty text block used as a separator when the key is empty.
- Makes separators more subtle by adjusting the brush.  
- Reverses the typographical hierarchy of detail key/value items, making
the value dominant and the key more subtle to help users focus on the
content.
- Defines new detail text styles derived from the base WinUI
typographical styles.


| Before | After |
|--------|-------|
| <img width="711" height="1795" alt="image"
src="https://github.com/user-attachments/assets/9155ec88-639a-44c1-a70d-edcd4107945e"
/> | <img width="743" height="1667" alt="image"
src="https://github.com/user-attachments/assets/9d1dc432-82da-4183-b347-74a2f3b96c53"
/> |


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

- [x] Closes: #42099 
- [x] Closes: #41664
- [ ] **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-30 09:06:48 -05:00
Jaylyn Barbee
544aca6273 Sunrise/Sunset offset were separated in the front end but not in the service. 2025-09-30 09:10:30 -04: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
Jaylyn Barbee
4cc8d05b7d Merge branch 'main' into jay/lsv2 2025-09-29 09:15:20 -04: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
Gordon Lam (SH)
8bb96d1ea0 Fix project configuration 2025-09-29 16:40:31 +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
Niels Laute
af8bc74d5d Update PowerToys.admx 2025-09-28 11:47:39 +02:00
Niels Laute
da250b2264 Exclude from run plugin 2025-09-28 11:45:35 +02:00
Niels Laute
0bcbd07f5c Fix settingscard crash 2025-09-28 11:43:55 +02:00
Niels Laute
ca65453ec5 Update LightSwitchPage.xaml 2025-09-27 17:12:58 +02:00
Niels Laute
831b4b23c8 Adding OOBE page + small fixes 2025-09-27 17:07:50 +02:00
Niels Laute
eed4033ade Updated description 2025-09-27 16:15:24 +02:00
Niels Laute
0cb9b35dfa Add GPO control instead of infobar 2025-09-27 16:12:06 +02:00
Niels Laute
14efadcab7 Merge branch 'main' into jay/lsv2 2025-09-27 16:09:12 +02:00
Niels Laute
a5901e02a2 Localization 2025-09-27 16:09:04 +02:00
Niels Laute
c432f3741f Fix spellcheck 2025-09-27 15:54:51 +02:00
Niels Laute
3fbf10a946 Merge branch 'jay/lsv2' of https://github.com/microsoft/PowerToys into jay/lsv2 2025-09-26 19:52:09 +02:00
Niels Laute
f6d7c5aefe Adding logic to hide the chart if location has not been set 2025-09-26 19:52:03 +02:00
Jaylyn Barbee
f2352bcedf Dev docs for Light Switch 2025-09-26 11:34:19 -04:00
Jaylyn Barbee
52f44c5693 updating packaging info for ui test 2025-09-26 09:42:08 -04:00
Jaylyn Barbee
23da7de971 Reverting change to DarkMode string 2025-09-25 11:39:38 -04:00
Jaylyn Barbee
03760fa182 Removing typos 2025-09-24 15:28:08 -04:00
Jaylyn Barbee
0f0285c364 Fix time switching btwn modes 2025-09-23 16:37:21 -04:00
Niels Laute
107967aa45 Update LightSwitchPage.xaml 2025-09-23 20:41:12 +02:00
Niels Laute
9283f99a31 XAML formatting + string tweaks 2025-09-23 20:36:49 +02:00
Niels Laute
8073be34b6 Merge branch 'jay/lsv2' of https://github.com/microsoft/PowerToys into jay/lsv2 2025-09-23 20:31:52 +02:00
Niels Laute
d13216eb01 Removing location lookup 2025-09-23 20:30:45 +02:00
Jaylyn Barbee
54887c7aa1 Some updates to settings ui 2025-09-23 13:22:24 -04:00
Jaylyn Barbee
131b8c6743 working on some known bugs 2025-09-23 11:55:06 -04:00
Jaylyn Barbee
77aa112aca Removed all logic for a debug window 2025-09-23 11:46:57 -04:00
Jaylyn Barbee
eb41935c50 Removed debug window 2025-09-23 09:51:52 -04:00
Jaylyn Barbee
6fee7eb55f Removed entry from Wix3 setup 2025-09-23 08:29:25 -04:00
Jaylyn Barbee
e5ddd39d0f Directory update 2025-09-22 20:33:27 -04:00
Jaylyn Barbee
0fb9333c69 added missing reference 2025-09-22 20:29:07 -04:00
Jaylyn Barbee
d6c6395b80 Fixing build errors, updating links 2025-09-22 17:54:03 -04:00
Jaylyn Barbee
e894982069 ComponentRef added 2025-09-22 17:01:15 -04:00
Jaylyn Barbee
8d8ed80fdb Fix to Light Switch interface 2025-09-22 15:24:08 -04:00
Jaylyn Barbee
02e87f8e78 Installer updates 2025-09-22 14:55:31 -04:00
Jaylyn Barbee
c27b0fff9c XAML formatting 2025-09-22 08:24:39 -04:00
Jaylyn Barbee
b585fa4534 more installer updates 2025-09-12 13:33:21 -04:00
Jaylyn Barbee
467b66d313 wix updates for light switch 2025-09-12 13:30:40 -04:00
Jaylyn Barbee
94be1f2be7 Adding ui tests (#41785) 2025-09-12 11:49:40 -04:00
Jaylyn Barbee
e9bcce7ec5 added installer files for Light Switch 2025-09-11 10:45:28 -04:00
Jaylyn Barbee
b0373362ee Adding extra logs 2025-09-09 12:29:08 -04:00
Jaylyn Barbee
f0cc35c1c1 Merge branch 'jay/lsv2' of https://github.com/microsoft/PowerToys into jay/lsv2 2025-09-09 11:24:14 -04:00
Jaylyn Barbee
9a44c6e4ee Working on setting the correct output paths 2025-09-09 10:28:50 -04:00
Niels Laute
ae00b77f8a Update LightSwitchPage.xaml 2025-09-09 09:53:51 +02:00
Niels Laute
6bfb11eafb Search index shouldn't be part of this PR 2025-09-09 09:48:11 +02:00
Jaylyn Barbee
e65d8fc2ed ensure light switch service exe is output in release mode 2025-09-08 23:30:14 -04:00
Jaylyn Barbee
c1510f2075 Fixed manual pickers not updating bug 2025-09-08 21:18:03 -04:00
Jaylyn Barbee
7de9686a38 fixed malformed entry in solution 2025-09-08 16:51:59 -04:00
Jaylyn Barbee
b6406eaa7e fixed xaml styling 2025-09-08 15:58:09 -04:00
Jaylyn Barbee
ad4ef1a8c3 separated the offsets 2025-09-08 15:34:13 -04:00
Jaylyn Barbee
f7139b88b2 changed from csv to static city list 2025-09-08 15:20:35 -04:00
Niels Laute
d34ba803be Merge branch 'main' into jay/lsv2 2025-09-08 20:16:27 +02:00
Jaylyn Barbee
a3a8c90df9 release mode not displaying cities list, fixed 2025-09-08 12:38:51 -04:00
Jaylyn Barbee
c8d9a8ccb6 file rearranging, should fix build issue 2025-09-08 10:50:38 -04:00
Jaylyn Barbee
a5dffc6ee3 Merge branch 'jay/lsv2' of https://github.com/microsoft/PowerToys into jay/lsv2 2025-09-08 09:46:00 -04:00
Jaylyn Barbee
2ebe234dca fixed release build 2025-09-08 09:45:48 -04:00
Niels Laute
c68bafcd1f Update LightSwitchPage.xaml 2025-09-08 15:24:18 +02:00
Jaylyn Barbee
c4a4c1d659 Removed merege artifcat 2025-09-05 13:07:56 -04:00
Jaylyn Barbee
f83e5ff218 removed uneeded import 2025-09-05 13:01:05 -04:00
Jaylyn Barbee
0462089c62 xaml styling 2025-09-05 12:39:20 -04:00
Jaylyn Barbee
af83f7d3ad Offset reflects to the timeline 2025-09-05 11:59:15 -04:00
Niels Laute
d10ecd8a13 Hiding panel 2025-09-05 15:10:46 +02:00
Niels Laute
e7ecaafdc1 UX tweaks to the location dialog 2025-09-05 14:57:10 +02:00
Jaylyn Barbee
432120f56b Updating suntimes at midnight 2025-09-04 16:03:21 -04:00
Jaylyn Barbee
88f66169cc Fixed timeline display 2025-09-04 12:36:14 -04:00
Jaylyn Barbee
1c4503753e making sure values update when they need to 2025-09-04 12:30:46 -04:00
Jaylyn Barbee
0810db222d merged the two sunset to sunrise mode, displays best information available (city name or just lat/long) 2025-09-04 12:18:37 -04:00
Niels Laute
1dac59c9c3 Merging dialog UX changes 2025-09-04 14:59:26 +02:00
Jaylyn Barbee
2c434d33f2 added auto suggest text box instead of drop down 2025-09-03 16:41:06 -04:00
Jaylyn Barbee
083ff01f47 fixed force mode isse 2025-09-03 15:47:16 -04:00
Jaylyn Barbee
b958f546b9 only running service when changes need to be made 2025-09-03 15:00:29 -04:00
Jaylyn Barbee
69b624991f small fixes to ui, mode needs to persist in content dialog 2025-09-03 13:05:31 -04:00
Jaylyn Barbee
d4b379d90a removed unneeded comments 2025-09-02 11:43:45 -04:00
Jaylyn Barbee
6c031d3b47 removed unneeded comments 2025-09-02 11:40:19 -04:00
Jaylyn Barbee
bccaaad247 small fixes to settings page 2025-09-02 11:25:36 -04:00
Jaylyn Barbee
9c4a713791 merging niels most recent changes 2025-09-02 10:49:21 -04:00
Jaylyn Barbee
1be874eade force mode respects checkboxes 2025-08-29 16:12:26 -04:00
Jaylyn Barbee
565ffb9084 small string fix 2025-08-29 16:03:48 -04:00
Jaylyn Barbee
afd32d0196 fixed toggle mode shortcut 2025-08-29 15:59:52 -04:00
Jaylyn Barbee
e80f38a302 more text fixes 2025-08-29 09:45:22 -04:00
Jaylyn Barbee
ee2dffc6a4 small text fix 2025-08-29 09:43:24 -04:00
Jaylyn Barbee
fee2d0ab81 small text updates 2025-08-29 09:43:00 -04:00
Niels Laute
d403d5a7fb Adding (temp) icon 2025-08-29 07:38:06 +02:00
Niels Laute
289f47c1ea UX tweaks 2025-08-29 07:33:33 +02:00
Jaylyn Barbee
6bbfdd9a93 all things merged and well 2025-08-28 17:23:02 -04:00
Jaylyn Barbee
47102583af Merge branch 'jay/DarkModeModule' of https://github.com/microsoft/PowerToys into jay/DarkModeModule 2025-08-28 17:00:42 -04:00
Jaylyn Barbee
52ff8bea34 merging renaming 2025-08-28 17:00:39 -04:00
Niels Laute
a3441eecc6 Rename pages 2025-08-28 23:00:14 +02:00
Jaylyn Barbee
88cf1ecaec Changed name from dark mode to light swith 2025-08-28 16:32:22 -04:00
Niels Laute
a1b09180d8 Moving location stuff to the dialog 2025-08-28 22:24:45 +02:00
Niels Laute
6907f26243 Small tweaks + adding chart 2025-08-28 19:41:11 +02:00
Jaylyn Barbee
911a4e1009 some clean up around forcing modeS 2025-08-27 15:41:18 -04:00
Jaylyn Barbee
44be38e9b6 shortcuts working 2025-08-27 15:25:54 -04:00
Jaylyn Barbee
00d15ba780 3rd mode added, user selected city location 2025-08-27 14:15:19 -04:00
Jaylyn Barbee
b1b1791489 offset setting added 2025-08-27 13:02:01 -04:00
Jaylyn Barbee
05ae7129aa moved some files around 2025-08-26 11:59:39 -04:00
Jaylyn Barbee
4b015605a1 working on mode switching, some bugs right now 2025-08-21 14:15:41 -04:00
Jaylyn Barbee
fa29bebec3 using location services to calculate sun rise and sunset times is working 2025-08-21 13:53:26 -04:00
Jaylyn Barbee
9816d6fc05 Small comment 2025-08-21 11:47:18 -04:00
Jaylyn Barbee
323ddfdf55 stale catchup from sleep/lock at next minute + wrap around scale rather than forcing light mode to be first 2025-08-21 11:35:01 -04:00
Jaylyn Barbee
ae6187101f stale catchup for if you were shutdown/not running the service 2025-08-21 11:15:53 -04:00
Jaylyn Barbee
be105b5e27 Fixed flashing issue, legacy code got stuck when removing old logic 2025-08-21 10:56:30 -04:00
Jaylyn Barbee
8aedc4a61d Force mode buttons working using custom actions 2025-08-21 10:43:35 -04:00
Jaylyn Barbee
9c82281bb1 gpo fixes, tested and works! 2025-08-20 14:10:17 -04:00
Jaylyn Barbee
df74f6e3c7 added GPO 2025-08-20 10:20:34 -04:00
Jaylyn Barbee
887e552d43 merged main + ui changes + clean up in logic 2025-08-19 12:09:18 -04:00
Jaylyn Barbee
df61b2863e changed to date time pickers and separated the apps and system theme change 2025-08-19 11:37:31 -04:00
Jaylyn Barbee
9f581101a8 settings are persisting 2025-08-19 11:21:48 -04:00
Jaylyn Barbee
db41f61010 settings saving correctly 2025-08-19 11:03:10 -04:00
Niels Laute
e28e4582c9 Merge branch 'main' into jay/DarkModeModule 2025-08-19 16:43:51 +02:00
Niels Laute
abc7f3f3fb UI tweaks in Settings 2025-08-19 16:40:41 +02:00
Jaylyn Barbee
039991bc4a wiring in settings to load into service 2025-08-19 10:19:37 -04:00
Jaylyn Barbee
a857cc688b removing the ui 2025-08-19 09:23:51 -04:00
Jaylyn Barbee
1be5e5931a in nuget package trouble, settings are saving but not loading properly 2025-08-18 17:04:59 -04:00
Jaylyn Barbee
03fafa747f settings saving appropriately. 2025-08-14 17:07:57 -04:00
Jaylyn Barbee
62b4075349 service works, changes settings based on the time it has in the settings, but settings ui not connected to settings 2025-08-11 14:37:27 -04:00
Jaylyn Barbee
cc42876c01 service is enabling and disabling via ui, still one error at launch 2025-08-11 11:15:54 -04:00
Jaylyn Barbee
2d30fe2ec2 moved logic to service, incomplete right now though 2025-08-08 12:32:29 -04:00
Jaylyn Barbee
247cc47491 got logging figured out 2025-08-07 18:02:28 -04:00
Jaylyn Barbee
7de506010e trying to figured out build error 2025-08-07 15:53:48 -04:00
Jaylyn Barbee
736a04f65c small updates 2025-08-06 11:59:42 -04:00
Jaylyn Barbee
b66b44cc49 showing in runner now. settings still not connected, service still not running. 2025-08-06 10:47:16 -04:00
Jaylyn Barbee
f55f465c83 forward progress, trying to figure out runner 2025-08-05 14:20:12 -04:00
Jaylyn Barbee
30e6215003 repair work to the module interface 2025-08-05 13:55:29 -04:00
Jaylyn Barbee
bdafb0e38a trying to get it to show in runner but imports are busted 2025-08-05 12:21:14 -04:00
Jaylyn Barbee
50c5d577bc something broken with converters flipping to show settings 2025-08-04 16:41:35 -04:00
Jaylyn Barbee
a9e838ae1d Starting to fill in settings page 2025-08-04 15:26:32 -04:00
Jaylyn Barbee
c47ff9cd55 interface done? 2025-08-04 12:23:44 -04:00
Jaylyn Barbee
104d4fd6a0 Working on interface 2025-08-04 12:16:55 -04:00
Jaylyn Barbee
37242fbb4d commit base interface 2025-08-04 11:28:39 -04:00
Jaylyn Barbee
2c39113914 Fixing build errors 2025-08-04 11:23:10 -04:00
Jaylyn Barbee
bf07c11640 Linked to project 2025-08-04 11:03:15 -04:00
Jaylyn Barbee
2c6a8bac27 Dark mode code dump 2025-08-04 10:54:36 -04:00
176 changed files with 8312 additions and 852 deletions

View File

@@ -210,6 +210,7 @@ capturevideosample
cmdow
Controlz
cortana
devhints
dlnilsson
fancymouse
firefox
@@ -229,6 +230,7 @@ regedit
roslyn
Skia
Spotify
tldr
Vanara
wangyi
WEX

View File

@@ -3,6 +3,7 @@ abcdefghjkmnpqrstuvxyz
abgr
ABlocked
ABOUTBOX
ABORTIFHUNG
Abug
Acceleratorkeys
ACCEPTFILES
@@ -77,6 +78,7 @@ appwiz
appxpackage
APSTUDIO
AQS
Aquadrant
ARandom
ARCHITEW
ARemapped
@@ -454,6 +456,7 @@ encryptor
ENDSESSION
ENSUREVISIBLE
ENTERSIZEMOVE
ENTRYW
ENU
environmentvariables
EOAC
@@ -571,6 +574,7 @@ GETDESKWALLPAPER
GETDLGCODE
GETDPISCALEDSIZE
getfilesiginforedist
geolocator
GETHOTKEY
GETICON
GETMINMAXINFO
@@ -826,6 +830,7 @@ lastcodeanalysissucceeded
LASTEXITCODE
LAYOUTRTL
LCh
lbl
lcid
LCIDTo
lcl
@@ -844,6 +849,7 @@ LIBID
LIMITSIZE
LIMITTEXT
lindex
lightswitch
linkid
LINKOVERLAY
LINQTo
@@ -867,10 +873,13 @@ logon
LOGMSG
LOGPIXELSX
LOGPIXELSY
lng
LOn
lon
longdate
LONGNAMES
lowlevel
lquadrant
LOWORD
lparam
LPBITMAPINFOHEADER
@@ -1383,6 +1392,7 @@ quickaccent
QUNS
RAII
RAlt
RAquadrant
randi
rasterization
Rasterize
@@ -1681,6 +1691,7 @@ Subdomain
SUBMODULEUPDATE
subresource
Superbar
suntimes
sut
svchost
SVGIn
@@ -1746,6 +1757,7 @@ tgz
themeresources
THH
THICKFRAME
THEMECHANGED
THISCOMPONENT
throughs
thumbnailhotkey
@@ -1762,6 +1774,7 @@ tkconverters
tlb
tlbimp
tlc
tmain
TNP
TOGGLEEASYMOUSE
Toolhelp

View File

@@ -133,6 +133,9 @@
"PowerToys.ImageResizerContextMenu.dll",
"ImageResizerContextMenuPackage.msix",
"PowerToys.LightSwitchModuleInterface.dll",
"LightSwitchService\\PowerToys.LightSwitchService.exe",
"PowerToys.KeyboardManager.dll",
"KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe",
"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe",
@@ -327,6 +330,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

@@ -29,8 +29,8 @@ steps:
displayName: 'Touchdown Build - 37400, PRODEXT'
inputs:
teamId: 37400
TDBuildServiceConnection: $(TouchdownServiceConnection)
authType: SubjectNameIssuer
FederatedIdentityTDBuildServiceConnection: $(TouchdownServiceConnection)
authType: FederatedIdentityTDBuild
resourceFilePath: |
src\**\Resources.resx
src\**\Resource.resx

View File

@@ -32,7 +32,7 @@ parameters:
- name: enableMsBuildCaching
type: boolean
displayName: "Enable MSBuild Caching"
default: false
default: true
- name: runTests
type: boolean
displayName: "Run Tests"

View File

@@ -8,8 +8,8 @@ steps:
displayName: 'Download Localization Files -- PowerToys 37400'
inputs:
teamId: 37400
TDBuildServiceConnection: $(TouchdownServiceConnection)
authType: SubjectNameIssuer
FederatedIdentityTDBuildServiceConnection: $(TouchdownServiceConnection)
authType: FederatedIdentityTDBuild
resourceFilePath: |
**\Resources.resx
**\Resource.resx

View File

@@ -108,7 +108,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

@@ -5,11 +5,13 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "runner", "src\runner\runner.vcxproj", "{9412D5C6-2CF2-4FC2-A601-B55508EA9B27}"
ProjectSection(ProjectDependencies) = postProject
{031AC72E-FA28-4AB7-B690-6F7B9C28AA73} = {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}
{08E71C67-6A7E-4CA1-B04E-2FB336410BAC} = {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}
{0B43679E-EDFA-4DA0-AD30-F4628B308B1B} = {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}
{0B593A6C-4143-4337-860E-DB5710FB87DB} = {0B593A6C-4143-4337-860E-DB5710FB87DB}
{17DA04DF-E393-4397-9CF0-84DABE11032E} = {17DA04DF-E393-4397-9CF0-84DABE11032E}
{217DF501-135C-4E38-BFC8-99D4821032EA} = {217DF501-135C-4E38-BFC8-99D4821032EA}
{2BE46397-4DFA-414C-9BD4-41E4BBF8CB34} = {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}
{38177D56-6AD1-4ADF-88C9-2843A7932166} = {38177D56-6AD1-4ADF-88C9-2843A7932166}
{48804216-2A0E-4168-A6D8-9CD068D14227} = {48804216-2A0E-4168-A6D8-9CD068D14227}
{51920F1F-C28C-4ADF-8660-4238766796C2} = {51920F1F-C28C-4ADF-8660-4238766796C2}
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}
@@ -793,6 +795,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Window
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LightSwitch", "LightSwitch", "{5B201255-53C8-490B-A34F-01F05D48A477}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LightSwitchModuleInterface", "src\modules\LightSwitch\LightSwitchModuleInterface\LightSwitchModuleInterface.vcxproj", "{38177D56-6AD1-4ADF-88C9-2843A7932166}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LightSwitchService", "src\modules\LightSwitch\LightSwitchService\LightSwitchService.vcxproj", "{08E71C67-6A7E-4CA1-B04E-2FB336410BAC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1-76DF-42AC-985C-164CC2EE57A1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}"
@@ -811,6 +819,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3DCCD936-D085-4869-A1DE-CA6A64152C94}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\modules\LightSwitch\Tests\LightSwitch.UITests\LightSwitch.UITests.csproj", "{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2889,6 +2903,22 @@ Global
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64
{38177D56-6AD1-4ADF-88C9-2843A7932166}.Debug|ARM64.ActiveCfg = Debug|ARM64
{38177D56-6AD1-4ADF-88C9-2843A7932166}.Debug|ARM64.Build.0 = Debug|ARM64
{38177D56-6AD1-4ADF-88C9-2843A7932166}.Debug|x64.ActiveCfg = Debug|x64
{38177D56-6AD1-4ADF-88C9-2843A7932166}.Debug|x64.Build.0 = Debug|x64
{38177D56-6AD1-4ADF-88C9-2843A7932166}.Release|ARM64.ActiveCfg = Release|ARM64
{38177D56-6AD1-4ADF-88C9-2843A7932166}.Release|ARM64.Build.0 = Release|ARM64
{38177D56-6AD1-4ADF-88C9-2843A7932166}.Release|x64.ActiveCfg = Release|x64
{38177D56-6AD1-4ADF-88C9-2843A7932166}.Release|x64.Build.0 = Release|x64
{08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Debug|ARM64.ActiveCfg = Debug|ARM64
{08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Debug|ARM64.Build.0 = Debug|ARM64
{08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Debug|x64.ActiveCfg = Debug|x64
{08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Debug|x64.Build.0 = Debug|x64
{08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Release|ARM64.ActiveCfg = Release|ARM64
{08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Release|ARM64.Build.0 = Release|ARM64
{08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Release|x64.ActiveCfg = Release|x64
{08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Release|x64.Build.0 = Release|x64
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.ActiveCfg = Debug|ARM64
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.Build.0 = Debug|ARM64
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|x64.ActiveCfg = Debug|x64
@@ -2945,6 +2975,26 @@ Global
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.ActiveCfg = Debug|ARM64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.Build.0 = Debug|ARM64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.Deploy.0 = Debug|ARM64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|x64.ActiveCfg = Debug|x64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|x64.Build.0 = Debug|x64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|x64.Deploy.0 = Debug|x64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|ARM64.ActiveCfg = Release|ARM64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|ARM64.Build.0 = Release|ARM64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|ARM64.Deploy.0 = Release|ARM64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|x64.ActiveCfg = Release|x64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|x64.Build.0 = Release|x64
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|x64.Deploy.0 = Release|x64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|ARM64.ActiveCfg = Debug|ARM64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|ARM64.Build.0 = Debug|ARM64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|x64.ActiveCfg = Debug|x64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|x64.Build.0 = Debug|x64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.ActiveCfg = Release|ARM64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3258,6 +3308,9 @@ Global
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{5B201255-53C8-490B-A34F-01F05D48A477} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{38177D56-6AD1-4ADF-88C9-2843A7932166} = {5B201255-53C8-490B-A34F-01F05D48A477}
{08E71C67-6A7E-4CA1-B04E-2FB336410BAC} = {5B201255-53C8-490B-A34F-01F05D48A477}
{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}
@@ -3267,6 +3320,9 @@ Global
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477}
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94}
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -0,0 +1,107 @@
# Light Switch
[Public Overview Microsoft Learn](https://learn.microsoft.com/en-us/windows/powertoys/light-switch)
## Quick Links
* [All Issues](https://github.com/microsoft/PowerToys/issues?q=is%3Aissue%20state%3Aopen%20label%3AProduct-LightSwitch)
* [Bugs](https://github.com/microsoft/PowerToys/issues?q=is%3Aissue%20state%3Aopen%20label%3AProduct-LightSwitch%20label%3AIssue-Bug)
* [Pull Requests](https://github.com/microsoft/PowerToys/pulls?q=is%3Apr+is%3Aopen+label%3AProduct-LightSwitch)
## Overview
The **Light Switch** module lets users automatically transition between light and dark mode using a timed schedule or a keyboard shortcut.
## Features
* Set custom times to start and stop dark mode.
* Use geolocation to determine local sunrise and sunset times.
* Apply offsets in sunrise mode (e.g., 15 minutes before sunset).
* Quickly toggle between modes with a keyboard shortcut (`Ctrl+Shift+Win+D` by default).
* Choose whether theme changes apply to:
* Apps only
* System only
* Both apps and system
## Architecture
### Main Components
* **Shortcut/Hotkey**
Listens for a hotkey event. Calling `onHotkey()` flips the theme flags.
> **Note:** Using the shortcut overrides the current schedule until the next transition event.
* **LightSwitchService**
Reads settings and applies theming. Runs a check every minute to ensure the state is correct.
* **SettingsXAML/LightSwitch**
Provides the settings UI for configuring schedules, syncing location, and customizing shortcuts.
* **Settings.UI/ViewModels/LightSwitchViewModel.cs**
Handles updates to the settings file and communicates changes to the front end.
* **modules/LightSwitch/Tests**
Contains UI tests that verify interactions between the settings UI, system state, and `settings.json`.
### Data Flow
1. User configures settings in the UI (default: manual mode, light mode from 06:0018:00).
2. Every minute, the service checks the time.
* If its not a threshold, the service sleeps until the next minute.
* If it matches a threshold, the service applies the theme based on settings and returns to sleep.
3. At **midnight**, when in *Sunrise to Sunset* mode, the service updates daily sunrise and sunset times.
4. If the machine was asleep during a scheduled event, the service applies the correct settings at the next check.
## User Interface
The modules settings are exposed in the PowerToys Settings UI. Options include:
* Shortcut customization
* Mode selection (Manual or Sunrise to Sunset)
* Manual start/stop times (manual mode only)
* Automatic sunrise/sunset calculation (location-based)
* Time offsets (sunrise mode)
* Target scope (system, apps, or both)
## Development Environment Setup
### Prerequisites
* Visual Studio 2019 or later
* Windows 10 SDK
* PowerToys repository cloned from GitHub
### Building and Testing
1. Clone the repo:
```sh
git clone https://github.com/microsoft/PowerToys.git
```
2. Initialize submodules:
```sh
git submodule update --init --recursive
```
3. Build the solution:
```sh
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln
```
> Note: This may take some time.
4. Set `runner` as the startup project and press **F5**.
5. Enable Light Switch in PowerToys Settings.
6. To debug the service:
* Press `Ctrl+Alt+P` or go to **Debug > Attach to Process**.
* Select `LightSwitchService.exe` and click **Attach**.
* You can now set breakpoints in the service files.
7. To debug the Settings UI:
* Set the startup project to `PowerToys.Settings` and press **F5**.
* Note: Light Switch settings will not persist in this mode (they depend on the service executable).
* Alternatively, you can attach `PowerToys.Settings.exe` to the debugger while `runner` is running to test the full flow with breakpoints.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -50,6 +50,7 @@ Contact the developers of a plugin directly for assistance with a specific plugi
| [Hotkeys](https://github.com/ruslanlap/PowerToysRun-Hotkeys) | [ruslanlap](https://github.com/ruslanlap) | Create, manage, and trigger custom keyboard shortcuts directly from PowerToys Run. |
| [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. |
| [Open With Cursor](https://github.com/VictorNoxx/PowerToys-Run-Cursor/) | [VictorNoxx](https://github.com/VictorNoxx) | Open Visual Studio, VS Code recents with Cursor AI |
| [CheatSheets](https://github.com/ruslanlap/PowerToysRun-CheatSheets) | [ruslanlap](https://github.com/ruslanlap) | 📚 Find cheat sheets and command examples instantly from tldr pages, cheat.sh, and devhints.io. Features include favorites system, categories, offline mode, and smart caching. |
## Extending software plugins

View File

@@ -1283,7 +1283,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
}
processes.resize(bytes / sizeof(processes[0]));
std::array<std::wstring_view, 41> processesToTerminate = {
std::array<std::wstring_view, 42> processesToTerminate = {
L"PowerToys.PowerLauncher.exe",
L"PowerToys.Settings.exe",
L"PowerToys.AdvancedPaste.exe",
@@ -1298,6 +1298,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.Hosts.exe",
L"PowerToys.PowerRename.exe",
L"PowerToys.ImageResizer.exe",
L"PowerToys.LightSwitchService.exe",
L"PowerToys.GcodeThumbnailProvider.exe",
L"PowerToys.BgcodeThumbnailProvider.exe",
L"PowerToys.PdfThumbnailProvider.exe",

View File

@@ -65,6 +65,7 @@
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\Hosts.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\Hosts.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\ImageResizer.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\ImageResizer.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\KeyboardManager.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\KeyboardManager.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\LightSwitch.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\LightSwitch.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\MouseWithoutBorders.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\MouseWithoutBorders.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\NewPlus.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\NewPlus.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\Peek.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\Peek.wxs.bk""""

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<?include $(sys.CURRENTDIR)\Common.wxi?>
<?define LightSwitchFiles=?>
<?define LightSwitchFilesPath=$(var.BinDir)\LightSwitchService\?>
<Fragment>
<!-- Light Switch background service -->
<!-- Create a directory for the service binaries -->
<DirectoryRef Id="INSTALLFOLDER">
<Directory Id="LightSwitchServiceFolder" Name="LightSwitchService" />
</DirectoryRef>
<!-- File components generated by generateAllFileComponents.ps1 -->
<DirectoryRef Id="LightSwitchServiceFolder" FileSource="$(var.LightSwitchFilesPath)">
<!-- Generated by generateFileComponents.ps1 -->
<!--LightSwitchFiles_Component_Def-->
</DirectoryRef>
<!-- Group to include the service + cleanup on uninstall -->
<ComponentGroup Id="LightSwitchComponentGroup">
<!-- Ensures folder removal on uninstall -->
<Component Id="RemoveLightSwitchServiceFolder" Guid="C1E2F2ED-34A2-4EB0-8E17-DC0535F50F9D" Directory="INSTALLFOLDER">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemoveLightSwitchServiceFolder" Value="" KeyPath="yes" />
</RegistryKey>
<RemoveFolder Id="RemoveFolderLightSwitchServiceFolder" Directory="LightSwitchServiceFolder" On="uninstall" />
</Component>
</ComponentGroup>
</Fragment>
</Wix>

View File

@@ -41,6 +41,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
call move /Y ..\..\..\FileExplorerPreview.wxs.bk ..\..\..\FileExplorerPreview.wxs
call move /Y ..\..\..\FileLocksmith.wxs.bk ..\..\..\FileLocksmith.wxs
call move /Y ..\..\..\Hosts.wxs.bk ..\..\..\Hosts.wxs
call move /Y ..\..\..\LightSwitch.wxs.bk ..\..\..\LightSwitch.wxs
call move /Y ..\..\..\ImageResizer.wxs.bk ..\..\..\ImageResizer.wxs
call move /Y ..\..\..\KeyboardManager.wxs.bk ..\..\..\KeyboardManager.wxs
call move /Y ..\..\..\MouseWithoutBorders.wxs.bk ..\..\..\MouseWithoutBorders.wxs
@@ -114,6 +115,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
<Compile Include="FileLocksmith.wxs" />
<Compile Include="Hosts.wxs" />
<Compile Include="ImageResizer.wxs" />
<Compile Include="LightSwitch.wxs" />
<Compile Include="KeyboardManager.wxs" />
<Compile Include="Peek.wxs" />
<Compile Include="PowerRename.wxs" />

View File

@@ -50,6 +50,7 @@
<ComponentGroupRef Id="HostsComponentGroup" />
<ComponentGroupRef Id="ImageResizerComponentGroup" />
<ComponentGroupRef Id="KeyboardManagerComponentGroup" />
<ComponentGroupRef Id="LightSwitchComponentGroup" />
<ComponentGroupRef Id="PeekComponentGroup" />
<ComponentGroupRef Id="PowerRenameComponentGroup" />
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />

View File

@@ -182,6 +182,10 @@ Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptR
Generate-FileList -fileDepsJson "" -fileListName ImageResizerAssetsFiles -wxsFilePath $PSScriptRoot\ImageResizer.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ImageResizer"
Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs -regroot $registryroot
# Light Switch Service
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs -regroot $registryroot
#New+
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs -regroot $registryroot

View File

@@ -2,7 +2,10 @@
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<CmdPalVersion Condition="'$(CmdPalVersion)'=='' and '$(XES_APPXMANIFESTVERSION)'!=''">$(XES_APPXMANIFESTVERSION)</CmdPalVersion>
<!-- MIKE: The file you're looking for is src/modules/cmdpal/custom.props -->
<CmdPalVersion Condition="'$(CmdPalVersion)'==''">0.0.1.0</CmdPalVersion>
<DevEnvironment>Local</DevEnvironment>
<!-- Forcing for every DLL on by default -->

View File

@@ -17,6 +17,7 @@ namespace Common.UI
Awake,
ColorPicker,
CmdNotFound,
LightSwitch,
FancyZones,
FileLocksmith,
Run,
@@ -60,6 +61,8 @@ namespace Common.UI
return "ColorPicker";
case SettingsWindow.CmdNotFound:
return "CmdNotFound";
case SettingsWindow.LightSwitch:
return "LightSwitch";
case SettingsWindow.FancyZones:
return "FancyZones";
case SettingsWindow.FileLocksmith:

View File

@@ -28,6 +28,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredCropAndLockEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredLightSwitchEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredLightSwitchEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredFancyZonesEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredFancyZonesEnabledValue());

View File

@@ -13,6 +13,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredCmdPalEnabledValue();
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();

View File

@@ -17,6 +17,7 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredCmdPalEnabledValue();
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();

View File

@@ -19,6 +19,7 @@ namespace ManagedCommon
Hosts,
ImageResizer,
KeyboardManager,
LightSwitch,
MouseHighlighter,
MouseJump,
MousePointerCrosshairs,

View File

@@ -81,6 +81,14 @@ namespace Microsoft.PowerToys.UITest
get { return this.windowsElement?.Selected ?? false; }
}
/// <summary>
/// Gets a value indicating whether the UI element is visible to the user.
/// </summary>
public bool Displayed
{
get { return this.windowsElement?.Displayed ?? false; }
}
/// <summary>
/// Gets the Rect of the UI element.
/// </summary>
@@ -329,7 +337,7 @@ namespace Microsoft.PowerToys.UITest
/// Send Key of the element.
/// </summary>
/// <param name="key">The Key to Send.</param>
protected void SendKeys(string key)
public void SendKeys(string key)
{
PerformAction((actions, windowElement) =>
{
@@ -369,5 +377,19 @@ namespace Microsoft.PowerToys.UITest
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method SaveToPngFile with parameter: path = {path}");
this.windowsElement.GetScreenshot().SaveAsFile(path);
}
public void EnsureVisible(Element scrollViewer, int maxScrolls = 10)
{
int count = 0;
if (scrollViewer.WindowsElement != null)
{
while (!this.windowsElement!.Displayed && count < maxScrolls)
{
scrollViewer.WindowsElement.SendKeys(OpenQA.Selenium.Keys.PageDown);
Task.Delay(250).Wait();
count++;
}
}
}
}
}

View File

@@ -34,6 +34,7 @@ namespace Microsoft.PowerToys.UITest
PowerRename,
CommandPalette,
ScreenRuler,
LightSwitch,
}
/// <summary>
@@ -106,6 +107,7 @@ namespace Microsoft.PowerToys.UITest
[PowerToysModule.PowerRename] = new ModuleInfo("PowerToys.PowerRename.exe", "PowerRename", "WinUI3Apps"),
[PowerToysModule.CommandPalette] = new ModuleInfo("Microsoft.CmdPal.UI.exe", "PowerToys Command Palette", "WinUI3Apps\\CmdPal"),
[PowerToysModule.ScreenRuler] = new ModuleInfo("PowerToys.MeasureToolUI.exe", "PowerToys.ScreenRuler", "WinUI3Apps"),
[PowerToysModule.LightSwitch] = new ModuleInfo("PowerToys.LightSwitch.exe", "PowerToys.LightSwitch", "LightSwitchService"),
};
}

View File

@@ -81,6 +81,7 @@ struct LogSettings
inline const static std::string workspacesSnapshotToolLoggerName = "workspaces-snapshot-tool";
inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.log";
inline const static std::string zoomItLoggerName = "zoom-it";
inline const static std::string lightSwitchLoggerName = "light-switch";
inline const static int retention = 30;
std::wstring logLevel;
LogSettings();

View File

@@ -257,7 +257,9 @@ inline HANDLE run_elevated(const std::wstring& file, const std::wstring& params,
exec_info.nShow = SW_HIDE;
}
return ShellExecuteExW(&exec_info) ? exec_info.hProcess : nullptr;
BOOL result = ShellExecuteExW(&exec_info);
return result ? exec_info.hProcess : nullptr;
}
// Run command as non-elevated user, returns true if succeeded, puts the process id into returnPid if returnPid != NULL

View File

@@ -30,6 +30,7 @@ namespace powertoys_gpo
const std::wstring POLICY_CONFIGURE_ENABLED_CMD_NOT_FOUND = L"ConfigureEnabledUtilityCmdNotFound";
const std::wstring POLICY_CONFIGURE_ENABLED_COLOR_PICKER = L"ConfigureEnabledUtilityColorPicker";
const std::wstring POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK = L"ConfigureEnabledUtilityCropAndLock";
const std::wstring POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH = L"ConfigureEnabledUtilityLightSwitch";
const std::wstring POLICY_CONFIGURE_ENABLED_FANCYZONES = L"ConfigureEnabledUtilityFancyZones";
const std::wstring POLICY_CONFIGURE_ENABLED_FILE_LOCKSMITH = L"ConfigureEnabledUtilityFileLocksmith";
const std::wstring POLICY_CONFIGURE_ENABLED_SVG_PREVIEW = L"ConfigureEnabledUtilityFileExplorerSVGPreview";
@@ -295,6 +296,11 @@ namespace powertoys_gpo
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK);
}
inline gpo_rule_configured_t getConfiguredLightSwitchEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH);
}
inline gpo_rule_configured_t getConfiguredFancyZonesEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_FANCYZONES);

View File

@@ -137,6 +137,16 @@
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityLightSwitch" class="Both" displayName="$(string.ConfigureEnabledUtilityLightSwitch)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityLightSwitch">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_95_0" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityEnvironmentVariables" class="Both" displayName="$(string.ConfigureEnabledUtilityEnvironmentVariables)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityEnvironmentVariables">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_75_0" />

View File

@@ -245,6 +245,7 @@ If you don't configure this policy, the user will be able to control the setting
<string id="ConfigureEnabledUtilityCmdNotFound">Command Not Found: Configure enabled state</string>
<string id="ConfigureEnabledUtilityCmdPal">CmdPal: Configure enabled state</string>
<string id="ConfigureEnabledUtilityCropAndLock">Crop And Lock: Configure enabled state</string>
<string id="ConfigureEnabledUtilityLightSwitch">Light Switch: Configure enabled state</string>
<string id="ConfigureEnabledUtilityEnvironmentVariables">Environment Variables: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFancyZones">FancyZones: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFileLocksmith">File Locksmith: Configure enabled state</string>

View File

@@ -0,0 +1,32 @@
1 VERSIONINFO
FILEVERSION 0,1,0,0
PRODUCTVERSION 0,1,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
#endif
FILEOS 0x40004L
FILETYPE 0x2L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "Company Name"
VALUE "FileDescription", "Light Switch Module"
VALUE "FileVersion", "0.1.0.0"
VALUE "InternalName", "Light Switch"
VALUE "LegalCopyright", "Copyright (C) 2019 Company Name"
VALUE "OriginalFilename", "PowerToys.LightSwitchModuleInterface.dll"
VALUE "ProductName", "Light Switch"
VALUE "ProductVersion", "0.1.0.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

View File

@@ -0,0 +1,225 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{38177d56-6ad1-4adf-88c9-2843a7932166}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>LightSwitchModuleInterface</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>LightSwitchModuleInterface</ProjectName>
<TargetName>PowerToys.LightSwitchModuleInterface</TargetName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">$(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="ThemeHelper.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Create</PrecompiledHeader>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|x64'">pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">pch.h</PrecompiledHeaderFile>
</ClCompile>
<ClCompile Include="ThemeHelper.cpp" />
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="LightSwitchModuleInterface.rc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj">
<Project>{4aed67b6-55fd-486f-b917-e543dee2cb3c}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ThemeHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ThemeHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Header Files">
<UniqueIdentifier>{bbf22ac8-46f8-4206-b44b-9c3897e99ce5}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files">
<UniqueIdentifier>{530ed784-9a70-46a0-8fb6-20d5dee4f7d3}</UniqueIdentifier>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{da1cb871-86d3-414c-adf5-a7e9f2077d2f}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="LightSwitchModuleInterface.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,81 @@
#include "pch.h"
#include <windows.h>
#include "ThemeHelper.h"
// Controls changing the themes.
void SetAppsTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = mode;
RegSetValueEx(hKey, L"AppsUseLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
void SetSystemTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = mode;
RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
bool GetCurrentSystemTheme()
{
HKEY hKey;
DWORD value = 1; // default = light
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
RegQueryValueEx(hKey, L"SystemUsesLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
RegCloseKey(hKey);
}
return value == 1; // true = light, false = dark
}
bool GetCurrentAppsTheme()
{
HKEY hKey;
DWORD value = 1;
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
RegQueryValueEx(hKey, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
RegCloseKey(hKey);
}
return value == 1; // true = light, false = dark
}

View File

@@ -0,0 +1,5 @@
#pragma once
void SetSystemTheme(bool dark);
void SetAppsTheme(bool dark);
bool GetCurrentSystemTheme();
bool GetCurrentAppsTheme();

View File

@@ -0,0 +1,570 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include "trace.h"
#include <common/logger/logger.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <locale>
#include <codecvt>
#include <common/utils/logger_helper.h>
#include "ThemeHelper.h"
extern "C" IMAGE_DOS_HEADER __ImageBase;
namespace
{
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_WIN[] = L"win";
const wchar_t JSON_KEY_ALT[] = L"alt";
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_TOGGLE_THEME_HOTKEY[] = L"toggle-theme-hotkey";
const wchar_t JSON_KEY_VALUE[] = L"value";
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
// The PowerToy name that will be shown in the settings.
const static wchar_t* MODULE_NAME = L"LightSwitch";
// Add a description that will we shown in the module settings page.
const static wchar_t* MODULE_DESC = L"This is a module that allows you to control light/dark theming via set times, sun rise, or directly invoking the change.";
enum class ScheduleMode
{
FixedHours,
SunsetToSunrise,
// add more later
};
inline std::wstring ToString(ScheduleMode mode)
{
switch (mode)
{
case ScheduleMode::SunsetToSunrise:
return L"SunsetToSunrise";
case ScheduleMode::FixedHours:
default:
return L"FixedHours";
}
}
inline ScheduleMode FromString(const std::wstring& str)
{
if (str == L"SunsetToSunrise")
return ScheduleMode::SunsetToSunrise;
return ScheduleMode::FixedHours;
}
// These are the properties shown in the Settings page.
struct ModuleSettings
{
bool m_changeSystem = true;
bool m_changeApps = true;
ScheduleMode m_scheduleMode = ScheduleMode::FixedHours;
int m_lightTime = 480;
int m_darkTime = 1200;
int m_sunrise_offset = 0;
int m_sunset_offset = 0;
std::wstring m_latitude = L"0.0";
std::wstring m_longitude = L"0.0";
} g_settings;
class LightSwitchInterface : public PowertoyModuleIface
{
private:
bool m_enabled = false;
HANDLE m_process{ nullptr };
HANDLE m_force_light_event_handle;
HANDLE m_force_dark_event_handle;
HANDLE m_manual_override_event_handle;
static const constexpr int NUM_DEFAULT_HOTKEYS = 4;
Hotkey m_toggle_theme_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'D' };
void init_settings();
public:
LightSwitchInterface()
{
LoggerHelpers::init_logger(L"LightSwitch", L"ModuleInterface", LogSettings::lightSwitchLoggerName);
m_force_light_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_LIGHT");
m_force_dark_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_DARK");
m_manual_override_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
init_settings();
};
virtual const wchar_t* get_key() override
{
return L"LightSwitch";
}
// Destroy the powertoy and free memory
virtual void destroy() override
{
delete this;
}
// Return the display name of the powertoy, this will be cached by the runner
virtual const wchar_t* get_name() override
{
return MODULE_NAME;
}
// Return the configured status for the gpo policy for the module
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::getConfiguredLightSwitchEnabledValue();
}
// Return JSON with the configuration options.
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
// Create a Settings object with your module name
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(MODULE_DESC);
settings.set_overview_link(L"https://aka.ms/powertoys");
// Boolean toggles
settings.add_bool_toggle(
L"changeSystem",
L"Change System Theme",
g_settings.m_changeSystem);
settings.add_bool_toggle(
L"changeApps",
L"Change Apps Theme",
g_settings.m_changeApps);
settings.add_choice_group(
L"scheduleMode",
L"Theme schedule mode",
ToString(g_settings.m_scheduleMode),
{ { L"FixedHours", L"Set hours manually" },
{ L"SunsetToSunrise", L"Use sunrise/sunset times" } });
// Integer spinners
settings.add_int_spinner(
L"lightTime",
L"Time to switch to light theme (minutes after midnight).",
g_settings.m_lightTime,
0,
1439,
1);
settings.add_int_spinner(
L"darkTime",
L"Time to switch to dark theme (minutes after midnight).",
g_settings.m_darkTime,
0,
1439,
1);
settings.add_int_spinner(
L"sunrise_offset",
L"Time to offset turning on your light theme.",
g_settings.m_sunrise_offset,
0,
1439,
1);
settings.add_int_spinner(
L"sunset_offset",
L"Time to offset turning on your dark theme.",
g_settings.m_sunset_offset,
0,
1439,
1);
// Strings for latitude and longitude
settings.add_string(
L"latitude",
L"Your latitude in decimal degrees (e.g. 39.95).",
g_settings.m_latitude);
settings.add_string(
L"longitude",
L"Your longitude in decimal degrees (e.g. -75.16).",
g_settings.m_longitude);
// One-shot actions (buttons)
settings.add_custom_action(
L"forceLight",
L"Switch immediately to light theme",
L"Force Light",
L"{}");
settings.add_custom_action(
L"forceDark",
L"Switch immediately to dark theme",
L"Force Dark",
L"{}");
// Hotkeys
PowerToysSettings::HotkeyObject dm_hk = PowerToysSettings::HotkeyObject::from_settings(
m_toggle_theme_hotkey.win,
m_toggle_theme_hotkey.ctrl,
m_toggle_theme_hotkey.alt,
m_toggle_theme_hotkey.shift,
m_toggle_theme_hotkey.key);
settings.add_hotkey(
L"toggle-theme-hotkey",
L"Shortcut to toggle theme immediately",
dm_hk);
// Serialize to buffer for the PowerToys runner
return settings.serialize_to_buffer(buffer, buffer_size);
}
// Signal from the Settings editor to call a custom action.
// This can be used to spawn more complex editors.
void call_custom_action(const wchar_t* action) override
{
try
{
auto action_object = PowerToysSettings::CustomActionObject::from_json_string(action);
if (action_object.get_name() == L"forceLight")
{
Logger::info(L"[Light Switch] Custom action triggered: Force Light");
SetSystemTheme(true);
SetAppsTheme(true);
}
else if (action_object.get_name() == L"forceDark")
{
Logger::info(L"[Light Switch] Custom action triggered: Force Dark");
SetSystemTheme(false);
SetAppsTheme(false);
}
}
catch (...)
{
Logger::error(L"[Light Switch] Invalid custom action JSON");
}
}
// Called by the runner to pass the updated settings values as a serialized JSON.
virtual void set_config(const wchar_t* config) override
{
try
{
auto values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
parse_hotkey(values);
if (auto v = values.get_bool_value(L"changeSystem"))
{
g_settings.m_changeSystem = *v;
}
if (auto v = values.get_bool_value(L"changeApps"))
{
g_settings.m_changeApps = *v;
}
if (auto v = values.get_string_value(L"scheduleMode"))
{
g_settings.m_scheduleMode = FromString(*v);
}
if (auto v = values.get_int_value(L"lightTime"))
{
g_settings.m_lightTime = *v;
}
if (auto v = values.get_int_value(L"darkTime"))
{
g_settings.m_darkTime = *v;
}
if (auto v = values.get_int_value(L"sunrise_offset"))
{
g_settings.m_sunrise_offset = *v;
}
if (auto v = values.get_int_value(L"m_sunset_offset"))
{
g_settings.m_sunset_offset = *v;
}
if (auto v = values.get_string_value(L"latitude"))
{
g_settings.m_latitude = *v;
}
if (auto v = values.get_string_value(L"longitude"))
{
g_settings.m_longitude = *v;
}
values.save_to_settings_file();
}
catch (const std::exception&)
{
Logger::error("[Light Switch] set_config: Failed to parse or apply config.");
}
}
virtual void enable()
{
m_enabled = true;
Logger::info(L"Enabling Light Switch module...");
unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring args = L"--pid " + std::to_wstring(powertoys_pid);
std::wstring exe_name = L"LightSwitchService\\PowerToys.LightSwitchService.exe";
std::wstring resolved_path(MAX_PATH, L'\0');
DWORD result = SearchPathW(
nullptr,
exe_name.c_str(),
nullptr,
static_cast<DWORD>(resolved_path.size()),
resolved_path.data(),
nullptr);
if (result == 0 || result >= resolved_path.size())
{
Logger::error(
L"Failed to locate Light Switch executable named '{}' at location '{}'",
exe_name,
resolved_path.c_str());
return;
}
resolved_path.resize(result);
Logger::debug(L"Resolved executable path: {}", resolved_path);
std::wstring command_line = L"\"" + resolved_path + L"\" " + args;
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
if (!CreateProcessW(
resolved_path.c_str(),
command_line.data(),
nullptr,
nullptr,
TRUE,
0,
nullptr,
nullptr,
&si,
&pi))
{
Logger::error(L"Failed to launch Light Switch process. {}", get_last_error_or_default(GetLastError()));
return;
}
Logger::info(L"Light Switch process launched successfully (PID: {}).", pi.dwProcessId);
m_process = pi.hProcess;
CloseHandle(pi.hThread);
}
// Disable the powertoy
virtual void disable()
{
Logger::info("Light Switch disabling");
m_enabled = false;
if (m_process)
{
constexpr DWORD timeout_ms = 1500;
DWORD result = WaitForSingleObject(m_process, timeout_ms);
if (result == WAIT_TIMEOUT)
{
Logger::warn("Light Switch: Process didn't exit in time. Forcing termination.");
TerminateProcess(m_process, 0);
}
CloseHandle(m_manual_override_event_handle);
m_manual_override_event_handle = nullptr;
CloseHandle(m_process);
m_process = nullptr;
}
}
// Returns if the powertoys is enabled
virtual bool is_enabled() override
{
return m_enabled;
}
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
{
auto settingsObject = settings.get_raw_json();
if (settingsObject.GetView().Size())
{
try
{
Hotkey _temp_toggle_theme;
auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_TOGGLE_THEME_HOTKEY).GetNamedObject(JSON_KEY_VALUE);
_temp_toggle_theme.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
_temp_toggle_theme.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
_temp_toggle_theme.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
_temp_toggle_theme.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
_temp_toggle_theme.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
m_toggle_theme_hotkey = _temp_toggle_theme;
}
catch (...)
{
Logger::error("Failed to initialize Light Switch force dark mode shortcut from settings. Value will keep unchanged.");
}
}
else
{
Logger::info("Light Switch settings are empty");
}
}
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
if (hotkeys && buffer_size >= 1)
{
hotkeys[0] = m_toggle_theme_hotkey;
}
return 1;
}
virtual bool on_hotkey(size_t hotkeyId) override
{
if (m_enabled)
{
Logger::trace(L"Light Switch hotkey pressed");
if (!is_process_running())
{
enable();
}
else if (hotkeyId == 0)
{
// get current will return true if in light mode, otherwise false
Logger::info(L"[Light Switch] Hotkey triggered: Toggle Theme");
if (g_settings.m_changeSystem)
{
SetSystemTheme(!GetCurrentSystemTheme());
}
if (g_settings.m_changeApps)
{
SetAppsTheme(!GetCurrentAppsTheme());
}
if (m_manual_override_event_handle)
{
SetEvent(m_manual_override_event_handle);
Logger::debug(L"[Light Switch] Manual override event set");
}
}
return true;
}
return false;
}
bool is_process_running()
{
return WaitForSingleObject(m_process, 0) == WAIT_TIMEOUT;
}
};
std::wstring utf8_to_wstring(const std::string& str)
{
if (str.empty())
return std::wstring();
int size_needed = MultiByteToWideChar(
CP_UTF8,
0,
str.c_str(),
static_cast<int>(str.size()),
nullptr,
0);
std::wstring wstr(size_needed, 0);
MultiByteToWideChar(
CP_UTF8,
0,
str.c_str(),
static_cast<int>(str.size()),
&wstr[0],
size_needed);
return wstr;
}
// Load the settings file.
void LightSwitchInterface::init_settings()
{
Logger::info(L"[Light Switch] init_settings: starting to load settings for module");
try
{
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(get_name());
parse_hotkey(settings);
if (auto v = settings.get_bool_value(L"changeSystem"))
g_settings.m_changeSystem = *v;
if (auto v = settings.get_bool_value(L"changeApps"))
g_settings.m_changeApps = *v;
if (auto v = settings.get_string_value(L"scheduleMode"))
g_settings.m_scheduleMode = FromString(*v);
if (auto v = settings.get_int_value(L"lightTime"))
g_settings.m_lightTime = *v;
if (auto v = settings.get_int_value(L"darkTime"))
g_settings.m_darkTime = *v;
if (auto v = settings.get_int_value(L"sunrise_offset"))
g_settings.m_sunrise_offset = *v;
if (auto v = settings.get_int_value(L"sunset_offset"))
g_settings.m_sunset_offset = *v;
if (auto v = settings.get_string_value(L"latitude"))
g_settings.m_latitude = *v;
if (auto v = settings.get_string_value(L"longitude"))
g_settings.m_longitude = *v;
Logger::info(L"[Light Switch] init_settings: loaded successfully");
}
catch (const winrt::hresult_error& e)
{
Logger::error(L"[Light Switch] init_settings: hresult_error 0x{:08X} - {}", e.code(), e.message().c_str());
}
catch (const std::exception& e)
{
std::wstring whatStr = utf8_to_wstring(e.what());
Logger::error(L"[Light Switch] init_settings: std::exception - {}", whatStr);
}
catch (...)
{
Logger::error(L"[Light Switch] init_settings: unknown exception while loading settings");
}
}
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new LightSwitchInterface();
}

View File

@@ -0,0 +1,2 @@
#include "pch.h"
#pragma comment(lib, "windowsapp")

View File

@@ -0,0 +1,14 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/utils/gpo.h>
#include <common/utils/winapi_error.h>
#include <shlwapi.h>
#include <shellapi.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.System.h>
#include <winrt/Windows.Globalization.h>
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.ApplicationModel.Core.h>

View File

@@ -0,0 +1,30 @@
#include "pch.h"
#include "trace.h"
#include <TraceLoggingProvider.h>
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::RegisterProvider()
{
TraceLoggingRegister(g_hProvider);
}
void Trace::UnregisterProvider()
{
TraceLoggingUnregister(g_hProvider);
}
void Trace::MyEvent()
{
TraceLoggingWrite(
g_hProvider,
"PowerToyName_MyEvent",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include <windows.h>
#include <TraceLoggingActivity.h>
#include <common/telemetry/ProjectTelemetry.h>
TRACELOGGING_DECLARE_PROVIDER(g_hProvider);
class Trace
{
public:
static void RegisterProvider();
static void UnregisterProvider();
static void MyEvent();
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -0,0 +1,295 @@
#include <windows.h>
#include <tchar.h>
#include "ThemeScheduler.h"
#include "ThemeHelper.h"
#include <common/SettingsAPI/settings_objects.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <stdio.h>
#include <string>
#include <LightSwitchSettings.h>
#include <common/utils/gpo.h>
SERVICE_STATUS g_ServiceStatus = {};
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
HANDLE g_ServiceStopEvent = nullptr;
static int g_lastUpdatedDay = -1;
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam);
// Entry point for the executable
int _tmain(int argc, TCHAR* argv[])
{
DWORD parentPid = 0;
bool debug = false;
for (int i = 1; i < argc; ++i)
{
if (_tcscmp(argv[i], _T("--debug")) == 0)
debug = true;
else if (_tcscmp(argv[i], _T("--pid")) == 0 && i + 1 < argc)
parentPid = _tstoi(argv[++i]);
}
// Try to connect to SCM
wchar_t serviceName[] = L"LightSwitchService";
SERVICE_TABLE_ENTRYW table[] = { { serviceName, ServiceMain }, { nullptr, nullptr } };
if (!StartServiceCtrlDispatcherW(table))
{
DWORD err = GetLastError();
if (err == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) // not launched by SCM
{
g_ServiceStopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
HANDLE hThread = CreateThread(
nullptr, 0, ServiceWorkerThread, reinterpret_cast<void*>(static_cast<ULONG_PTR>(parentPid)), 0, nullptr);
// Wait so the process stays alive
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(g_ServiceStopEvent);
return 0;
}
return static_cast<int>(err);
}
return 0;
}
// Called when the service is launched by Windows
VOID WINAPI ServiceMain(DWORD, LPTSTR*)
{
g_StatusHandle = RegisterServiceCtrlHandler(_T("LightSwitchService"), ServiceCtrlHandler);
if (!g_StatusHandle)
return;
g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
g_ServiceStopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!g_ServiceStopEvent)
{
g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
g_ServiceStatus.dwWin32ExitCode = GetLastError();
SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
return;
}
SECURITY_ATTRIBUTES sa{ sizeof(sa) };
sa.bInheritHandle = FALSE;
sa.lpSecurityDescriptor = nullptr;
g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
HANDLE hThread = CreateThread(nullptr, 0, ServiceWorkerThread, nullptr, 0, nullptr);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(g_ServiceStopEvent);
g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
g_ServiceStatus.dwWin32ExitCode = 0;
SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
}
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl)
{
switch (dwCtrl)
{
case SERVICE_CONTROL_STOP:
if (g_ServiceStatus.dwCurrentState != SERVICE_RUNNING)
break;
g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING;
SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
// Signal the service to stop
SetEvent(g_ServiceStopEvent);
break;
default:
break;
}
}
static void update_sun_times(auto& settings)
{
double latitude = std::stod(settings.latitude);
double longitude = std::stod(settings.longitude);
SYSTEMTIME st;
GetLocalTime(&st);
SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay);
int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute;
int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute;
auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
values.add_property(L"lightTime", newLightTime);
values.add_property(L"darkTime", newDarkTime);
values.save_to_settings_file();
OutputDebugString(L"[LightSwitchService] Updated sun times and saved to config.\n");
}
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
{
DWORD parentPid = static_cast<DWORD>(reinterpret_cast<ULONG_PTR>(lpParam));
HANDLE hParent = nullptr;
if (parentPid)
hParent = OpenProcess(SYNCHRONIZE, FALSE, parentPid);
OutputDebugString(L"[LightSwitchService] Worker thread starting...\n");
// Initialize settings system
LightSwitchSettings::instance().InitFileWatcher();
// Open the manual override event created by the module interface
HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
auto applyTheme = [](int nowMinutes, int lightMinutes, int darkMinutes, const auto& settings) {
bool isLightActive = false;
if (lightMinutes < darkMinutes)
{
// Normal case: sunrise < sunset
isLightActive = (nowMinutes >= lightMinutes && nowMinutes < darkMinutes);
}
else
{
// Wraparound case: e.g. light at 21:00, dark at 06:00
isLightActive = (nowMinutes >= lightMinutes || nowMinutes < darkMinutes);
}
bool isSystemCurrentlyLight = GetCurrentSystemTheme();
bool isAppsCurrentlyLight = GetCurrentAppsTheme();
if (isLightActive)
{
if (settings.changeSystem && !isSystemCurrentlyLight)
SetSystemTheme(true);
if (settings.changeApps && !isAppsCurrentlyLight)
SetAppsTheme(true);
}
else
{
if (settings.changeSystem && isSystemCurrentlyLight)
SetSystemTheme(false);
if (settings.changeApps && isAppsCurrentlyLight)
SetAppsTheme(false);
}
};
// --- At service start: immediately honor the schedule ---
{
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
LightSwitchSettings::instance().LoadSettings();
const auto& settings = LightSwitchSettings::instance().settings();
applyTheme(nowMinutes, settings.lightTime + settings.sunrise_offset, settings.darkTime + settings.sunset_offset, settings);
}
// --- Main loop: wakes once per minute or stop/parent death ---
for (;;)
{
HANDLE waits[2] = { g_ServiceStopEvent, hParent };
DWORD count = hParent ? 2 : 1;
SYSTEMTIME st;
GetLocalTime(&st);
int nowMinutes = st.wHour * 60 + st.wMinute;
LightSwitchSettings::instance().LoadSettings();
const auto& settings = LightSwitchSettings::instance().settings();
// Refresh suntimes at day boundary
if (g_lastUpdatedDay != st.wDay)
{
update_sun_times(settings);
g_lastUpdatedDay = st.wDay;
OutputDebugString(L"[LightSwitchService] Recalculated sun times at new day boundary.\n");
}
wchar_t msg[160];
swprintf_s(msg,
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d\n",
st.wHour,
st.wMinute,
settings.lightTime / 60,
settings.lightTime % 60,
settings.darkTime / 60,
settings.darkTime % 60);
OutputDebugString(msg);
// --- Manual override check ---
bool manualOverrideActive = false;
if (hManualOverride)
{
manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
}
if (manualOverrideActive)
{
// Did we hit a scheduled boundary? (reset override at boundary)
if (nowMinutes == (settings.lightTime + settings.sunrise_offset) % 1440 ||
nowMinutes == (settings.darkTime + settings.sunset_offset) % 1440)
{
ResetEvent(hManualOverride);
OutputDebugString(L"[LightSwitchService] Manual override cleared at boundary\n");
}
else
{
OutputDebugString(L"[LightSwitchService] Skipping schedule due to manual override\n");
goto sleep_until_next_minute;
}
}
// Apply theme logic (only runs if no manual override or override just cleared)
applyTheme(nowMinutes, settings.lightTime + settings.sunrise_offset, settings.darkTime + settings.sunset_offset, settings);
sleep_until_next_minute:
GetLocalTime(&st);
int msToNextMinute = (60 - st.wSecond) * 1000 - st.wMilliseconds;
if (msToNextMinute < 50)
msToNextMinute = 50;
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute);
if (wait == WAIT_OBJECT_0) // stop event
break;
if (hParent && wait == WAIT_OBJECT_0 + 1) // parent exited
break;
}
if (hManualOverride)
CloseHandle(hManualOverride);
if (hParent)
CloseHandle(hParent);
return 0;
}
int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)
{
wchar_t msg[160];
swprintf_s(
msg,
L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.\n");
OutputDebugString(msg);
return 0;
}
int argc = 0;
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
int rc = _tmain(argc, argv); // reuse your existing logic
LocalFree(argv);
return rc;
}

View File

@@ -0,0 +1,219 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{08e71c67-6a7e-4ca1-b04e-2fb336410bac}</ProjectGuid>
<RootNamespace>LightSwitchService</RootNamespace>
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
<ProjectName>LightSwitchService</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
<TargetName>PowerToys.LightSwitchService</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalIncludeDirectories>
./../;
..\..\..\common\Telemetry;
..\..\..\common;
..\..\..\;
..\..\..\..\deps\spdlog\include;
./;
%(AdditionalIncludeDirectories)
</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>Advapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\notifications\notifications.vcxproj">
<Project>{1d5be09d-78c0-4fd7-af00-ae7c1af7c525}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\Telemetry\EtwTrace\EtwTrace.vcxproj">
<Project>{8f021b46-362b-485c-bfba-ccf83e820cbd}</Project>
</ProjectReference>
</ItemGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="LightSwitchService.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="LightSwitchSettings.cpp" />
<ClCompile Include="SettingsConstants.cpp" />
<ClCompile Include="ThemeHelper.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="ThemeScheduler.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="WinHookEventIDs.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="LightSwitchService.rc" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="LightSwitchSettings.h" />
<ClInclude Include="SettingsConstants.h" />
<ClInclude Include="SettingsObserver.h" />
<ClInclude Include="ThemeHelper.h" />
<ClInclude Include="ThemeScheduler.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">false</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="WinHookEventIDs.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="LightSwitchService.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ThemeScheduler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ThemeHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\..\common\SettingsAPI\settings_helpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\..\common\SettingsAPI\settings_objects.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\..\common\SettingsAPI\FileWatcher.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LightSwitchSettings.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SettingsConstants.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WinHookEventIDs.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="ThemeScheduler.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ThemeHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LightSwitchSettings.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SettingsConstants.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SettingsObserver.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WinHookEventIDs.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,167 @@
#include "LightSwitchSettings.h"
#include <common/utils/json.h>
#include <common/SettingsAPI/settings_helpers.h>
#include "SettingsObserver.h"
#include <filesystem>
#include <fstream>
#include <WinHookEventIDs.h>
using namespace std;
LightSwitchSettings& LightSwitchSettings::instance()
{
static LightSwitchSettings inst;
return inst;
}
LightSwitchSettings::LightSwitchSettings()
{
LoadSettings();
}
std::wstring LightSwitchSettings::GetSettingsFileName()
{
return PTSettingsHelper::get_module_save_file_location(L"LightSwitch");
}
void LightSwitchSettings::InitFileWatcher()
{
const std::wstring& settingsFileName = GetSettingsFileName();
m_settingsFileWatcher = std::make_unique<FileWatcher>(settingsFileName, [&]() {
PostMessageW(HWND_BROADCAST, WM_PRIV_SETTINGS_CHANGED, NULL, NULL);
});
}
void LightSwitchSettings::AddObserver(SettingsObserver& observer)
{
m_observers.insert(&observer);
}
void LightSwitchSettings::RemoveObserver(SettingsObserver& observer)
{
m_observers.erase(&observer);
}
void LightSwitchSettings::NotifyObservers(SettingId id) const
{
for (auto observer : m_observers)
{
if (observer->WantsToBeNotified(id))
{
observer->SettingsUpdate(id);
}
}
}
void LightSwitchSettings::LoadSettings()
{
try
{
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
if (const auto jsonVal = values.get_string_value(L"scheduleMode"))
{
auto val = *jsonVal;
auto newMode = FromString(val);
if (m_settings.scheduleMode != newMode)
{
m_settings.scheduleMode = newMode;
NotifyObservers(SettingId::ScheduleMode);
}
}
// Latitude
if (const auto jsonVal = values.get_string_value(L"latitude"))
{
auto val = *jsonVal;
if (m_settings.latitude != val)
{
m_settings.latitude = val;
NotifyObservers(SettingId::Latitude);
}
}
// Longitude
if (const auto jsonVal = values.get_string_value(L"longitude"))
{
auto val = *jsonVal;
if (m_settings.longitude != val)
{
m_settings.longitude = val;
NotifyObservers(SettingId::Longitude);
}
}
// LightTime
if (const auto jsonVal = values.get_int_value(L"lightTime"))
{
auto val = *jsonVal;
if (m_settings.lightTime != val)
{
m_settings.lightTime = val;
NotifyObservers(SettingId::LightTime);
}
}
// DarkTime
if (const auto jsonVal = values.get_int_value(L"darkTime"))
{
auto val = *jsonVal;
if (m_settings.darkTime != val)
{
m_settings.darkTime = val;
NotifyObservers(SettingId::DarkTime);
}
}
// Offset
if (const auto jsonVal = values.get_int_value(L"sunrise_offset"))
{
auto val = *jsonVal;
if (m_settings.sunrise_offset != val)
{
m_settings.sunrise_offset = val;
NotifyObservers(SettingId::Sunrise_Offset);
}
}
if (const auto jsonVal = values.get_int_value(L"sunset_offset"))
{
auto val = *jsonVal;
if (m_settings.sunset_offset != val)
{
m_settings.sunset_offset = val;
NotifyObservers(SettingId::Sunset_Offset);
}
}
// ChangeSystem
if (const auto jsonVal = values.get_bool_value(L"changeSystem"))
{
auto val = *jsonVal;
if (m_settings.changeSystem != val)
{
m_settings.changeSystem = val;
NotifyObservers(SettingId::ChangeSystem);
}
}
// ChangeApps
if (const auto jsonVal = values.get_bool_value(L"changeApps"))
{
auto val = *jsonVal;
if (m_settings.changeApps != val)
{
m_settings.changeApps = val;
NotifyObservers(SettingId::ChangeApps);
}
}
}
catch (...)
{
// Keeps defaults if load fails
}
}

View File

@@ -0,0 +1,88 @@
#pragma once
#include <unordered_set>
#include <string>
#include <vector>
#include <memory>
#include <windows.h>
#include <common/SettingsAPI/FileWatcher.h>
#include <common/SettingsAPI/settings_objects.h>
#include <SettingsConstants.h>
class SettingsObserver;
enum class ScheduleMode
{
FixedHours,
SunsetToSunrise
// Add more in the future
};
inline std::wstring ToString(ScheduleMode mode)
{
switch (mode)
{
case ScheduleMode::FixedHours:
return L"FixedHours";
case ScheduleMode::SunsetToSunrise:
return L"SunsetToSunrise";
default:
return L"FixedHours";
}
}
inline ScheduleMode FromString(const std::wstring& str)
{
if (str == L"SunsetToSunrise")
return ScheduleMode::SunsetToSunrise;
else
return ScheduleMode::FixedHours;
}
struct LightSwitchConfig
{
ScheduleMode scheduleMode = ScheduleMode::FixedHours;
std::wstring latitude = L"0.0";
std::wstring longitude = L"0.0";
// Stored as minutes since midnight
int lightTime = 8 * 60; // 08:00 default
int darkTime = 20 * 60; // 20:00 default
int sunrise_offset = 0;
int sunset_offset = 0;
bool changeSystem = false;
bool changeApps = false;
};
class LightSwitchSettings
{
public:
static LightSwitchSettings& instance();
static inline const LightSwitchConfig& settings()
{
return instance().m_settings;
}
void InitFileWatcher();
static std::wstring GetSettingsFileName();
void AddObserver(SettingsObserver& observer);
void RemoveObserver(SettingsObserver& observer);
void LoadSettings();
private:
LightSwitchSettings();
~LightSwitchSettings() = default;
LightSwitchConfig m_settings;
std::unique_ptr<FileWatcher> m_settingsFileWatcher;
std::unordered_set<SettingsObserver*> m_observers;
void NotifyObservers(SettingId id) const;
};

View File

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

View File

@@ -0,0 +1,14 @@
#pragma once
enum class SettingId
{
ScheduleMode = 0,
Latitude,
Longitude,
LightTime,
DarkTime,
Sunrise_Offset,
Sunset_Offset,
ChangeSystem,
ChangeApps
};

View File

@@ -0,0 +1,32 @@
#pragma once
#include <unordered_set>
#include "SettingsConstants.h"
class LightSwitchSettings;
class SettingsObserver
{
public:
SettingsObserver(std::unordered_set<SettingId> observedSettings) :
m_observedSettings(std::move(observedSettings))
{
LightSwitchSettings::instance().AddObserver(*this);
}
virtual ~SettingsObserver()
{
LightSwitchSettings::instance().RemoveObserver(*this);
}
// Override this in your class to respond to updates
virtual void SettingsUpdate(SettingId type) {}
bool WantsToBeNotified(SettingId type) const noexcept
{
return m_observedSettings.contains(type);
}
protected:
std::unordered_set<SettingId> m_observedSettings;
};

View File

@@ -0,0 +1,81 @@
#include <windows.h>
#include "ThemeHelper.h"
// Controls changing the themes.
void SetAppsTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = mode;
RegSetValueEx(hKey, L"AppsUseLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
void SetSystemTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = mode;
RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
// Can think of this as "is the current theme light?"
bool GetCurrentSystemTheme()
{
HKEY hKey;
DWORD value = 1; // default = light
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
RegQueryValueEx(hKey, L"SystemUsesLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
RegCloseKey(hKey);
}
return value == 1; // true = light, false = dark
}
bool GetCurrentAppsTheme()
{
HKEY hKey;
DWORD value = 1;
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
RegQueryValueEx(hKey, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
RegCloseKey(hKey);
}
return value == 1; // true = light, false = dark
}

View File

@@ -0,0 +1,5 @@
#pragma once
void SetSystemTheme(bool dark);
void SetAppsTheme(bool dark);
bool GetCurrentSystemTheme();
bool GetCurrentAppsTheme();

View File

@@ -0,0 +1,89 @@
#include "ThemeScheduler.h"
#include <utility>
SunTimes CalculateSunriseSunset(double latitude, double longitude, int year, int month, int day)
{
double zenith = 90.833;
int N1 = static_cast<int>(floor(275.0 * month / 9.0));
int N2 = static_cast<int>(floor((static_cast<double>(month) + 9) / 12.0));
int N3 = static_cast<int>(floor((1.0 + floor((year - 4.0 * floor(year / 4.0) + 2.0) / 3.0))));
int N = N1 - (N2 * N3) + day - 30;
auto calcTime = [&](bool sunrise) -> double {
double lngHour = longitude / 15.0;
double t = sunrise ? N + ((6 - lngHour) / 24) : N + ((18 - lngHour) / 24);
double M = (0.9856 * t) - 3.289;
double L = M + (1.916 * sin(deg2rad(M))) + (0.020 * sin(2 * deg2rad(M))) + 282.634;
if (L < 0)
L += 360;
if (L > 360)
L -= 360;
double RA = rad2deg(atan(0.91764 * tan(deg2rad(L))));
if (RA < 0)
RA += 360;
if (RA > 360)
RA -= 360;
double Lquadrant = floor(L / 90) * 90;
double RAquadrant = floor(RA / 90) * 90;
RA = RA + (Lquadrant - RAquadrant);
RA /= 15;
double sinDec = 0.39782 * sin(deg2rad(L));
double cosDec = cos(asin(sinDec));
double cosH = (cos(deg2rad(zenith)) - (sinDec * sin(deg2rad(latitude)))) / (cosDec * cos(deg2rad(latitude)));
if (cosH > 1 || cosH < -1)
return -1;
double H = sunrise ? 360 - rad2deg(acos(cosH)) : rad2deg(acos(cosH));
H /= 15;
double T = H + RA - (0.06571 * t) - 6.622;
double UT = T - lngHour;
while (UT < 0)
UT += 24;
while (UT >= 24)
UT -= 24;
return UT;
};
double riseUT = calcTime(true);
double setUT = calcTime(false);
auto toLocal = [](double UT) {
TIME_ZONE_INFORMATION tz;
DWORD state = GetTimeZoneInformation(&tz);
double totalBias = tz.Bias;
if (state == TIME_ZONE_ID_DAYLIGHT)
totalBias += tz.DaylightBias;
else if (state == TIME_ZONE_ID_STANDARD)
totalBias += tz.StandardBias;
double biasHours = -(totalBias / 60.0);
double localTime = UT + biasHours;
while (localTime < 0)
localTime += 24;
while (localTime >= 24)
localTime -= 24;
int hour = static_cast<int>(localTime);
int minute = static_cast<int>((localTime - hour) * 60);
return std::pair<int, int>{ hour, minute };
};
auto [riseHour, riseMinute] = toLocal(riseUT);
auto [setHour, setMinute] = toLocal(setUT);
SunTimes result;
result.sunriseHour = riseHour;
result.sunriseMinute = riseMinute;
result.sunsetHour = setHour;
result.sunsetMinute = setMinute;
return result;
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include <cmath>
#include <ctime>
#include <windows.h>
// Struct to hold calculated sunrise/sunset times
struct SunTimes
{
int sunriseHour;
int sunriseMinute;
int sunsetHour;
int sunsetMinute;
};
constexpr double PI = 3.14159265358979323846;
constexpr double deg2rad(double deg)
{
return deg * PI / 180.0;
}
constexpr double rad2deg(double rad)
{
return rad * 180.0 / PI;
}
SunTimes CalculateSunriseSunset(double latitude, double longitude, int year, int month, int day);

View File

@@ -0,0 +1,15 @@
#include "WinHookEventIDs.h"
#include <wtypes.h>
#include <mutex>
UINT WM_PRIV_SETTINGS_CHANGED = 0;
std::once_flag init_flag;
void InitializeWinhookEventIds()
{
std::call_once(init_flag, [&] {
WM_PRIV_SETTINGS_CHANGED = RegisterWindowMessage(L"{11978F7B-221A-4E65-B9A9-693F7D6E4B25}");
});
}

View File

@@ -0,0 +1,6 @@
#pragma once
#include <Windows.h>
extern UINT WM_PRIV_SETTINGS_CHANGED; // Scheduled when a watched settings file is updated
void InitializeWinhookEventIds();

View File

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

View File

@@ -0,0 +1,16 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by LightSwitchService.rc
//
#define IDI_ICON1 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<RootNamespace>PowerToys.LightSwitch.UITests</RootNamespace>
<AssemblyName>LightSwitch.UITests</AssemblyName>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<!-- This is a UI test, so don't run as part of MSBuild -->
<RunVSTest>false</RunVSTest>
</PropertyGroup>
<PropertyGroup>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\LightSwitch.UITests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
<ProjectReference Include="..\..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="ad22010c-e33e-4459-848e-a1ed976bfd3b"
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="ad22010c-e33e-4459-848e-a1ed976bfd3b" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>LightSwitch.UITests</DisplayName>
<PublisherDisplayName>Microsoft</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="LightSwitch.UITests"
Description="LightSwitch.UITests"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace LightSwitch.UITests
{
[TestClass]
public class TestGeolocation : UITestBase
{
public TestGeolocation()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("LightSwitch.Geolocation")]
[TestCategory("Location")]
public void TestGeolocationUpdate()
{
TestHelper.InitializeTest(this, "geolocation test");
TestHelper.PerformGeolocationTest(this);
TestHelper.CleanupTest(this);
}
}
}

View File

@@ -0,0 +1,439 @@
// 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 System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Win32;
namespace LightSwitch.UITests
{
internal sealed class TestHelper
{
private static readonly string[] ShortcutSeparators = { " + ", "+", " " };
/// <summary>
/// Performs common test initialization: navigate to settings, enable toggle, verify shortcut
/// </summary>
/// <param name="testBase">The test base instance</param>
/// <param name="testName">Name of the test for assertions</param>
/// <returns>The activation keys for the test</returns>
public static Key[] InitializeTest(UITestBase testBase, string testName)
{
LaunchFromSetting(testBase);
var toggleSwitch = SetLightSwitchToggle(testBase, enable: true);
Assert.IsTrue(
toggleSwitch.IsOn,
$"Light Switch toggle switch should be ON for {testName}");
var activationKeys = ReadActivationShortcut(testBase);
Assert.IsNotNull(activationKeys, "Should be able to read activation shortcut");
Assert.IsTrue(activationKeys.Length > 0, "Activation shortcut should contain at least one key");
return activationKeys;
}
/// <summary>
/// Navigate to the Light Switch settings page
/// </summary>
public static void LaunchFromSetting(UITestBase testBase)
{
var lightSwitch = testBase.Session.FindAll<NavigationViewItem>(By.AccessibilityId("LightSwitchNavItem"));
if (lightSwitch.Count == 0)
{
testBase.Session.Find<NavigationViewItem>(By.AccessibilityId("SystemToolsNavItem"), 5000).Click(msPostAction: 500);
}
testBase.Session.Find<NavigationViewItem>(By.AccessibilityId("LightSwitchNavItem"), 5000).Click(msPostAction: 500);
}
/// <summary>
/// Set the Light Switch enable toggle switch to the specified state
/// </summary>
public static ToggleSwitch SetLightSwitchToggle(UITestBase testBase, bool enable)
{
var toggleSwitch = testBase.Session.Find<ToggleSwitch>(By.AccessibilityId("Toggle_LightSwitch"), 5000);
if (toggleSwitch.IsOn != enable)
{
toggleSwitch.Click(msPreAction: 1000, msPostAction: 2000);
}
if (toggleSwitch.IsOn != enable)
{
testBase.Session.SendKey(Key.Space, msPreAction: 0, msPostAction: 2000);
}
return toggleSwitch;
}
/// <summary>
/// Read the current activation shortcut from the ShortcutControl
/// </summary>
public static Key[] ReadActivationShortcut(UITestBase testBase)
{
var shortcutCard = testBase.Session.Find<Element>(By.AccessibilityId("Shortcut_LightSwitch"), 5000);
var shortcutButton = shortcutCard.Find<Element>(By.AccessibilityId("EditButton"), 5000);
return ParseShortcutText(shortcutButton.HelpText);
}
/// <summary>
/// Parse shortcut text like "Win + Ctrl + Shift + M" into Key array
/// </summary>
private static Key[] ParseShortcutText(string shortcutText)
{
if (string.IsNullOrEmpty(shortcutText))
{
return new Key[] { Key.Win, Key.Ctrl, Key.Shift, Key.D };
}
var keys = new List<Key>();
var parts = shortcutText.Split(ShortcutSeparators, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
var cleanPart = part.Trim().ToLowerInvariant();
var key = cleanPart switch
{
"win" or "windows" => Key.Win,
"ctrl" or "control" => Key.Ctrl,
"shift" => Key.Shift,
"alt" => Key.Alt,
_ when cleanPart.Length == 1 && char.IsLetter(cleanPart[0]) &&
cleanPart[0] >= 'a' && cleanPart[0] <= 'z' =>
(Key)Enum.Parse(typeof(Key), cleanPart.ToUpperInvariant()),
_ => (Key?)null,
};
if (key.HasValue)
{
keys.Add(key.Value);
}
}
return keys.Count > 0 ? keys.ToArray() : new Key[] { Key.Win, Key.Ctrl, Key.Shift, Key.D };
}
/// <summary>
/// Performs common test cleanup: close LightSwitch task
/// </summary>
/// <param name="testBase">The test base instance</param>
public static void CleanupTest(UITestBase testBase)
{
// TODO: Make sure the task kills?
// CloseLightSwitch(testBase);
// Ensure we're attached to settings after cleanup
try
{
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
}
catch
{
// Ignore attachment errors - this is just cleanup
}
}
/// <summary>
/// Perform a update time test operation
/// </summary>
public static void PerformUpdateTimeTest(UITestBase testBase)
{
// Make sure in manual mode
var modeCombobox = testBase.Session.Find<Element>(By.AccessibilityId("ModeSelection_LightSwitch"), 5000);
Assert.IsNotNull(modeCombobox, "Mode combobox not found.");
var neededTabs = 6;
if (modeCombobox.Text != "Manual")
{
modeCombobox.Click();
var manualListItem = testBase.Session.Find<Element>(By.AccessibilityId("ManualCBItem_LightSwitch"), 5000);
Assert.IsNotNull(manualListItem, "Manual combobox item not found.");
manualListItem.Click();
neededTabs = 1;
}
Assert.AreEqual("Manual", modeCombobox.Text, "Mode combobox should be set to Manual.");
var timeline = testBase.Session.Find<Element>(By.AccessibilityId("Timeline_LightSwitch"), 5000);
Assert.IsNotNull(timeline, "Timeline not found.");
var helpText = timeline.GetAttribute("HelpText");
string originalEndValue = GetHelpTextValue(helpText, "End");
for (int i = 0; i < neededTabs; i++)
{
testBase.Session.SendKeys(Key.Tab);
}
testBase.Session.SendKeys(Key.Enter);
testBase.Session.SendKeys(Key.Up);
testBase.Session.SendKeys(Key.Enter);
helpText = timeline.GetAttribute("HelpText");
string updatedEndValue = GetHelpTextValue(helpText, "End");
Assert.AreNotEqual(originalEndValue, updatedEndValue, "Timeline end time should have been updated.");
helpText = timeline.GetAttribute("HelpText");
string originalStartValue = GetHelpTextValue(helpText, "Start");
testBase.Session.SendKeys(Key.Tab);
testBase.Session.SendKeys(Key.Enter);
testBase.Session.SendKeys(Key.Up);
testBase.Session.SendKeys(Key.Enter);
helpText = timeline.GetAttribute("HelpText");
string updatedStartValue = GetHelpTextValue(helpText, "Start");
Assert.AreNotEqual(originalStartValue, updatedStartValue, "Timeline start time should have been updated.");
}
/// <summary>
/// Perform a update geolocation test operation
/// </summary>
public static void PerformUserSelectedLocationTest(UITestBase testBase)
{
// Make sure in sun time mode
var modeCombobox = testBase.Session.Find<Element>(By.AccessibilityId("ModeSelection_LightSwitch"), 5000);
Assert.IsNotNull(modeCombobox, "Mode combobox not found.");
if (modeCombobox.Text != "Sunset to sunrise")
{
modeCombobox.Click();
var sunriseListItem = testBase.Session.Find<Element>(By.AccessibilityId("SunCBItem_LightSwitch"), 5000);
Assert.IsNotNull(sunriseListItem, "Sunrise combobox item not found.");
sunriseListItem.Click();
}
Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise.");
var setLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000);
Assert.IsNotNull(setLocationButton, "Set location button not found.");
setLocationButton.Click();
var autoSuggestTextbox = testBase.Session.Find<Element>(By.AccessibilityId("CitySearchBox_LightSwitch"), 5000);
Assert.IsNotNull(autoSuggestTextbox, "City search box not found.");
autoSuggestTextbox.Click();
autoSuggestTextbox.SendKeys("Seattle");
autoSuggestTextbox.SendKeys(OpenQA.Selenium.Keys.Down);
autoSuggestTextbox.SendKeys(OpenQA.Selenium.Keys.Enter);
var latLong = testBase.Session.Find<Element>(By.AccessibilityId("LocationResultText_LightSwitch"), 5000);
Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text));
var sunrise = testBase.Session.Find<Element>(By.AccessibilityId("SunriseText_LightSwitch"), 5000);
Assert.IsFalse(string.IsNullOrWhiteSpace(sunrise.Text));
var sunset = testBase.Session.Find<Element>(By.AccessibilityId("SunsetText_LightSwitch"), 5000);
Assert.IsFalse(string.IsNullOrWhiteSpace(sunset.Text));
}
/// <summary>
/// Perform a update geolocation test operation
/// </summary>
public static void PerformGeolocationTest(UITestBase testBase)
{
// Make sure in sun time mode
var modeCombobox = testBase.Session.Find<Element>(By.AccessibilityId("ModeSelection_LightSwitch"), 5000);
Assert.IsNotNull(modeCombobox, "Mode combobox not found.");
if (modeCombobox.Text != "Sunset to sunrise")
{
modeCombobox.Click();
var sunriseListItem = testBase.Session.Find<Element>(By.AccessibilityId("SunCBItem_LightSwitch"), 5000);
Assert.IsNotNull(sunriseListItem, "Sunrise combobox item not found.");
sunriseListItem.Click();
}
Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise.");
// Click the select city button
var setLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000);
Assert.IsNotNull(setLocationButton, "Set location button not found.");
setLocationButton.Click(msPostAction: 8000);
var latLong = testBase.Session.Find<Element>(By.AccessibilityId("LocationResultText_LightSwitch"), 5000);
Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text));
var sunrise = testBase.Session.Find<Element>(By.AccessibilityId("SunriseText_LightSwitch"), 5000);
Assert.IsFalse(string.IsNullOrWhiteSpace(sunrise.Text));
var sunset = testBase.Session.Find<Element>(By.AccessibilityId("SunsetText_LightSwitch"), 5000);
Assert.IsFalse(string.IsNullOrWhiteSpace(sunset.Text));
}
/// <summary>
/// Perform a update time test operation
/// </summary>
public static void PerformOffsetTest(UITestBase testBase)
{
// Make sure in sun time mode
var modeCombobox = testBase.Session.Find<Element>(By.AccessibilityId("ModeSelection_LightSwitch"), 5000);
Assert.IsNotNull(modeCombobox, "Mode combobox not found.");
if (modeCombobox.Text != "Sunset to sunrise")
{
modeCombobox.Click();
var sunriseListItem = testBase.Session.Find<Element>(By.AccessibilityId("SunCBItem_LightSwitch"), 5000);
Assert.IsNotNull(sunriseListItem, "Sunrise combobox item not found.");
sunriseListItem.Click();
}
Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise.");
// Testing sunrise offset
var sunriseOffset = testBase.Session.Find<Element>(By.AccessibilityId("SunriseOffset_LightSwitch"), 5000);
Assert.IsNotNull(sunriseOffset, "Sunrise offset number box not found.");
var timeline = testBase.Session.Find<Element>(By.AccessibilityId("Timeline_LightSwitch"), 5000);
Assert.IsNotNull(timeline, "Timeline not found.");
var helpText = timeline.GetAttribute("HelpText");
string originalStartValue = GetHelpTextValue(helpText, "Start");
sunriseOffset.Click();
testBase.Session.SendKeys(Key.Up);
helpText = timeline.GetAttribute("HelpText");
string updatedStartValue = GetHelpTextValue(helpText, "Start");
Assert.AreNotEqual(originalStartValue, updatedStartValue, "Timeline start time should have been updated.");
// Testing sunset offset
var sunsetOffset = testBase.Session.Find<Element>(By.AccessibilityId("SunsetOffset_LightSwitch"), 5000);
Assert.IsNotNull(sunsetOffset, "Sunrise offset number box not found.");
helpText = timeline.GetAttribute("HelpText");
string originalEndValue = GetHelpTextValue(helpText, "End");
sunsetOffset.Click();
testBase.Session.SendKeys(Key.Up);
helpText = timeline.GetAttribute("HelpText");
string updatedEndValue = GetHelpTextValue(helpText, "End");
Assert.AreNotEqual(originalEndValue, updatedEndValue, "Timeline end time should have been updated.");
}
/// <summary>
/// Perform a test for shortcut changing themes
/// </summary>
public static void PerformShortcutTest(UITestBase testBase, Key[] activationKeys)
{
// Test when both are checked
var systemCheckbox = testBase.Session.Find<Element>(By.AccessibilityId("ChangeSystemCheckbox_LightSwitch"), 5000);
Assert.IsNotNull(systemCheckbox, "System checkbox not found.");
var scrollViewer = testBase.Session.Find<Element>(By.AccessibilityId("PageScrollViewer"));
systemCheckbox.EnsureVisible(scrollViewer);
int neededTabs = 10;
if (!systemCheckbox.Selected)
{
for (int i = 0; i < neededTabs; i++)
{
testBase.Session.SendKeys(Key.Tab);
}
systemCheckbox.Click();
}
Assert.IsTrue(systemCheckbox.Selected, "System checkbox should be checked.");
var appsCheckbox = testBase.Session.Find<Element>(By.AccessibilityId("ChangeAppsCheckbox_LightSwitch"), 5000);
Assert.IsNotNull(appsCheckbox, "Apps checkbox not found.");
if (!appsCheckbox.Selected)
{
appsCheckbox.Click();
}
Assert.IsTrue(appsCheckbox.Selected, "Apps checkbox should be checked.");
var systemBeforeValue = GetSystemTheme();
var appsBeforeValue = GetAppsTheme();
testBase.Session.SendKeys(activationKeys);
Task.Delay(5000).Wait();
var systemAfterValue = GetSystemTheme();
var appsAfterValue = GetAppsTheme();
Assert.AreNotEqual(systemBeforeValue, systemAfterValue, "System theme should have changed.");
Assert.AreNotEqual(appsBeforeValue, appsAfterValue, "Apps theme should have changed.");
// Test with nothing checked
if (systemCheckbox.Selected)
{
systemCheckbox.Click();
}
if (appsCheckbox.Selected)
{
appsCheckbox.Click();
}
Assert.IsFalse(systemCheckbox.Selected, "System checkbox should be unchecked.");
Assert.IsFalse(appsCheckbox.Selected, "Apps checkbox should be unchecked.");
var noneSystemBeforeValue = GetSystemTheme();
var noneAppsBeforeValue = GetAppsTheme();
testBase.Session.SendKeys(activationKeys);
Task.Delay(5000).Wait();
var noneSystemAfterValue = GetSystemTheme();
var noneAppsAfterValue = GetAppsTheme();
Assert.AreEqual(noneSystemBeforeValue, noneSystemAfterValue, "System theme should not have changed.");
Assert.AreEqual(noneAppsBeforeValue, noneAppsAfterValue, "Apps theme should not have changed.");
}
/* Helpers */
private static int GetSystemTheme()
{
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
if (key is null)
{
return 1;
}
return (int)key.GetValue("SystemUsesLightTheme", 1);
}
private static int GetAppsTheme()
{
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
if (key is null)
{
return 1;
}
return (int)key.GetValue("AppsUseLightTheme", 1);
}
private static string GetHelpTextValue(string helpText, string key)
{
foreach (var part in helpText.Split(';'))
{
var kv = part.Split('=');
if (kv.Length == 2 && kv[0] == key)
{
return kv[1];
}
}
return string.Empty;
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace LightSwitch.UITests
{
[TestClass]
public class TestOffset : UITestBase
{
public TestOffset()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("LightSwitch.Offset")]
[TestCategory("Time")]
public void TestTimeOffset()
{
TestHelper.InitializeTest(this, "offset test");
TestHelper.PerformOffsetTest(this);
TestHelper.CleanupTest(this);
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace LightSwitch.UITests
{
[TestClass]
public class TestShortcut : UITestBase
{
public TestShortcut()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("LightSwitch.TestShortcut")]
[TestCategory("Shortcut")]
public void TestLightSwitchShortcut()
{
var activationKeys = TestHelper.InitializeTest(this, "light switch shortcut test");
TestHelper.PerformShortcutTest(this, activationKeys);
TestHelper.CleanupTest(this);
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace LightSwitch.UITests
{
[TestClass]
public class TestUpdateManualTime : UITestBase
{
public TestUpdateManualTime()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("LightSwitch.UpdateManualTime")]
[TestCategory("Time")]
public void TestUpdateTime()
{
TestHelper.InitializeTest(this, "update manual time test");
TestHelper.PerformUpdateTimeTest(this);
TestHelper.CleanupTest(this);
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="LightSwitch.UITests.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

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

@@ -258,16 +258,6 @@ private:
{
Logger::info("AlwaysOnTop settings are empty");
}
if (!m_hotkey.key)
{
Logger::info("AlwaysOnTop is going to use default shortcut");
m_hotkey.win = true;
m_hotkey.alt = false;
m_hotkey.shift = false;
m_hotkey.ctrl = true;
m_hotkey.key = 'T';
}
}
bool is_process_running()

View File

@@ -1,5 +1,64 @@
<?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">

View File

@@ -3,12 +3,13 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.2.0" />
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.5.250829002" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0-preview.24508.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools.MSIX" Version="1.7.20250829.1" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />

View File

@@ -40,10 +40,13 @@
<ItemGroup>
<PackageReference Include="Microsoft.CommandPalette.Extensions" />
<PackageReference Include="Microsoft.Windows.CsWinRT" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Web.WebView2" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="Shmuelie.WinRTServer" />
<!-- Needed to enable building an MSIX package -->
<PackageReference Include="Microsoft.Windows.SDK.BuildTools.MSIX">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<!--
@@ -84,6 +87,9 @@
<!-- In Release builds, trimming is enabled by default.
feel free to disable this if needed -->
<PublishTrimmed>true</PublishTrimmed>
<!-- In release, also ignore the aforementioned ILLink warning -->
<ILLinkTreatWarningsAsErrors>false</ILLinkTreatWarningsAsErrors>
</PropertyGroup>

View File

@@ -2,9 +2,17 @@
// 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.UI.Xaml;
namespace Microsoft.CmdPal.UI.Helpers;
internal static class BindTransformers
{
public static bool Negate(bool value) => !value;
public static Visibility EmptyToCollapsed(string? input)
=> string.IsNullOrEmpty(input) ? Visibility.Collapsed : Visibility.Visible;
public static Visibility EmptyOrWhitespaceToCollapsed(string? input)
=> string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible;
}

View File

@@ -41,6 +41,31 @@
FalseValue="Visible"
TrueValue="Collapsed" />
<Style
x:Key="DetailKeyTextBlockStyle"
BasedOn="{StaticResource CaptionTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
</Style>
<Style
x:Key="SeparatorKeyTextBlockStyle"
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
</Style>
<Style
x:Key="DetailValueTextBlockStyle"
BasedOn="{StaticResource BodyTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
</Style>
<DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel">
<cpcontrols:Tag
HorizontalAlignment="Left"
@@ -68,7 +93,7 @@
Margin="0,3,8,0"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock Text="{x:Bind Name}" />
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind Name}" />
</StackPanel>
</Button>
</StackPanel>
@@ -76,20 +101,13 @@
<DataTemplate x:Key="DetailsLinkTemplate" x:DataType="coreViewModels:DetailsLinkViewModel">
<StackPanel Orientation="Vertical">
<TextBlock Style="{StaticResource DetailKeyTextBlockStyle}" Text="{x:Bind Key, Mode=OneWay}" />
<TextBlock
IsTextSelectionEnabled="True"
Text="{x:Bind Key, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<TextBlock
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True"
Style="{StaticResource DetailValueTextBlockStyle}"
Text="{x:Bind Text, Mode=OneWay}"
TextWrapping="WrapWholeWords"
Visibility="{x:Bind IsText, Mode=OneWay}" />
<HyperlinkButton
Padding="0"
FontSize="12"
NavigateUri="{x:Bind Link, Mode=OneWay}"
Visibility="{x:Bind IsLink, Mode=OneWay}">
<TextBlock Text="{x:Bind Text, Mode=OneWay}" TextWrapping="Wrap" />
@@ -98,10 +116,7 @@
</DataTemplate>
<DataTemplate x:Key="DetailsCommandsTemplate" x:DataType="coreViewModels:DetailsCommandsViewModel">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock
IsTextSelectionEnabled="True"
Text="{x:Bind Key, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<TextBlock Style="{StaticResource DetailKeyTextBlockStyle}" Text="{x:Bind Key, Mode=OneWay}" />
<ItemsControl
ItemTemplate="{StaticResource CommandTemplate}"
ItemsSource="{x:Bind Commands, Mode=OneWay}"
@@ -111,24 +126,20 @@
<DataTemplate x:Key="DetailsSeparatorTemplate" x:DataType="coreViewModels:DetailsSeparatorViewModel">
<StackPanel Margin="0,8,8,0" Orientation="Vertical">
<Border
Margin="8,0,0,0"
BorderBrush="{ThemeResource TextFillColorSecondaryBrush}"
Margin="0,0,0,0"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,2">
<TextBlock
Margin="-8,0,0,8"
FontWeight="SemiBold"
IsTextSelectionEnabled="True"
Margin="0,0,0,0"
Style="{StaticResource SeparatorKeyTextBlockStyle}"
Text="{x:Bind Key, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
Visibility="{x:Bind help:BindTransformers.EmptyOrWhitespaceToCollapsed(Key), FallbackValue=Collapsed}" />
</Border>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="DetailsTagsTemplate" x:DataType="coreViewModels:DetailsTagsViewModel">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock
IsTextSelectionEnabled="True"
Text="{x:Bind Key, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
<TextBlock Style="{StaticResource DetailKeyTextBlockStyle}" Text="{x:Bind Key, Mode=OneWay}" />
<ItemsControl
ItemTemplate="{StaticResource TagTemplate}"
ItemsSource="{x:Bind Tags, Mode=OneWay}"

View File

@@ -24,10 +24,11 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Name="TitleBar">
<TitleBar x:Name="AppTitleBar" PaneToggleRequested="AppTitleBar_PaneToggleRequested">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
x:Name="WorkAroundIcon"
Height="16"
Margin="16,0,0,0"
Source="ms-appx:///Assets/icon.svg" />
@@ -36,18 +37,19 @@
<NavigationView
x:Name="NavView"
Grid.Row="1"
CompactModeThresholdWidth="1007"
DisplayModeChanged="NavView_DisplayModeChanged"
ExpandedModeThresholdWidth="1007"
IsBackButtonVisible="Collapsed"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
ItemInvoked="NavView_ItemInvoked"
Loaded="NavView_Loaded"
OpenPaneLength="200">
Loaded="NavView_Loaded">
<NavigationView.Resources>
<SolidColorBrush x:Key="NavigationViewContentBackground" Color="Transparent" />
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
<Thickness x:Key="NavigationViewHeaderMargin">15,0,0,0</Thickness>
</NavigationView.Resources>
<NavigationView.MenuItems>
<NavigationViewItem
x:Uid="Settings_GeneralPage_NavigationViewItem_General"
@@ -58,34 +60,32 @@
Icon="{ui:FontIcon Glyph=&#xEA86;}"
Tag="Extensions" />
</NavigationView.MenuItems>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<BreadcrumbBar
x:Name="NavigationBreadcrumbBar"
Grid.Row="0"
MaxWidth="1000"
ItemClicked="NavigationBreadcrumbBar_ItemClicked"
ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}">
<BreadcrumbBar.ItemTemplate>
<DataTemplate x:DataType="local:Crumb">
<TextBlock Text="{x:Bind Label, Mode=OneWay}" />
</DataTemplate>
</BreadcrumbBar.ItemTemplate>
<BreadcrumbBar.Resources>
<ResourceDictionary>
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">28</x:Double>
<Thickness x:Key="BreadcrumbBarChevronPadding">7,4,8,0</Thickness>
<FontWeight x:Key="BreadcrumbBarItemFontWeight">SemiBold</FontWeight>
<x:Double x:Key="BreadcrumbBarChevronFontSize">16</x:Double>
</ResourceDictionary>
</BreadcrumbBar.Resources>
</BreadcrumbBar>
<Grid Padding="16,0">
<BreadcrumbBar
x:Name="NavigationBreadcrumbBar"
MaxWidth="1000"
ItemClicked="NavigationBreadcrumbBar_ItemClicked"
ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}">
<BreadcrumbBar.ItemTemplate>
<DataTemplate x:DataType="local:Crumb">
<TextBlock Text="{x:Bind Label, Mode=OneWay}" />
</DataTemplate>
</BreadcrumbBar.ItemTemplate>
<BreadcrumbBar.Resources>
<ResourceDictionary>
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">28</x:Double>
<Thickness x:Key="BreadcrumbBarChevronPadding">7,4,8,0</Thickness>
<FontWeight x:Key="BreadcrumbBarItemFontWeight">SemiBold</FontWeight>
<x:Double x:Key="BreadcrumbBarChevronFontSize">16</x:Double>
</ResourceDictionary>
</BreadcrumbBar.Resources>
</BreadcrumbBar>
</Grid>
<Frame x:Name="NavFrame" Grid.Row="1" />
</Grid>
</NavigationView>

View File

@@ -14,6 +14,7 @@ using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using WinUIEx;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
using TitleBar = Microsoft.UI.Xaml.Controls.TitleBar;
namespace Microsoft.CmdPal.UI.Settings;
@@ -34,7 +35,7 @@ public sealed partial class SettingsWindow : WindowEx,
var title = RS_.GetString("SettingsWindowTitle");
this.AppWindow.Title = title;
this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
this.TitleBar.Title = title;
this.AppTitleBar.Title = title;
PositionCentered();
WeakReferenceMessenger.Default.Register<NavigateToExtensionSettingsMessage>(this);
@@ -142,11 +143,13 @@ public sealed partial class SettingsWindow : WindowEx,
{
if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal)
{
NavView.IsPaneToggleButtonVisible = false;
AppTitleBar.IsPaneToggleButtonVisible = true;
WorkAroundIcon.Margin = new Thickness(8, 0, 16, 0); // Required for workaround, see XAML comment
}
else
{
NavView.IsPaneToggleButtonVisible = true;
AppTitleBar.IsPaneToggleButtonVisible = false;
WorkAroundIcon.Margin = new Thickness(16, 0, 0, 0); // Required for workaround, see XAML comment
}
}
@@ -155,6 +158,11 @@ public sealed partial class SettingsWindow : WindowEx,
// This might come in on a background thread
DispatcherQueue.TryEnqueue(() => Close());
}
private void AppTitleBar_PaneToggleRequested(TitleBar sender, object args)
{
NavView.IsPaneOpen = !NavView.IsPaneOpen;
}
}
public readonly struct Crumb

View File

@@ -0,0 +1,19 @@
<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>
<RootNamespace>Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests</RootNamespace>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,274 @@
// 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.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests;
[TestClass]
public class UrlHelperTests
{
[TestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[DataRow("\t")]
[DataRow("\r\n")]
public void IsValidUrl_ReturnsFalse_WhenUrlIsNullOrWhitespace(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
[DataRow("test\nurl")]
[DataRow("test\rurl")]
[DataRow("http://example.com\nmalicious")]
[DataRow("https://test.com\r\nheader")]
public void IsValidUrl_ReturnsFalse_WhenUrlContainsNewlines(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
[DataRow("com")]
[DataRow("org")]
[DataRow("localhost")]
[DataRow("test")]
[DataRow("http")]
[DataRow("https")]
public void IsValidUrl_ReturnsFalse_WhenUrlDoesNotContainDot(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
[DataRow("https://www.example.com")]
[DataRow("http://test.org")]
[DataRow("ftp://files.example.net")]
[DataRow("file://localhost/path/to/file.txt")]
[DataRow("https://subdomain.example.co.uk")]
[DataRow("http://192.168.1.1")]
[DataRow("https://example.com:8080/path")]
public void IsValidUrl_ReturnsTrue_WhenUrlIsWellFormedAbsolute(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow("www.example.com")]
[DataRow("example.org")]
[DataRow("subdomain.test.net")]
[DataRow("github.com/user/repo")]
[DataRow("stackoverflow.com/questions/123")]
[DataRow("192.168.1.1")]
public void IsValidUrl_ReturnsTrue_WhenUrlIsValidWithoutProtocol(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow("not a url")]
[DataRow("invalid..url")]
[DataRow("http://")]
[DataRow("https://")]
[DataRow("://example.com")]
[DataRow("ht tp://example.com")]
public void IsValidUrl_ReturnsFalse_WhenUrlIsInvalid(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
[DataRow(" https://www.example.com ")]
[DataRow("\t\tgithub.com\t\t")]
[DataRow(" \r\n stackoverflow.com \r\n ")]
public void IsValidUrl_TrimsWhitespace_BeforeValidation(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow("tel:+1234567890")]
[DataRow("javascript:alert('test')")]
public void IsValidUrl_ReturnsFalse_ForNonWebProtocols(string url)
{
// Act
var result = UrlHelper.IsValidUrl(url);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void NormalizeUrl_ReturnsInput_WhenUrlIsNullOrWhitespace(string url)
{
// Act
var result = UrlHelper.NormalizeUrl(url);
// Assert
Assert.AreEqual(url, result);
}
[TestMethod]
[DataRow("https://www.example.com")]
[DataRow("http://test.org")]
[DataRow("ftp://files.example.net")]
[DataRow("file://localhost/path/to/file.txt")]
public void NormalizeUrl_ReturnsUnchanged_WhenUrlIsAlreadyWellFormed(string url)
{
// Act
var result = UrlHelper.NormalizeUrl(url);
// Assert
Assert.AreEqual(url, result);
}
[TestMethod]
[DataRow("www.example.com", "https://www.example.com")]
[DataRow("example.org", "https://example.org")]
[DataRow("github.com/user/repo", "https://github.com/user/repo")]
[DataRow("stackoverflow.com/questions/123", "https://stackoverflow.com/questions/123")]
public void NormalizeUrl_AddsHttpsPrefix_WhenNoProtocolPresent(string input, string expected)
{
// Act
var result = UrlHelper.NormalizeUrl(input);
// Assert
Assert.AreEqual(expected, result);
}
[TestMethod]
[DataRow(" www.example.com ", "https://www.example.com")]
[DataRow("\t\tgithub.com\t\t", "https://github.com")]
[DataRow(" \r\n stackoverflow.com \r\n ", "https://stackoverflow.com")]
public void NormalizeUrl_TrimsWhitespace_BeforeNormalizing(string input, string expected)
{
// Act
var result = UrlHelper.NormalizeUrl(input);
// Assert
Assert.AreEqual(expected, result);
}
[TestMethod]
[DataRow(@"C:\Users\Test\Documents\file.txt")]
[DataRow(@"D:\Projects\MyProject\readme.md")]
[DataRow(@"E:\")]
[DataRow(@"F:")]
[DataRow(@"G:\folder\subfolder")]
public void IsValidUrl_ReturnsTrue_ForValidLocalPaths(string path)
{
// Act
var result = UrlHelper.IsValidUrl(path);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow(@"\\server\share")]
[DataRow(@"\\server\share\folder")]
[DataRow(@"\\192.168.1.100\public")]
[DataRow(@"\\myserver\documents\file.docx")]
[DataRow(@"\\domain.com\share\folder\file.pdf")]
public void IsValidUrl_ReturnsTrue_ForValidNetworkPaths(string path)
{
// Act
var result = UrlHelper.IsValidUrl(path);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow(@"\\")]
[DataRow(@":")]
[DataRow(@"Z")]
[DataRow(@"folder")]
[DataRow(@"folder\file.txt")]
[DataRow(@"documents\project\readme.md")]
[DataRow(@"./config/settings.json")]
[DataRow(@"../data/input.csv")]
public void IsValidUrl_ReturnsFalse_ForInvalidPathsAndRelativePaths(string path)
{
// Act
var result = UrlHelper.IsValidUrl(path);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
[DataRow(@"C:\Users\Test\Documents\file.txt")]
[DataRow(@"D:\Projects\MyProject")]
[DataRow(@"E:\")]
public void NormalizeUrl_ConvertsLocalPathToFileUri_WhenValidLocalPath(string path)
{
// Act
var result = UrlHelper.NormalizeUrl(path);
// Assert
Assert.IsTrue(result.StartsWith("file:///", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(result.Contains(path.Replace('\\', '/')));
}
[TestMethod]
[DataRow(@"\\server\share")]
[DataRow(@"\\192.168.1.100\public")]
[DataRow(@"\\myserver\documents")]
public void NormalizeUrl_ConvertsNetworkPathToFileUri_WhenValidNetworkPath(string path)
{
// Act
var result = UrlHelper.NormalizeUrl(path);
// Assert
Assert.IsTrue(result.StartsWith("file://", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(result.Contains(path.Replace('\\', '/')));
}
[TestMethod]
[DataRow("file:///C:/Users/Test/file.txt")]
[DataRow("file://server/share/folder")]
public void NormalizeUrl_ReturnsUnchanged_WhenAlreadyFileUri(string fileUri)
{
// Act
var result = UrlHelper.NormalizeUrl(fileUri);
// Assert
Assert.AreEqual(fileUri, result);
}
}

View File

@@ -23,6 +23,7 @@ public class NormalizeCommandLineTests : CommandPaletteUnitTestBase
[DataRow("ping bing.com", "c:\\Windows\\system32\\ping.exe", "bing.com")]
[DataRow("curl bing.com", "c:\\Windows\\system32\\curl.exe", "bing.com")]
[DataRow("ipconfig /all", "c:\\Windows\\system32\\ipconfig.exe", "/all")]
[DataRow("ipconfig a b \"c d\"", "c:\\Windows\\system32\\ipconfig.exe", "a b \"c d\"")]
public void NormalizeCommandLineSimple(string input, string expectedExe, string expectedArgs = "")
{
NormalizeTestCore(input, expectedExe, expectedArgs);
@@ -46,7 +47,7 @@ public class NormalizeCommandLineTests : CommandPaletteUnitTestBase
[TestMethod]
[DataRow("cmd --run --test", "C:\\Windows\\System32\\cmd.exe", "--run --test")]
[DataRow("cmd --run --test ", "C:\\Windows\\System32\\cmd.exe", "--run --test")]
[DataRow("cmd \"--run --test\" --pass", "C:\\Windows\\System32\\cmd.exe", "--run --test --pass")]
[DataRow("cmd \"--run --test\" --pass", "C:\\Windows\\System32\\cmd.exe", "\"--run --test\" --pass")]
public void NormalizeArgsWithSpaces(string input, string expectedExe, string expectedArgs = "")
{
NormalizeTestCore(input, expectedExe, expectedArgs);

View File

@@ -5,7 +5,7 @@
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
<VersionMajor>0</VersionMajor>
<VersionMinor>5</VersionMinor>
<VersionMinor>6</VersionMinor>
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,144 @@
// 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 Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
internal static class UrlHelper
{
/// <summary>
/// Validates if a string is a valid URL or file path
/// </summary>
/// <param name="url">The string to validate</param>
/// <returns>True if the string is a valid URL or file path, false otherwise</returns>
internal static bool IsValidUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
return false;
}
// Trim whitespace for validation
url = url.Trim();
// URLs should not contain newlines
if (url.Contains('\n', StringComparison.Ordinal) || url.Contains('\r', StringComparison.Ordinal))
{
return false;
}
// Check if it's a valid file path (local or network)
if (IsValidFilePath(url))
{
return true;
}
if (!url.Contains('.', StringComparison.OrdinalIgnoreCase))
{
// eg: 'com', 'org'. We don't think it's a valid url.
// This can simplify the logic of checking if the url is valid.
return false;
}
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
return true;
}
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
if (Uri.IsWellFormedUriString("https://" + url, UriKind.Absolute))
{
return true;
}
}
return false;
}
/// <summary>
/// Normalizes a URL or file path by adding appropriate schema if none is present
/// </summary>
/// <param name="url">The URL or file path to normalize</param>
/// <returns>Normalized URL or file path with schema</returns>
internal static string NormalizeUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
return url;
}
// Trim whitespace
url = url.Trim();
// If it's a valid file path, convert to file:// URI
if (IsValidFilePath(url) && !url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
try
{
// Convert to file URI (path is already absolute since we only accept absolute paths)
return new Uri(url).ToString();
}
catch
{
// If conversion fails, return original
return url;
}
}
if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
url = "https://" + url;
}
}
return url;
}
/// <summary>
/// Checks if a string represents a valid file path (local or network)
/// </summary>
/// <param name="path">The string to check</param>
/// <returns>True if the string is a valid file path, false otherwise</returns>
private static bool IsValidFilePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
try
{
// Check for UNC paths (network paths starting with \\)
if (path.StartsWith(@"\\", StringComparison.Ordinal))
{
// Basic UNC path validation: \\server\share or \\server\share\path
var parts = path.Substring(2).Split('\\', StringSplitOptions.RemoveEmptyEntries);
return parts.Length >= 2; // At minimum: server and share
}
// Check for drive letters (C:\ or C:)
if (path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':')
{
return true;
}
return false;
}
catch
{
return false;
}
}
}

View File

@@ -2,11 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
@@ -16,4 +11,6 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory;
internal static class KeyChords
{
internal static KeyChord DeleteEntry { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete);
internal static KeyChord OpenUrl { get; } = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.O);
}

View File

@@ -22,6 +22,7 @@ internal sealed partial class ClipboardListItem : ListItem
private readonly CommandContextItem _deleteContextMenuItem;
private readonly CommandContextItem? _pasteCommand;
private readonly CommandContextItem? _copyCommand;
private readonly CommandContextItem? _openUrlCommand;
private readonly Lazy<Details> _lazyDetails;
public override IDetails? Details
@@ -72,11 +73,26 @@ internal sealed partial class ClipboardListItem : ListItem
_pasteCommand = new CommandContextItem(new PasteCommand(_item, ClipboardFormat.Text, _settingsManager));
_copyCommand = new CommandContextItem(new CopyCommand(_item, ClipboardFormat.Text));
// Check if the text content is a valid URL and add OpenUrl command
if (UrlHelper.IsValidUrl(_item.Content ?? string.Empty))
{
var normalizedUrl = UrlHelper.NormalizeUrl(_item.Content ?? string.Empty);
_openUrlCommand = new CommandContextItem(new OpenUrlCommand(normalizedUrl))
{
RequestedShortcut = KeyChords.OpenUrl,
};
}
else
{
_openUrlCommand = null;
}
}
else
{
_pasteCommand = null;
_copyCommand = null;
_openUrlCommand = null;
}
RefreshCommands();
@@ -99,12 +115,7 @@ internal sealed partial class ClipboardListItem : ListItem
{
case PrimaryAction.Paste:
Command = _pasteCommand?.Command;
MoreCommands =
[
_copyCommand!,
new Separator(),
_deleteContextMenuItem,
];
MoreCommands = BuildMoreCommands(_copyCommand);
if (_item.IsText)
{
@@ -124,12 +135,7 @@ internal sealed partial class ClipboardListItem : ListItem
case PrimaryAction.Copy:
default:
Command = _copyCommand?.Command;
MoreCommands =
[
_pasteCommand!,
new Separator(),
_deleteContextMenuItem,
];
MoreCommands = BuildMoreCommands(_pasteCommand);
if (_item.IsText)
{
@@ -148,6 +154,26 @@ internal sealed partial class ClipboardListItem : ListItem
}
}
private IContextItem[] BuildMoreCommands(CommandContextItem? firstCommand)
{
var commands = new List<IContextItem>();
if (firstCommand != null)
{
commands.Add(firstCommand);
}
if (_openUrlCommand != null)
{
commands.Add(_openUrlCommand);
}
commands.Add(new Separator());
commands.Add(_deleteContextMenuItem);
return commands.ToArray();
}
private Details CreateDetails()
{
IDetailsElement[] metadata =

View File

@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests")]

View File

@@ -183,4 +183,7 @@
<data name="settings_primary_action_copy" xml:space="preserve">
<value>Copy to Clipboard</value>
</data>
<data name="open_url_command_name" xml:space="preserve">
<value>Open URL</value>
</data>
</root>

View File

@@ -157,7 +157,98 @@ public class ShellListPageHelpers
executable = segments[0];
if (segments.Length > 1)
{
arguments = string.Join(' ', segments[1..]);
arguments = ArgumentBuilder.BuildArguments(segments[1..]);
}
}
private static class ArgumentBuilder
{
internal static string BuildArguments(string[] arguments)
{
if (arguments.Length <= 0)
{
return string.Empty;
}
var stringBuilder = new StringBuilder();
foreach (var argument in arguments)
{
AppendArgument(stringBuilder, argument);
}
return stringBuilder.ToString();
}
private static void AppendArgument(StringBuilder stringBuilder, string argument)
{
if (stringBuilder.Length > 0)
{
stringBuilder.Append(' ');
}
if (argument.Length == 0 || ShouldBeQuoted(argument))
{
stringBuilder.Append('\"');
var index = 0;
while (index < argument.Length)
{
var c = argument[index++];
if (c == '\\')
{
var numBackSlash = 1;
while (index < argument.Length && argument[index] == '\\')
{
index++;
numBackSlash++;
}
if (index == argument.Length)
{
stringBuilder.Append('\\', numBackSlash * 2);
}
else if (argument[index] == '\"')
{
stringBuilder.Append('\\', (numBackSlash * 2) + 1);
stringBuilder.Append('\"');
index++;
}
else
{
stringBuilder.Append('\\', numBackSlash);
}
continue;
}
if (c == '\"')
{
stringBuilder.Append('\\');
stringBuilder.Append('\"');
continue;
}
stringBuilder.Append(c);
}
stringBuilder.Append('\"');
}
else
{
stringBuilder.Append(argument);
}
}
private static bool ShouldBeQuoted(string s)
{
foreach (var c in s)
{
if (char.IsWhiteSpace(c) || c == '\"')
{
return true;
}
}
return false;
}
}
}

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