Squashed commit of the following:

commit 32c13cead4
Author: Jiří Polášek <me@jiripolasek.com>
Date:   Mon Dec 1 20:24:54 2025 +0100

    CmdPal: Remove fallbacks from the home page when there's no query (#44005)

    This PR stops fallback commands from showing on the homepage when
    there’s no query, with the reasons detailed in the linked issue.

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

    - [x] Closes: #44004
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 33808fdb9c
Author: Jiří Polášek <me@jiripolasek.com>
Date:   Mon Dec 1 20:23:48 2025 +0100

    CmdPal: Hide RDC fallback item by default (#43994)

    This fixes few nits with RDC extension:
    - hides the RDC fallback item from the home page when there’s no query;
    - fixes MSTSC process working directory (must physically exists or be an
    empty string)

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit f510be4c53
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Dec 1 22:29:21 2025 +0800

    Build(deps): Bump actions/checkout from 3 to 6 (#43838)

    Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to
    6.
    <details>
    <summary>Release notes</summary>
    <p><em>Sourced from <a
    href="https://github.com/actions/checkout/releases">actions/checkout's
    releases</a>.</em></p>
    <blockquote>
    <h2>v6.0.0</h2>
    <h2>What's Changed</h2>
    <ul>
    <li>Update README to include Node.js 24 support details and requirements
    by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
    in <a
    href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
    <li>Persist creds to a separate file by <a
    href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
    <li>v6-beta by <a
    href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2298">actions/checkout#2298</a></li>
    <li>update readme/changelog for v6 by <a
    href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2311">actions/checkout#2311</a></li>
    </ul>
    <p><strong>Full Changelog</strong>: <a
    href="https://github.com/actions/checkout/compare/v5.0.0...v6.0.0">https://github.com/actions/checkout/compare/v5.0.0...v6.0.0</a></p>
    <h2>v6-beta</h2>
    <h2>What's Changed</h2>
    <p>Updated persist-credentials to store the credentials under
    <code>$RUNNER_TEMP</code> instead of directly in the local git
    config.</p>
    <p>This requires a minimum Actions Runner version of <a
    href="https://github.com/actions/runner/releases/tag/v2.329.0">v2.329.0</a>
    to access the persisted credentials for <a
    href="https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action">Docker
    container action</a> scenarios.</p>
    <h2>v5.0.1</h2>
    <h2>What's Changed</h2>
    <ul>
    <li>Port v6 cleanup to v5 by <a
    href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
    </ul>
    <p><strong>Full Changelog</strong>: <a
    href="https://github.com/actions/checkout/compare/v5...v5.0.1">https://github.com/actions/checkout/compare/v5...v5.0.1</a></p>
    <h2>v5.0.0</h2>
    <h2>What's Changed</h2>
    <ul>
    <li>Update actions checkout to use node 24 by <a
    href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
    <li>Prepare v5.0.0 release by <a
    href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2238">actions/checkout#2238</a></li>
    </ul>
    <h2>⚠️ Minimum Compatible Runner Version</h2>
    <p><strong>v2.327.1</strong><br />
    <a
    href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
    Notes</a></p>
    <p>Make sure your runner is updated to this version or newer to use this
    release.</p>
    <p><strong>Full Changelog</strong>: <a
    href="https://github.com/actions/checkout/compare/v4...v5.0.0">https://github.com/actions/checkout/compare/v4...v5.0.0</a></p>
    <h2>v4.3.1</h2>
    <h2>What's Changed</h2>
    <ul>
    <li>Port v6 cleanup to v4 by <a
    href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
    </ul>
    <p><strong>Full Changelog</strong>: <a
    href="https://github.com/actions/checkout/compare/v4...v4.3.1">https://github.com/actions/checkout/compare/v4...v4.3.1</a></p>
    <h2>v4.3.0</h2>
    <h2>What's Changed</h2>
    <ul>
    <li>docs: update README.md by <a
    href="https://github.com/motss"><code>@​motss</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
    <li>Add internal repos for checking out multiple repositories by <a
    href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
    <li>Documentation update - add recommended permissions to Readme by <a
    href="https://github.com/benwells"><code>@​benwells</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
    </ul>
    <!-- raw HTML omitted -->
    </blockquote>
    <p>... (truncated)</p>
    </details>
    <details>
    <summary>Changelog</summary>
    <p><em>Sourced from <a
    href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
    changelog</a>.</em></p>
    <blockquote>
    <h1>Changelog</h1>
    <h2>V6.0.0</h2>
    <ul>
    <li>Persist creds to a separate file by <a
    href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
    <li>Update README to include Node.js 24 support details and requirements
    by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
    in <a
    href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
    </ul>
    <h2>V5.0.1</h2>
    <ul>
    <li>Port v6 cleanup to v5 by <a
    href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
    </ul>
    <h2>V5.0.0</h2>
    <ul>
    <li>Update actions checkout to use node 24 by <a
    href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
    </ul>
    <h2>V4.3.1</h2>
    <ul>
    <li>Port v6 cleanup to v4 by <a
    href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
    </ul>
    <h2>V4.3.0</h2>
    <ul>
    <li>docs: update README.md by <a
    href="https://github.com/motss"><code>@​motss</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
    <li>Add internal repos for checking out multiple repositories by <a
    href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
    <li>Documentation update - add recommended permissions to Readme by <a
    href="https://github.com/benwells"><code>@​benwells</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
    <li>Adjust positioning of user email note and permissions heading by <a
    href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
    <li>Update README.md by <a
    href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
    <li>Update CODEOWNERS for actions by <a
    href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
    in <a
    href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
    <li>Update package dependencies by <a
    href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
    </ul>
    <h2>v4.2.2</h2>
    <ul>
    <li><code>url-helper.ts</code> now leverages well-known environment
    variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
    in <a
    href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
    <li>Expand unit test coverage for <code>isGhes</code> by <a
    href="https://github.com/jww3"><code>@​jww3</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
    </ul>
    <h2>v4.2.1</h2>
    <ul>
    <li>Check out other refs/* by commit if provided, fall back to ref by <a
    href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
    </ul>
    <h2>v4.2.0</h2>
    <ul>
    <li>Add Ref and Commit outputs by <a
    href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
    <li>Dependency updates by <a
    href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
    href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
    <a
    href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
    </ul>
    <h2>v4.1.7</h2>
    <ul>
    <li>Bump the minor-npm-dependencies group across 1 directory with 4
    updates by <a
    href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
    <li>Bump actions/checkout from 3 to 4 by <a
    href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
    <li>Check out other refs/* by commit by <a
    href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
    <li>Pin actions/checkout's own workflows to a known, good, stable
    version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
    <a
    href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
    </ul>
    <h2>v4.1.6</h2>
    <ul>
    <li>Check platform to set archive extension appropriately by <a
    href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
    <a
    href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
    </ul>
    <h2>v4.1.5</h2>
    <ul>
    <li>Update NPM dependencies by <a
    href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
    <a
    href="https://redirect.github.com/actions/checkout/pull/1703">actions/checkout#1703</a></li>
    <li>Bump github/codeql-action from 2 to 3 by <a
    href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1694">actions/checkout#1694</a></li>
    <li>Bump actions/setup-node from 1 to 4 by <a
    href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1696">actions/checkout#1696</a></li>
    <li>Bump actions/upload-artifact from 2 to 4 by <a
    href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
    href="https://redirect.github.com/actions/checkout/pull/1695">actions/checkout#1695</a></li>
    </ul>
    <!-- raw HTML omitted -->
    </blockquote>
    <p>... (truncated)</p>
    </details>
    <details>
    <summary>Commits</summary>
    <ul>
    <li><a
    href="1af3b93b68"><code>1af3b93</code></a>
    update readme/changelog for v6 (<a
    href="https://redirect.github.com/actions/checkout/issues/2311">#2311</a>)</li>
    <li><a
    href="71cf2267d8"><code>71cf226</code></a>
    v6-beta (<a
    href="https://redirect.github.com/actions/checkout/issues/2298">#2298</a>)</li>
    <li><a
    href="069c695914"><code>069c695</code></a>
    Persist creds to a separate file (<a
    href="https://redirect.github.com/actions/checkout/issues/2286">#2286</a>)</li>
    <li><a
    href="ff7abcd0c3"><code>ff7abcd</code></a>
    Update README to include Node.js 24 support details and requirements (<a
    href="https://redirect.github.com/actions/checkout/issues/2248">#2248</a>)</li>
    <li><a
    href="08c6903cd8"><code>08c6903</code></a>
    Prepare v5.0.0 release (<a
    href="https://redirect.github.com/actions/checkout/issues/2238">#2238</a>)</li>
    <li><a
    href="9f265659d3"><code>9f26565</code></a>
    Update actions checkout to use node 24 (<a
    href="https://redirect.github.com/actions/checkout/issues/2226">#2226</a>)</li>
    <li><a
    href="08eba0b27e"><code>08eba0b</code></a>
    Prepare release v4.3.0 (<a
    href="https://redirect.github.com/actions/checkout/issues/2237">#2237</a>)</li>
    <li><a
    href="631c7dc4f8"><code>631c7dc</code></a>
    Update package dependencies (<a
    href="https://redirect.github.com/actions/checkout/issues/2236">#2236</a>)</li>
    <li><a
    href="8edcb1bdb4"><code>8edcb1b</code></a>
    Update CODEOWNERS for actions (<a
    href="https://redirect.github.com/actions/checkout/issues/2224">#2224</a>)</li>
    <li><a
    href="09d2acae67"><code>09d2aca</code></a>
    Update README.md (<a
    href="https://redirect.github.com/actions/checkout/issues/2194">#2194</a>)</li>
    <li>Additional commits viewable in <a
    href="https://github.com/actions/checkout/compare/v3...v6">compare
    view</a></li>
    </ul>
    </details>
    <br />

    [![Dependabot compatibility
    score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=3&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

    Dependabot will resolve any conflicts with this PR as long as you don't
    alter it yourself. You can also trigger a rebase manually by commenting
    `@dependabot rebase`.

    [//]: # (dependabot-automerge-start)
    [//]: # (dependabot-automerge-end)

    ---

    <details>
    <summary>Dependabot commands and options</summary>
    <br />

    You can trigger Dependabot actions by commenting on this PR:
    - `@dependabot rebase` will rebase this PR
    - `@dependabot recreate` will recreate this PR, overwriting any edits
    that have been made to it
    - `@dependabot merge` will merge this PR after your CI passes on it
    - `@dependabot squash and merge` will squash and merge this PR after
    your CI passes on it
    - `@dependabot cancel merge` will cancel a previously requested merge
    and block automerging
    - `@dependabot reopen` will reopen this PR if it is closed
    - `@dependabot close` will close this PR and stop Dependabot recreating
    it. You can achieve the same result by closing it manually
    - `@dependabot show <dependency name> ignore conditions` will show all
    of the ignore conditions of the specified dependency
    - `@dependabot ignore this major version` will close this PR and stop
    Dependabot creating any more for this major version (unless you reopen
    the PR or upgrade to it yourself)
    - `@dependabot ignore this minor version` will close this PR and stop
    Dependabot creating any more for this minor version (unless you reopen
    the PR or upgrade to it yourself)
    - `@dependabot ignore this dependency` will close this PR and stop
    Dependabot creating any more for this dependency (unless you reopen the
    PR or upgrade to it yourself)

    </details>

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 4d3c223402
Author: Jiří Polášek <me@jiripolasek.com>
Date:   Mon Dec 1 02:32:30 2025 +0100

    CmdPal: Fix grid views (#43991)

    This PR fixes the crash due to binding to a trimmed property. For this
    it converts runtime bindings on GridView to use `{x:Bind}` so this issue
    can't happen in the future.

    - Fixes a crash related to the `Visibility` property in gallery/grid
    views when trimmed during AOT builds.
    - Fixes ShowTitle and ShowSubtitle properties, they are now taken into
    account in a view.
    - Improves UI layout, removes some margins and maches the corner radius
    of the item contaienr with the item content in the gallery view.
    - Refactores gallery and grid views to move logic from the view to the
    view model so we can x:Bind to them.
    - Replaces `{Binding}` with `{x:Bind}` to improve performance and enable
    compile-time binding validation.
    - Properties related to grids are splatted on to the common
    `IGridPropertiesViewModel` interface. Subclassing would add extra
    overhead without substential benefit.
    - Adds new samples to showcase various grid view configurations.

    A) Gallery view (with title and subtitle)
    <img width="909" height="583" alt="image"
    src="https://github.com/user-attachments/assets/b807e7a8-412f-4817-8121-e3470c49e0c0"
    />

    B) Gallery view (only title)
    <img width="903" height="582" alt="image"
    src="https://github.com/user-attachments/assets/b619d63f-04d0-42f2-9207-de256dc5e481"
    />

    C) Gallery view (no title or subtitle)
    <img width="900" height="583" alt="image"
    src="https://github.com/user-attachments/assets/c48cd1fc-8f51-40c1-8bce-607916e9f742"
    />

    D) Small icons
    <img width="907" height="582" alt="image"
    src="https://github.com/user-attachments/assets/8327da0a-fa45-443f-b52c-f0f1edd7b861"
    />

    E) Medium icons (with labels)
    <img width="914" height="588" alt="image"
    src="https://github.com/user-attachments/assets/dee9fab1-54e8-45f8-96d7-502b121a6ac2"
    />

    F) Medium icons (no labels)
    <img width="915" height="588" alt="image"
    src="https://github.com/user-attachments/assets/a32e8af2-6cb1-4106-91db-ca396253c0a3"
    />

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

    - [x] Closes: #43973
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 1ba5a258e9
Author: Jiří Polášek <me@jiripolasek.com>
Date:   Sun Nov 30 01:59:58 2025 +0100

    CmdPal: Add custom search engine option to Web Search extension (#43941)

    This PR allows user to customize a search query in Command Palette's Web
    Search built-in extension. This will also solve a problem with some
    browser that doesn't handle argument in form "? <query>" as it will
    allow user to specify the complete URI.

    - Introduces a new text box in Web Search extension settings for
    specifying a custom search engine URI
    - If the text box is non-empty, the provided URI is used for queries
    - If left empty, the extension defaults to previous behavior, sending
    queries in the format "? query"

    <img width="825" height="566" alt="image"
    src="https://github.com/user-attachments/assets/fbf3d3a5-ebfe-4c16-a5f1-0d044b6f9047"
    />

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

    - [x] Closes: #43940
    - [x] Closes: #42867
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 8aea589b01
Author: Jiří Polášek <me@jiripolasek.com>
Date:   Sun Nov 30 00:36:55 2025 +0100

    CmdPal: Align spellchecker and naming to .NET guidelines (#43974)

    - Add command-line parameter value (icf)
    - Unify file and class name casing to match .NET naming conventions (RDP
    -> Rdp as Url, Dns, Xml) -- fixes IRDP spellchecking error
    - Rename IRdpConnectionManager to IRdpConnectionsManager (*s) to match
    the class name

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit afd9d4cc3c
Author: Clint Rutkas <clint@rutkas.com>
Date:   Sat Nov 29 15:11:17 2025 -0800

    Update PowerToys download links to version 0.96.1 (#43965)

    <!-- 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)?
    -->

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit bc0a760aff
Author: Jiří Polášek <me@jiripolasek.com>
Date:   Sat Nov 29 23:23:24 2025 +0100

    CmdPal: Add mini dev center (#43939)

    This PR introduces a small ribbon to the CmdPal for app developers. The
    dev ribbon is dynamically added to the main window in local (non-CI)
    builds. It shows the number of logged errors and warnings, the current
    build configuration (Debug or Release), and whether it’s built with AOT.

    The flyout shows the latest errors and warnings and lets you quickly
    access the logs.

    <img width="985" height="589" alt="image"
    src="https://github.com/user-attachments/assets/6528b02b-b4b4-4968-91bf-e67a29f86415"
    />

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

    - [x] Closes: #43318
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 06afe09973
Author: Michael Jolley <mike@baldbeardedbuilder.com>
Date:   Sat Nov 29 13:07:19 2025 -0600

    CmdPal: New Remote Desktop built-in extension (#43090)

    This PR introduces a new built-in extension for Remote Desktop users.

    It allows you to view past RDP connections, save predefined connections,
    and connect to any of them. Or start a new RDP connection.

    https://github.com/user-attachments/assets/6a5041a6-5741-4df0-a305-da7166f962e1

    Closes #38305

    ---------

    Co-authored-by: Niels Laute <niels.laute@live.nl>
    Co-authored-by: Jiří Polášek <me@jiripolasek.com>

commit 0de60445ea
Author: Jiří Polášek <me@jiripolasek.com>
Date:   Thu Nov 27 16:31:10 2025 +0100

    CmdPal: Use Shell API to determine the default browser in WebSearch (#43339)

    This PR introduces a new method for determining the default browser
    using the Windows Shell API. The new provider selects the browser
    associated with the HTTPS protocol (falling back to HTTP if necessary).
    The original implementation is retained as a fallback for now, and the
    codebase is prepared for future extensions (e.g., manual default-browser
    selection).

    As a flyby, it also fixes an issue where commands continued showing the
    previous browser name if the user changed their default browser while
    the Command Palette was running.

    Fixed default browser selection in the Web Search built-in extension.

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

    - [x] Closes: #42343
    - [ ] **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 -->

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

commit 47d4a65223
Author: Jiří Polášek <me@jiripolasek.com>
Date:   Thu Nov 27 16:24:47 2025 +0100

    CmdPal: Add option to return to home automatically after a delay (#43551)

    This PR replaces the Go home when activated setting with a new
    Automatically return home option. This allows users to specify how long
    the Command Palette should wait after being dismissed before
    automatically returning to the home page. It also introduces migration
    logic to transition from the old setting to the new one.

    <img width="1337" height="762" alt="image"
    src="https://github.com/user-attachments/assets/c649ef03-b3ee-40ba-ac67-485bc40efa73"
    />

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

    - [x] Closes: #43355
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 1b72c0b969
Author: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date:   Thu Nov 27 17:22:59 2025 +0800

    Update check-spelling expect list (#43925)

    <!-- 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)?
    -->

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

    Spell no complain

commit 9160c82fc2
Author: Pratyush Nalam <github@code.pratyushnalam.com>
Date:   Thu Nov 27 05:15:06 2025 +0530

    Update Command Palette's Learn More string to be consistent with other utilities (#43898)

    <!-- 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)?
    -->

    In the PowerToys "What's new" window, every utility has the text "Learn
    more about <utility name>" next to the "Settings" button. Examples
    below:

    <img width="247" height="38" alt="learnmore-fancyzones"
    src="https://github.com/user-attachments/assets/fecdeb4b-e01c-438d-8d11-c056e613768e"
    />
    <img width="258" height="40" alt="learnmore-textextractor"
    src="https://github.com/user-attachments/assets/ffb0c801-5b89-46d1-b493-b57287303e65"
    />

    The only exception is the Command Palette utility which just says "Learn
    more".

    <img width="152" height="32" alt="learnmore-cmdpal"
    src="https://github.com/user-attachments/assets/232c11cd-b621-46eb-87f1-d3fc708d6286"
    />

    This is an inconsistency and this PR fixes that string.

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

    - [x] Closes: #43897
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 452e0dcf51
Author: Mike Hall <mikehall@microsoft.com>
Date:   Wed Nov 26 14:08:34 2025 +0000

    Module Loader tool for rapid testing of modules (#43813)
    ModuleLoader tool, a stand-alone Win32 executable for testing of
    PowerToy modules without needing branch builds.

    sample output from running the tool is below:

    .\ModuleLoader.exe .\powertoys.cursorwrap.dll
    PowerToys Module Loader v1.0
    =============================

    Loading module: .\powertoys.cursorwrap.dll
    Detected module name: cursorwrap

    Loading settings...
    Trying settings path:
    C:\Users\mikehall\AppData\Local\Microsoft\PowerToys\cursorwrap\settings.json
    Settings file loaded (315 characters)
    Settings loaded successfully.

    Loading module DLL...
    Module instance created successfully
    Module DLL loaded successfully.
    Module key: CursorWrap
    Module name: CursorWrap

    Applying settings to module...
    Settings applied.

    Registering module hotkeys...
    Module reports 1 legacy hotkey(s)
      Registering hotkey 0: Win+Alt+U - OK
    Hotkeys registered: 1

    Enabling module...
    Module enabled.

    =============================
    Module is now running!
    =============================

    Module Status:
      - Name: CursorWrap
      - Key: CursorWrap
      - Enabled: Yes
      - Hotkeys: 1 registered

    Registered Hotkeys:
      Win+Alt+U

    Press Ctrl+C to exit.
    You can press the module's hotkey to toggle its functionality.

    Note that this doesn't integrate with Powertoys settings UI - this is
    purely to test Powertoys module functionality.

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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
    See details above.
    ModuleLoader tested on Windows 11, Surface Laptop 7 Pro.

commit 2c9a9e9fca
Author: Jiří Polášek <me@jiripolasek.com>
Date:   Mon Nov 24 23:57:10 2025 +0100

    CmdPal: Improve Command Palette behavior in "Last position" mode (#43543)

    This PR improves Command Palette behavior in “Last position” mode:
    - Correctly handles DPI changes between monitors.
    - Ensures the window is always visible — if it’s fully off-screen or has
    less than 100px visible on any axis, it is re-centered.

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

    - [x] Closes: #43398
    - [ ] **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 -->

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

commit 09c8c1d79a
Author: leileizhang <leilzh@microsoft.com>
Date:   Mon Nov 24 10:42:35 2025 +0800

    [Hot Fix] Fix Image Resizer not working on Win10 (#43763)

    <!-- 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)?
    -->
    Windows 10 can’t launch the app using the Sparse Package. Remove the app
    manifest so that Image Resizer can start properly on Windows 10.

    We will figure out how to support Sparse Packages on Windows 10 in the
    next release.
    <!-- Please review the items on the PR checklist before submitting-->

    - [x] Closes: #43747 #43734 #43722 #43759
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 95c8a83f79
Author: leileizhang <leilzh@microsoft.com>
Date:   Mon Nov 24 10:08:12 2025 +0800

    [Hotfix] Remove the properties in Prompt Execution Settings for OpenAI (#43766)

    <!-- 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)?
    -->
    Remove the properties in Prompt Execution Settings for OpenAI, as the
    new models may not support them.

    Will try to expose them in the UI so users can add them on their own in
    the next release.

    <!-- Please review the items on the PR checklist before submitting-->
    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 2830ea919c
Author: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date:   Mon Nov 24 09:51:27 2025 +0800

    Advanced Paste: Adjust model parameter to make the result longer (#43768)

    <!-- 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)?
    -->
    Adjust model parameter to make the result longer
    <!-- Please review the items on the PR checklist before submitting-->

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 725ad21952
Author: Dave Rayment <dave.rayment@gmail.com>
Date:   Mon Nov 24 01:12:54 2025 +0000

    [Awake] Fix issue with timed mode not expiring correctly (#43785)

    <!-- 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)?
    -->
    Resolves an issue with the timed mode's expiry not completing correctly.

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

    - [x] Closes: #43775
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->
    This was because of my recent change to the timed mode. The `Subscribe`
    method on the `Observable` interval accidentally wired the completion
    logic to the **Error** handler instead of the **Completion** handler
    because of the use of a discard `_` instead of an empty parameter list
    `()`. As a result of the incorrect overload being called, Awake stayed
    in the Timed state despite the timer reaching zero.

    <!-- Describe how you validated the behavior. Add automated tests
    wherever possible, but list manual validation steps taken as well -->
    Confirmed that the timed mode times out and exits upon expiry.

commit ebc3a139c5
Author: Erik Anderson <erik.anderson@microsoft.com>
Date:   Fri Nov 21 02:34:34 2025 -0800

    Fix typo in AI settings card description (#43757)
    The word "cloud" does not use a vowel sound, so the preceding word
    should be "A" instead of "An".

    - [X] Closes: #43756
    - [ ] **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

    Co-authored-by: Erik Anderson <erikan@ntdev.microsoft.com>

commit 28dba2633e
Author: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com>
Date:   Thu Nov 20 15:22:40 2025 -0800

    [Light Switch][Dev Docs] Clarify LightSwitchService and LightSwitchStateManager roles (#43748)

    Updated LightSwitch module documentation to clarify the role of
    LightSwitchService and LightSwitchStateManager.

    ---------

    Co-authored-by: Niels Laute <niels.laute@live.nl>

commit 9fbd3de3a2
Author: Jiří Polášek <me@jiripolasek.com>
Date:   Thu Nov 20 16:23:42 2025 +0100

    CmdPal: Add native debugging launch profile to launchSettings.json (#43718)

    See title.

    I’m too lazy to open the dialog and then revert the change later.

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 4a0d9912ae
Author: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date:   Thu Nov 20 09:57:29 2025 +0800

    Advanced Paste:  No cache for foundry local model list  (#43716)

    <!-- 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)?
    -->
    Cache of the downloaded model will make the newly added model only work
    after running of powertoys, this disable the cache, so just downloaded
    model will take effect immediately

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 15c79a0176
Author: Dave Rayment <dave.rayment@gmail.com>
Date:   Wed Nov 19 08:50:25 2025 +0000

    [Settings] Fix inconsistent description text for the mouse tools (#43651)

    <!-- 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)?
    -->
    Change some of the mouse utilities' descriptions from declarative to
    imperative, to match best practice and to be consistent with the other
    descriptions.

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

    <!-- Describe how you validated the behavior. Add automated tests
    wherever possible, but list manual validation steps taken as well -->
    - Confirmed the changed descriptions were updated in Settings.

commit 97d46efec2
Author: Dave Rayment <dave.rayment@gmail.com>
Date:   Wed Nov 19 08:49:40 2025 +0000

    [Settings] Fix Dashboard toggle glitches and sorting UI (#43626)
    Fixes two UI bugs in the Settings Dashboard: module list glitching when
    toggling modules, and incorrect sort menu checkmarks.

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

    - [x] Closes: #43624
    - [x] Closes: #43625
    - [ ] **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 -->
    When enabling or disabling a module from the "Utilities" list, the
    entire list would flicker and redraw, causing other toggles to glitch.
    This made it appear as if multiple modules were being affected by a
    single change.

    **Root cause**
    The `AllModules` ObservableCollection was being completely cleared and
    re-populated on every change, forcing the UI to destroy and recreate all
    list items.

    **Fix**
    Refactored collection updates to modify items in-place:
    - Introduced `_moduleItems` master list, built once during
    initialization.
    - `RefreshModuleList()` now updates properties without clearing
    collections
    - `SortModuleList()` uses `ObservableCollection.Move()` instead of
    `Clear()`/`Add()`
    The checkmark in the "Sort by" menu would not update correctly when
    changing sort order, sometimes showing the incorrect item checked, or
    even both at once.

    **Root cause**
    The `IsChecked` prop on the `ToggleMenuFlyoutItem` is bound to
    `DashboardSortOrder`, but the binding was not updating because the
    ViewModel didn't raise a property change notification when the sort
    order was changed.

    **Fix**
    Added `OnPropertyChanged(nameof(DashboardSortOrder))` in
    `SortModuleList()`.

    1. Renamed `GetShortcutModules()` to `RefreshShortcutModules()`. The
    original name implied a getter, but the routine actually affects state
    by rebuilding the shortcut and action lists, violating the Command-Query
    Separation principle.
    2. Added an `_isUpdatingFromUI` flag as a defensive measure against
    circular updates when a UI toggle is changed.
    3. Separation of concerns for operations on the modules list. Building,
    sorting and refreshing it are separated.
    4. Added comments and XML doc headers for new methods. Included brief
    description of GPO locking behaviour.

    <!-- Describe how you validated the behavior. Add automated tests
    wherever possible, but list manual validation steps taken as well -->
    - Verified that toggling modules in the list no longer causes the list
    to flicker or for other toggles to glitch.
    - Confirmed that the sort order checkmarks update correctly and reflect
    the current sort order.
    - Tested GPO policy settings are still queried as before.
    - Checked sort behaviour is unaffected.

    *Sorting UI*

    https://github.com/user-attachments/assets/3484bf63-2946-4460-83a5-361fa7e41c82

    *Toggle behaviour*

    https://github.com/user-attachments/assets/1fae5429-6fa3-4431-80f3-0907dab4f326

    ---------

    Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>

commit 46242b384e
Author: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date:   Wed Nov 19 16:25:52 2025 +0800

    96 release change log (#43330)

    <!-- 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)?
    -->

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

    - [ ] 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 -->

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

    ---------

    Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
    Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
    Co-authored-by: Jiří Polášek <me@jiripolasek.com>
    Co-authored-by: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com>
    Co-authored-by: Jeremy Sinclair <4016293+snickler@users.noreply.github.com>
    Co-authored-by: leileizhang <leilzh@microsoft.com>
    Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
    Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
    Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
    Co-authored-by: Niels Laute <niels.laute@live.nl>
    Co-authored-by: Dave Rayment <dave.rayment@gmail.com>
    Co-authored-by: Gleb Khmyznikov <gleb.khmyznikov@gmail.com>
    Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
    Co-authored-by: Juju Anselum J <106316316+anselumjuju@users.noreply.github.com>
    Co-authored-by: Dustin L. Howett <duhowett@microsoft.com>
    Co-authored-by: Leon Zandman <leon@wirwar.com>
    Co-authored-by: Leon Zandman <lzandman@rdw.nl>
    Co-authored-by: moooyo <42196638+moooyo@users.noreply.github.com>
    Co-authored-by: Yu Leng <yuleng@microsoft.com>
    Co-authored-by: Mike Griese <migrie@microsoft.com>
    Co-authored-by: Michael Jolley <mike@baldbeardedbuilder.com>
    Co-authored-by: Mario Hewardt <marioh@microsoft.com>
    Co-authored-by: Alex Mihaiuc <69110671+foxmsft@users.noreply.github.com>
    Co-authored-by: Mike Hall <mikehall@microsoft.com>
    Co-authored-by: Trevor <ngo.trev.95@gmail.com>

commit 84be261581
Author: Niels Laute <niels.laute@live.nl>
Date:   Tue Nov 18 19:19:38 2025 -0800

    Logo change for Azure Inference (#43686)

    <!-- 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)?
    -->

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 5a8095b704
Author: Niels Laute <niels.laute@live.nl>
Date:   Tue Nov 18 18:32:14 2025 -0800

    Loc bug (#43685)

    <!-- 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)?
    -->

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 417c1a6b98
Author: Niels Laute <niels.laute@live.nl>
Date:   Tue Nov 18 16:21:14 2025 -0800

    Update FoundryLocal.svg (#43682)

    <!-- 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)?
    -->

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 2593149d22
Author: leileizhang <leilzh@microsoft.com>
Date:   Tue Nov 18 14:22:03 2025 +0800

    Fix OOBE Mouse Utilities crash by correcting localization key (#43664)

    <!-- 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)?
    -->

    - The OOBE Mouse Utilities page crashed when selected because the
    TextBlock with x:Uid="Oobe_MouseUtils_MousePointerCrosshairs" tried to
    bind a Description property that doesn’t exist.
    - Updated Resources.resw so the string entry is named
    Oobe_MouseUtils_MousePointerCrosshairs_Description.Text, matching the
    markdown description control instead of the TextBlock.
    - With the correct resource key, the XAML loader no longer resolves an
    invalid property and navigation succeeds.

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

    - [x] Closes:  #43663
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 0b50c38fe1
Author: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date:   Tue Nov 18 12:59:52 2025 +0800

    Advanced Paste: Refresh environment if foundry is not present (#43662)

    <!-- 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)?
    -->
    As title
    <!-- Please review the items on the PR checklist before submitting-->

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

    [12:55:29.6763496] [Info] FoundryClient.cs::CreateAsync::23
        [FoundryClient] First attempt failed, refreshing PATH and retrying
    [12:55:29.6766491] [Info] FoundryClient.cs::RefreshEnvironmentPath::225
        [FoundryClient] Refreshing PATH environment variable from system
    [12:55:29.6768710] [Info] FoundryClient.cs::RefreshEnvironmentPath::266
        [FoundryClient] Updating process PATH with latest system values
    [12:55:29.6769080] [Info] FoundryClient.cs::TryCreateClientAsync::33
        [FoundryClient] Creating Foundry Local client
    [12:55:29.6769312] [Info] FoundryClient.cs::TryCreateClientAsync::45
    [FoundryClient] Starting Foundry service using
    manager.StartServiceAsync()
    [12:55:29.9807668] [Info] FoundryClient.cs::TryCreateClientAsync::48
        [FoundryClient] Foundry service started successfully

    Verified, fist launch successfully

commit 840808b465
Author: Niels Laute <niels.laute@live.nl>
Date:   Tue Nov 18 03:28:40 2025 +0100

    [AP] Adding a single scrollviewer and fixing hidden tabstop (#43660)

    <!-- 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)?
    -->

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

    - [x] Closes: #43655
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit b50df36b70
Author: Jiří Polášek <me@jiripolasek.com>
Date:   Tue Nov 18 03:00:37 2025 +0100

    Setup: Hide apps in PowerToys.SpareApps package from Start Menu (#43650)

    <!-- 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)?
    -->

    This PR updates the Appx manifest for PowerToys.SpareApps to hide the
    apps from the Start Menu, as they lack proper visual elements like icons
    and text.

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

    - [x] Closes: #43647
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit b94593ef73
Author: Jiří Polášek <me@jiripolasek.com>
Date:   Tue Nov 18 00:55:14 2025 +0100

    Settings: Add ScrollViewer to Command Palette page in PowerToys Settings (#43649)

commit 7a01d56179
Author: Mario Hewardt <marioh@microsoft.com>
Date:   Mon Nov 17 07:26:42 2025 -0800

    Updates version for standalone release (#43645)

    <!-- 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)?
    -->
    Updates the version for standalone release

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 130e9a0a68
Author: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date:   Mon Nov 17 12:55:22 2025 +0800

    cmdpal: Fix launch by button in settings not work (#43634)

    <!-- 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)?
    -->
    Shell does not know it's a protocol, so add protocol to it.

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 34c37f2d38
Author: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date:   Mon Nov 17 09:48:32 2025 +0800

    Add not signed dll (#43631)

    <!-- 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)?
    -->

    This dll is not signed, will fail the pipeline

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 47aed03c03
Author: Niels Laute <niels.laute@live.nl>
Date:   Sun Nov 16 15:13:40 2025 +0100

    [AP] Loc fix (#43617)

    <!-- 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)?
    -->

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 6423c7693d
Author: Niels Laute <niels.laute@live.nl>
Date:   Sun Nov 16 15:02:38 2025 +0100

    [CmdPal] Settings page refresh (#43487)

    - Refreshed the CmdPal page.
    - CmdPal can now also be activated from this page by clicking a button
    - Added a Preview InfoBar for FL in AP

    <img width="1384" height="1067" alt="image"
    src="https://github.com/user-attachments/assets/f670e13b-5b4a-4f6a-bcb0-e1dc357afb1e"
    />

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

    - [ ] 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 -->

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

    ---------

    Co-authored-by: vanzue <vanzue@outlook.com>

commit 3e14d50f65
Author: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date:   Sun Nov 16 15:26:05 2025 +0800

    Advanced paste: Add more error handle for foundry local (#43600)

    <!-- 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)?
    -->
    Foundry local sdk will not run models that is not in catalog, when
    catalog removes some, the old ones will fail executing,
    so add error hint for users to re-configure the models in settings.

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

    <!-- Describe how you validated the behavior. Add automated tests
    wherever possible, but list manual validation steps taken as well -->
    <img width="864" height="216" alt="image"
    src="https://github.com/user-attachments/assets/654207b3-ff50-4888-a638-82136216de7b"
    />

commit db7c9e180e
Author: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com>
Date:   Sat Nov 15 08:08:20 2025 -0500

    [Light Switch] Removed logs from every tick, only logging key events. (#43572)

    Title

commit bcc3ded280
Author: Michael Jolley <mike@baldbeardedbuilder.com>
Date:   Sat Nov 15 07:07:52 2025 -0600

    CmdPal: Adding page Id to OpenPage telemetry event (#43584)

    @niels9001 requested this.

    As the name says

commit 24a3cdd486
Author: Mario Hewardt <marioh@microsoft.com>
Date:   Fri Nov 14 17:36:31 2025 -0800

    Fixes regressions introduced with GIF support (#43589)

    <!-- 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)?
    -->
    - Switches default recording format back to MP4
    - Fixes framerate issues between the two formats
    - Fixes file path names

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

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [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
    - [ ] **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 -->
    - Switches default recording format back to MP4
    - Fixes framerate issues between the two formats
    - Fixes file path names
    -
    <!-- Describe how you validated the behavior. Add automated tests
    wherever possible, but list manual validation steps taken as well -->
    Manual testing

commit 1884e6abc1
Author: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
Date:   Fri Nov 14 16:56:23 2025 +0800

    Remove unused properties in AP (#43564)

    <!-- 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)?
    -->
    This pull request removes legacy provider configuration migration logic
    and associated data structures from the Advanced Paste AI provider
    settings. The changes simplify the codebase by eliminating support for
    legacy provider configuration snapshots and related migration methods,
    focusing configuration management on the current provider model.

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

    ---------

    Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>

commit ad4b553bb1
Author: leileizhang <leilzh@microsoft.com>
Date:   Fri Nov 14 16:15:15 2025 +0800

    Remove all AdvancedPaste stored keys during uninstall (#43563)

    <!-- 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)?
    -->
    Remove all AdvancedPaste stored keys during uninstall
    <!-- Please review the items on the PR checklist before submitting-->

    - [ ] Closes: #xxx
    <!-- - [ ] Closes: #yyy (add separate lines for additional resolved
    issues) -->
    - [ ] **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 -->

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

commit 193d9aacbe
Author: Dustin L. Howett <duhowett@microsoft.com>
Date:   Thu Nov 13 17:59:21 2025 -0600

    BugReportTool: replace cziplib with tar.exe (#41127)

    BugReportTool is the last consumer in the PowerToys repo of cziplib, a
    library we use to produce ZIP files.

    This pull request replaces cziplib with a simple CreateProcess call that
    spawns `tar.exe`, which comes with Windows as of RS4 and can produce ZIP
    files!

    I've tested this by producing a bug report archive and attempting to
    open it with File Explorer. It works fine.

    We have taken every precaution to ensure that we do not allow any
    attacker-controlled input to tar's command line. We are *not* using
    `system()`, and we are not opening up a vector through which a nefarious
    caller can perform shell injection.

    We do not pass filenames to tar except that of the final archive. We do
    not pass directory names to tar; we rely on the current directory
    instead.
This commit is contained in:
Gleb Khmyznikov
2025-12-01 15:44:53 -08:00
parent 0e9b5f3acb
commit 5e9dd15f14
149 changed files with 6525 additions and 1376 deletions

View File

@@ -2,8 +2,8 @@ AAAAs
abcdefghjkmnpqrstuvxyz
abgr
ABlocked
ABOUTBOX
ABORTIFHUNG
ABOUTBOX
Abug
Acceleratorkeys
ACCEPTFILES
@@ -56,6 +56,7 @@ ANull
AOC
aocfnapldcnfbofgmbbllojgocaelgdd
AOklab
aot
APARTMENTTHREADED
APeriod
apicontract
@@ -97,8 +98,8 @@ ASSOCSTR
ASYNCWINDOWPLACEMENT
ASYNCWINDOWPOS
atl
ATX
ATRIOX
ATX
aumid
authenticode
AUTOBUDDY
@@ -117,10 +118,10 @@ azman
azureaiinference
azureinference
azureopenai
backticks
bbwe
BCIE
bck
backticks
BESTEFFORT
bezelled
bhid
@@ -148,8 +149,8 @@ bmi
BNumber
BODGY
BOklab
Bootstrappers
BOOTSTRAPPERINSTALLFOLDER
Bootstrappers
BOTTOMALIGN
boxmodel
BPBF
@@ -176,17 +177,16 @@ BYPOSITION
CALCRECT
CALG
callbackptr
cabstr
calpwstr
caub
Cangjie
CANRENAME
Carlseibert
Canvascustomlayout
CAPTUREBLT
CAPTURECHANGED
CARETBLINKING
Carlseibert
CAtl
caub
CBN
cch
CCHDEVICENAME
@@ -206,11 +206,9 @@ changecursor
CHILDACTIVATE
CHILDWINDOW
CHOOSEFONT
CIBUILD
cidl
CIELCh
cim
claude
CImage
cla
CLASSDC
@@ -264,7 +262,6 @@ CONFIGW
CONFLICTINGMODIFIERKEY
CONFLICTINGMODIFIERSHORTCUT
CONOUT
coreclr
constexpr
contentdialog
contentfiles
@@ -276,6 +273,7 @@ copiedcolorrepresentation
coppied
copyable
COPYPEN
coreclr
COREWINDOW
Corpor
cotaskmem
@@ -284,18 +282,18 @@ countof
covrun
cpcontrols
cph
cppcoreguidelines
cplusplus
CPower
cppcoreguidelines
cpptools
cppvsdbg
cppwinrt
createdump
creativecommons
CREATEPROCESS
CREATESCHEDULEDTASK
CREATESTRUCT
CREATEWINDOWFAILED
creativecommons
CRECT
CRH
critsec
@@ -331,7 +329,6 @@ CYSCREEN
CYSMICON
CYVIRTUALSCREEN
Czechia
cziplib
Dac
dacl
DAffine
@@ -355,9 +352,7 @@ Deact
debugbreak
decryptor
Dedup
dfx
Deduplicator
Deeplink
DEFAULTBOOTSTRAPPERINSTALLFOLDER
DEFAULTCOLOR
DEFAULTFLAGS
@@ -404,7 +399,6 @@ DISPLAYFREQUENCY
displayname
DISPLAYORIENTATION
divyan
djwsxzxb
Dlg
DLGFRAME
DLGMODALFRAME
@@ -417,7 +411,6 @@ DONTVALIDATEPATH
dotnet
downsampled
downsampling
Downsampled
downscale
DPICHANGED
DPIs
@@ -531,7 +524,6 @@ EXTRINSICPROPERTIES
eyetracker
FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
FNumber
FARPROC
fdx
fesf
@@ -563,8 +555,8 @@ FIXEDSYS
flac
flyouts
FMask
foundrylocal
fmtid
FNumber
FOF
FOFX
FOLDERID
@@ -575,6 +567,7 @@ FORCEMINIMIZE
FORMATDLGORD
formatetc
FORPARSING
foundrylocal
FRAMECHANGED
frm
FROMTOUCH
@@ -593,13 +586,13 @@ gdi
gdiplus
GDIPVER
GDISCALED
geolocator
GETCLIENTAREAANIMATION
GETCURSEL
GETDESKWALLPAPER
GETDLGCODE
GETDPISCALEDSIZE
getfilesiginforedist
geolocator
GETHOTKEY
GETICON
GETLBTEXT
@@ -610,11 +603,12 @@ GETSCREENSAVERRUNNING
GETSECKEY
GETSTICKYKEYS
GETTEXTLENGTH
GIFs
gitmodules
GHND
gitmodules
GMEM
GNumber
googleai
googlegemini
gpedit
gpo
GPOCA
@@ -631,8 +625,6 @@ GValue
gwl
GWLP
GWLSTYLE
googleai
googlegemini
hangeul
Hanzi
Hardlines
@@ -743,9 +735,7 @@ IDCANCEL
IDD
idk
idl
IIM
idlist
ifd
IDOK
IDOn
IDR
@@ -754,15 +744,16 @@ ietf
IEXPLORE
IFACEMETHOD
IFACEMETHODIMP
ifd
IGNOREUNKNOWN
IGo
iid
IIM
Iindex
Ijwhost
ILD
IMAGEHLP
IMAGERESIZERCONTEXTMENU
IPTC
IMAGERESIZEREXT
imageresizerinput
imageresizersettings
@@ -798,7 +789,6 @@ INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
INSTALLLOCATION
INSTALLMESSAGE
INSTALLPROPERTY
installscopeperuser
INSTALLSTARTMENUSHORTCUT
INSTALLSTATE
Inste
@@ -811,6 +801,7 @@ invokecommand
ipcmanager
IPREVIEW
ipreviewhandlervisualssetfont
IPTC
irow
irprops
isbi
@@ -854,15 +845,14 @@ keyvault
KILLFOCUS
killrunner
kmph
ksa
kvp
Kybd
LARGEICON
lastcodeanalysissucceeded
LASTEXITCODE
LAYOUTRTL
LCh
lbl
LCh
lcid
LCIDTo
lcl
@@ -878,10 +868,10 @@ LExit
lhwnd
LIBFUZZER
LIBID
lightswitch
LIMITSIZE
LIMITTEXT
lindex
lightswitch
linkid
LINKOVERLAY
LINQTo
@@ -892,6 +882,7 @@ LLKH
llkhf
LMEM
LMENU
lng
LOADFROMFILE
LOBYTE
localappdata
@@ -901,17 +892,14 @@ LOCATIONCHANGE
LOCKTYPE
LOGFONT
LOGFONTW
logon
lon
LOGMSG
logon
LOGPIXELSX
LOGPIXELSY
lng
lon
longdate
LONGNAMES
lowlevel
lquadrant
LOWORD
lparam
LPBITMAPINFOHEADER
@@ -945,6 +933,7 @@ lpv
LPW
lpwcx
lpwndpl
lquadrant
LReader
LRESULT
LSTATUS
@@ -971,6 +960,7 @@ MAKELONG
MAKELPARAM
makepri
MAKEWPARAM
Malware
manifestdependency
MAPPEDTOSAMEKEY
MAPTOSAMESHORTCUT
@@ -993,8 +983,8 @@ MENUITEMINFO
MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metadatas
metadatamatters
Metadatas
metafile
mfc
Mgmt
@@ -1040,9 +1030,6 @@ mousepointer
mouseutils
MOVESIZEEND
MOVESIZESTART
muxx
muxxc
muxxh
MRM
MRT
mru
@@ -1071,10 +1058,14 @@ msrc
msstore
msvcp
MT
mstsc
MTND
MULTIPLEUSE
multizone
muxc
muxx
muxxc
muxxh
MVPs
mvvm
MVVMTK
@@ -1157,7 +1148,6 @@ nonstd
NOOWNERZORDER
NOPARENTNOTIFY
NOPREFIX
NPU
NOREDIRECTIONBITMAP
NOREDRAW
NOREMOVE
@@ -1186,6 +1176,7 @@ nowarn
NOZORDER
NPH
npmjs
NPU
NResize
NTAPI
ntdll
@@ -1210,15 +1201,17 @@ oldpath
oldtheme
oleaut
OLECHAR
ollama
onebranch
onnx
OOBEUI
openas
opencode
OPENFILENAME
openrdp
opensource
openxmlformats
ollama
Olllama
onnx
OPTIMIZEFORINVOKE
ORPHANEDDIALOGTITLE
@@ -1292,6 +1285,7 @@ pguid
phbm
phbmp
phicon
Photoshop
phwnd
pici
pidl
@@ -1314,7 +1308,6 @@ pnid
PNMLINK
Poc
Podcasts
Photoshop
POINTERID
POINTERUPDATE
Pokedex
@@ -1409,10 +1402,9 @@ pwsz
pwtd
QDC
qit
QNN
Qualcomm
QITAB
QITABENT
QNN
qoi
Quarternary
QUERYENDSESSION
@@ -1422,8 +1414,8 @@ quickaccent
QUNS
RAII
RAlt
RAquadrant
randi
RAquadrant
rasterization
Rasterize
RAWINPUTDEVICE
@@ -1433,6 +1425,8 @@ RAWPATH
rbhid
rclsid
RCZOOMIT
remotedesktop
rdp
RDW
READMODE
READOBJECTS
@@ -1450,9 +1444,7 @@ regfile
REGISTERCLASSFAILED
REGISTRYHEADER
REGISTRYPREVIEWEXT
registryroot
regkey
regroot
regsvr
REINSTALLMODE
releaseblog
@@ -1505,7 +1497,6 @@ rstringalpha
rstringdigit
rtb
RTLREADING
rtm
runas
rundll
rungameid
@@ -1562,8 +1553,8 @@ SETRULES
SETSCREENSAVEACTIVE
SETSTICKYKEYS
SETTEXT
settingscard
SETTINGCHANGE
settingscard
SETTINGSCHANGED
settingsheader
settingshotkeycontrol
@@ -1708,6 +1699,7 @@ stringtable
stringval
Strm
strret
STRSAFE
stscanf
sttngs
Stubless
@@ -1719,7 +1711,6 @@ sublang
SUBMODULEUPDATE
subresource
Superbar
suntimes
sut
svchost
SVGIn
@@ -1753,7 +1744,6 @@ SYSTEMMODAL
SYSTEMTIME
TARG
TARGETAPPHEADER
TARGETDIR
targetentrypoint
TARGETHEADER
targetver
@@ -1783,10 +1773,10 @@ textextractor
TEXTINCLUDE
tfopen
tgz
THEMECHANGED
themeresources
THH
THICKFRAME
THEMECHANGED
THISCOMPONENT
throughs
TILEDWINDOW
@@ -1883,7 +1873,6 @@ USEINSTALLERFORTEST
USESHOWWINDOW
USESTDHANDLES
USRDLL
utm
UType
uuidv
uwp
@@ -1956,11 +1945,11 @@ Wca
WCE
wcex
WClass
WCRAPI
wcsicmp
wcsncpy
wcsnicmp
WCT
WCRAPI
WDA
wdm
wdp
@@ -1988,6 +1977,7 @@ WINDOWPLACEMENT
WINDOWPOSCHANGED
WINDOWPOSCHANGING
WINDOWSBUILDNUMBER
windowsml
windowssearch
windowssettings
WINDOWSTYLES
@@ -2003,9 +1993,8 @@ Winhook
WINL
winlogon
winmd
WINNT
windowsml
winml
WINNT
winres
winrt
winsdk
@@ -2067,20 +2056,21 @@ WTSAT
Wubi
WUX
Wwanpp
xap
XAxis
XButton
xclip
xcopy
xap
XDeployment
XDimension
xdf
XDimension
XDocument
XElement
xfd
XFile
XIncrement
XLoc
xmp
XNamespace
Xoshiro
XPels
@@ -2091,23 +2081,22 @@ xsi
XSpeed
XStr
xstyler
xmp
XTimer
XUP
XVIRTUALSCREEN
xxxxxx
YAxis
ycombinator
YIncrement
YDimension
YIncrement
yinle
yinyue
YPels
YPos
YResolution
YSpeed
YTimer
YStr
YTimer
YVIRTUALSCREEN
ZEROINIT
zonability

View File

@@ -21,6 +21,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4

View File

@@ -27,7 +27,7 @@ jobs:
issue: ${{ fromJson(github.event.inputs.issue_numbers) }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Run GenAI Issue Deduplicator
uses: pelikhan/action-genai-issue-dedup@v0

3
.gitmodules vendored
View File

@@ -4,6 +4,3 @@
[submodule "deps/expected-lite"]
path = deps/expected-lite
url = https://github.com/martinmoene/expected-lite.git
[submodule "deps/cziplib"]
path = deps/cziplib
url = https://github.com/kuba--/zip.git

View File

@@ -291,6 +291,7 @@
"Mono.Cecil.Rocks.dll",
"Newtonsoft.Json.dll",
"CommunityToolkit.WinUI.Controls.TitleBar.dll",
"CommunityToolkit.WinUI.Controls.OpacityMaskView.dll",
"NLog.dll",
"HtmlAgilityPack.dll",

View File

@@ -26,6 +26,7 @@
<PropertyGroup Condition="'$(SkipCppCodeAnalysis)' == ''">
<RunCodeAnalysis>true</RunCodeAnalysis>
<CodeAnalysisRuleSet>$(MsbuildThisFileDirectory)\CppRuleSet.ruleset</CodeAnalysisRuleSet>
<CAExcludePath>$(MSBuildThisFileDirectory)deps;$(MSBuildThisFileDirectory)packages;$(CAExcludePath)</CAExcludePath>
</PropertyGroup>
<!-- C++ source compile-specific things for all configurations -->
@@ -34,7 +35,7 @@
<PreferredToolArchitecture Condition="'$(PROCESSOR_ARCHITECTURE)' == 'ARM64' or '$(PROCESSOR_ARCHITEW6432)' == 'ARM64'">arm64</PreferredToolArchitecture>
<VcpkgEnabled>false</VcpkgEnabled>
<ReplaceWildcardsInProjectItems>true</ReplaceWildcardsInProjectItems>
<ExternalIncludePath>$(MSBuildThisFileFullPath)\..\deps\;$(MSBuildThisFileFullPath)\..\packages\;$(ExternalIncludePath)</ExternalIncludePath>
<ExternalIncludePath>$(MSBuildThisFileDirectory)deps;$(MSBuildThisFileDirectory)packages;$(ExternalIncludePath)</ExternalIncludePath>
<!-- Enable control flow guard for C++ projects that don't consume any C++ files -->
<!-- This covers the case where a .dll exports a .lib, but doesn't have any ClCompile entries. -->
<LinkControlFlowGuard>Guard</LinkControlFlowGuard>

View File

@@ -7,6 +7,7 @@
<PackageVersion Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" />
<PackageVersion Include="AdaptiveCards.Rendering.WinUI3" Version="2.1.0-beta" />
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251101-build.2372" />
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
@@ -68,7 +69,7 @@
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4948" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.37" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" />

View File

@@ -1498,6 +1498,7 @@ SOFTWARE.
- CoenM.ImageSharp.ImageHash
- CommunityToolkit.Common
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
- CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView
- CommunityToolkit.Mvvm
- CommunityToolkit.WinUI.Animations
- CommunityToolkit.WinUI.Collections

View File

@@ -834,6 +834,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanguageModelProvider", "sr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewModels.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UI.ViewModels.UnitTests\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj", "{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.RemoteDesktop", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj", "{2B3FB837-23DE-629F-82C6-42304E7083C9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj", "{DB34808A-FF91-D06E-A426-AFB5A8BD583B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -3036,6 +3040,22 @@ Global
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.Build.0 = Release|ARM64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.ActiveCfg = Release|x64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.Build.0 = Release|x64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|ARM64.ActiveCfg = Debug|ARM64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|ARM64.Build.0 = Debug|ARM64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|x64.ActiveCfg = Debug|x64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|x64.Build.0 = Debug|x64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|ARM64.ActiveCfg = Release|ARM64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|ARM64.Build.0 = Release|ARM64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|x64.ActiveCfg = Release|x64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|x64.Build.0 = Release|x64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|ARM64.ActiveCfg = Debug|ARM64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|ARM64.Build.0 = Debug|ARM64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|x64.ActiveCfg = Debug|x64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|x64.Build.0 = Debug|x64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|ARM64.ActiveCfg = Release|ARM64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|ARM64.Build.0 = Release|ARM64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|x64.ActiveCfg = Release|x64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3367,6 +3387,8 @@ Global
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{45354F4F-1414-45CE-B600-51CD1209FD19} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{2B3FB837-23DE-629F-82C6-42304E7083C9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
{DB34808A-FF91-D06E-A426-AFB5A8BD583B} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

240
README.md
View File

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

1
deps/cziplib vendored

Submodule deps/cziplib deleted from 81314fff0a

View File

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

View File

@@ -4,6 +4,7 @@
#include <ProjectTelemetry.h>
#include <spdlog/sinks/base_sink.h>
#include <filesystem>
#include <string_view>
#include "../../src/common/logger/logger.h"
#include "../../src/common/utils/gpo.h"
@@ -856,14 +857,69 @@ UINT __stdcall UnsetAdvancedPasteAPIKeyCA(MSIHANDLE hInstall)
try
{
winrt::Windows::Security::Credentials::PasswordVault vault;
winrt::Windows::Security::Credentials::PasswordCredential cred;
hr = WcaInitialize(hInstall, "UnsetAdvancedPasteAPIKey");
ExitOnFailure(hr, "Failed to initialize");
cred = vault.Retrieve(L"https://platform.openai.com/api-keys", L"PowerToys_AdvancedPaste_OpenAIKey");
vault.Remove(cred);
winrt::Windows::Security::Credentials::PasswordVault vault;
auto hasPrefix = [](std::wstring_view value, wchar_t const* prefix) {
std::wstring_view prefixView{ prefix };
return value.compare(0, prefixView.size(), prefixView) == 0;
};
const wchar_t* resourcePrefixes[] = {
L"https://platform.openai.com/api-keys",
L"https://azure.microsoft.com/products/ai-services/openai-service",
L"https://azure.microsoft.com/products/ai-services/ai-inference",
L"https://console.mistral.ai/account/api-keys",
L"https://ai.google.dev/",
};
const wchar_t* usernamePrefixes[] = {
L"PowerToys_AdvancedPaste_",
};
auto credentials = vault.RetrieveAll();
for (auto const& credential : credentials)
{
bool shouldRemove = false;
std::wstring resource{ credential.Resource() };
for (auto const prefix : resourcePrefixes)
{
if (hasPrefix(resource, prefix))
{
shouldRemove = true;
break;
}
}
if (!shouldRemove)
{
std::wstring username{ credential.UserName() };
for (auto const prefix : usernamePrefixes)
{
if (hasPrefix(username, prefix))
{
shouldRemove = true;
break;
}
}
}
if (!shouldRemove)
{
continue;
}
try
{
vault.Remove(credential);
}
catch (...)
{
}
}
}
catch (...)
{

View File

@@ -42,7 +42,8 @@
Description="PowerToys OCR Module"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
Square44x44Logo="Images\Square44x44Logo.png"
AppListEntry="none">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.SettingsUI" Executable="WinUI3Apps\PowerToys.Settings.exe" EntryPoint="Windows.FullTrustApplication">
@@ -51,7 +52,8 @@
Description="PowerToys Settings UI"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
Square44x44Logo="Images\Square44x44Logo.png"
AppListEntry="none">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.ImageResizerUI" Executable="WinUI3Apps\PowerToys.ImageResizer.exe" EntryPoint="Windows.FullTrustApplication">
@@ -60,7 +62,8 @@
Description="PowerToys Image Resizer UI"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
Square44x44Logo="Images\Square44x44Logo.png"
AppListEntry="none">
</uap:VisualElements>
</Application>
</Applications>

View File

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

View File

@@ -12,8 +12,8 @@ namespace LanguageModelProvider;
public sealed class FoundryLocalModelProvider : ILanguageModelProvider
{
private IEnumerable<ModelDetails>? _downloadedModels;
private FoundryClient? _foundryClient;
private IEnumerable<FoundryCatalogModel>? _catalogModels;
private string? _serviceUrl;
public static FoundryLocalModelProvider Instance { get; } = new();
@@ -24,22 +24,8 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
public IChatClient? GetIChatClient(string modelId)
{
try
{
Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {modelId}");
InitializeAsync().GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.LogError($"[FoundryLocal] Failed to initialize: {ex.Message}");
return null;
}
if (string.IsNullOrWhiteSpace(_serviceUrl) || _foundryClient == null)
{
Logger.LogError("[FoundryLocal] Service URL or manager is null");
return null;
}
Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {modelId}");
InitializeAsync().GetAwaiter().GetResult();
if (string.IsNullOrWhiteSpace(modelId))
{
@@ -47,39 +33,38 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
return null;
}
// Ensure the model is loaded before returning chat client
try
// Check if model is in catalog
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
if (!isInCatalog)
{
var isLoaded = _foundryClient.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
if (!isLoaded)
{
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
return null;
}
Logger.LogInfo($"[FoundryLocal] Model is loaded: {modelId}");
var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings.";
Logger.LogError($"[FoundryLocal] {errorMessage}");
throw new InvalidOperationException(errorMessage);
}
catch (Exception ex)
// Ensure the model is loaded before returning chat client
var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
if (!isLoaded)
{
Logger.LogError($"[FoundryLocal] Exception ensuring model loaded: {ex.Message}");
return null;
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
throw new InvalidOperationException($"Failed to load the model '{modelId}'.");
}
// Use ServiceUri instead of Endpoint since Endpoint already includes /v1
var baseUri = _foundryClient.GetServiceUri();
if (baseUri == null)
{
Logger.LogError("[FoundryLocal] Service URI is null");
return null;
const string message = "Foundry Local service URL is not available. Please make sure Foundry Local is installed and running.";
Logger.LogError($"[FoundryLocal] {message}");
throw new InvalidOperationException(message);
}
var endpointUri = new Uri($"{baseUri.ToString().TrimEnd('/')}/v1");
Logger.LogInfo($"[FoundryLocal] Creating OpenAI client with endpoint: {endpointUri}");
Logger.LogInfo($"[FoundryLocal] Model ID for chat client: {modelId}");
return new OpenAIClient(
new ApiKeyCredential("none"),
new OpenAIClientOptions { Endpoint = endpointUri })
new OpenAIClientOptions { Endpoint = endpointUri, NetworkTimeout = TimeSpan.FromMinutes(5) })
.GetChatClient(modelId)
.AsIChatClient();
}
@@ -105,49 +90,16 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{_serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()";
}
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default)
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(CancellationToken cancelationToken = default)
{
if (ignoreCached)
{
Logger.LogInfo("[FoundryLocal] Ignoring cached models, resetting");
Reset();
}
await InitializeAsync(cancelationToken);
Logger.LogInfo($"[FoundryLocal] Returning {_downloadedModels?.Count() ?? 0} downloaded models");
return _downloadedModels ?? [];
}
private void Reset()
{
_downloadedModels = null;
_ = InitializeAsync();
}
private async Task InitializeAsync(CancellationToken cancelationToken = default)
{
if (_foundryClient != null && _downloadedModels != null && _downloadedModels.Any())
{
await _foundryClient.EnsureRunning().ConfigureAwait(false);
return;
}
Logger.LogInfo("[FoundryLocal] Initializing provider");
_foundryClient ??= await FoundryClient.CreateAsync();
if (_foundryClient == null)
{
Logger.LogError("[FoundryLocal] Failed to create Foundry client");
return;
return Array.Empty<ModelDetails>();
}
_serviceUrl ??= await _foundryClient.GetServiceUrl();
Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}");
var cachedModels = await _foundryClient.ListCachedModels();
Logger.LogInfo($"[FoundryLocal] Found {cachedModels.Count} cached models");
List<ModelDetails> downloadedModels = [];
foreach (var model in cachedModels)
@@ -160,13 +112,37 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
Url = $"fl://{model.Name}",
Description = $"{model.Name} running locally with Foundry Local",
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
SupportedOnQualcomm = true,
ProviderModelDetails = model,
});
}
_downloadedModels = downloadedModels;
Logger.LogInfo($"[FoundryLocal] Initialization complete. Total downloaded models: {downloadedModels.Count}");
return downloadedModels;
}
private async Task InitializeAsync(CancellationToken cancelationToken = default)
{
if (_foundryClient != null && _catalogModels != null && _catalogModels.Any())
{
await _foundryClient.EnsureRunning().ConfigureAwait(false);
return;
}
Logger.LogInfo("[FoundryLocal] Initializing provider");
_foundryClient ??= await FoundryClient.CreateAsync();
if (_foundryClient == null)
{
const string message = "Foundry Local client could not be created. Please make sure Foundry Local is installed and running.";
Logger.LogError($"[FoundryLocal] {message}");
throw new InvalidOperationException(message);
}
_serviceUrl ??= await _foundryClient.GetServiceUrl();
Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}");
var catalogModels = await _foundryClient.ListCatalogModels();
Logger.LogInfo($"[FoundryLocal] Found {catalogModels.Count} catalog models");
_catalogModels = catalogModels;
}
public async Task<bool> IsAvailable()

View File

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

View File

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

View File

@@ -31,6 +31,11 @@ namespace ManagedCommon
/// </summary>
public static string CurrentVersionLogDirectoryPath { get; private set; }
/// <summary>
/// Gets the path to the current log file.
/// </summary>
public static string CurrentLogFile { get; private set; }
/// <summary>
/// Gets the path to the log directory for the app.
/// </summary>
@@ -55,7 +60,9 @@ namespace ManagedCommon
AppLogDirectoryPath = basePath;
CurrentVersionLogDirectoryPath = versionedPath;
var logFilePath = Path.Combine(versionedPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log");
var logFile = "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
var logFilePath = Path.Combine(versionedPath, logFile);
CurrentLogFile = logFilePath;
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));

View File

@@ -558,7 +558,7 @@
<TextBlock
x:Uid="AIProvidersFlyoutHeader"
Grid.Row="0"
Style="{StaticResource BodyStrongTextBlockStyle}" />
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<ListView
x:Name="AIProviderListView"
Grid.Row="1"

View File

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

View File

@@ -168,6 +168,15 @@ namespace AdvancedPaste.Settings
}
var properties = settings.Properties;
bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag);
bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag;
PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
if (legacyCredential is null)
{
return legacyAdvancedAIConsumed;
}
var configuration = properties.PasteAIConfiguration;
if (configuration is null)
@@ -176,30 +185,11 @@ namespace AdvancedPaste.Settings
properties.PasteAIConfiguration = configuration;
}
bool hasLegacyProviders = configuration.LegacyProviderConfigurations is { Count: > 0 };
bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag);
bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag;
PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
if (!hasLegacyProviders && legacyCredential is null && !legacyAdvancedAIConsumed)
{
return false;
}
bool configurationUpdated = false;
if (hasLegacyProviders)
{
configurationUpdated |= AdvancedPasteMigrationHelper.MigrateLegacyProviderConfigurations(configuration);
}
PasteAIProviderDefinition openAIProvider = null;
if (legacyCredential is not null || hasLegacyProviders || legacyAdvancedAIConsumed)
{
var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
openAIProvider = ensureResult.Provider;
configurationUpdated |= ensureResult.Updated;
}
var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
PasteAIProviderDefinition openAIProvider = ensureResult.Provider;
configurationUpdated |= ensureResult.Updated;
if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled)
{
@@ -207,13 +197,13 @@ namespace AdvancedPaste.Settings
configurationUpdated = true;
}
if (legacyCredential is not null && openAIProvider is not null)
if (openAIProvider is not null)
{
StoreMigratedOpenAICredential(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password);
RemoveLegacyOpenAICredential();
}
bool shouldEnableAI = legacyCredential is not null;
const bool shouldEnableAI = true;
bool enabledUpdated = false;
if (properties.IsAIEnabled != shouldEnableAI)
{

View File

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

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using LanguageModelProvider;
using Microsoft.Extensions.AI;
@@ -33,10 +34,6 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
_config = config;
}
public string ProviderName => AIServiceType.FoundryLocal.ToNormalizedKey();
public string DisplayName => string.IsNullOrWhiteSpace(_config?.Model) ? "Foundry Local" : _config.Model;
public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -76,13 +73,20 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
}
cancellationToken.ThrowIfCancellationRequested();
var chatClient = _modelProvider.GetIChatClient(modelReference);
if (chatClient is null)
IChatClient chatClient;
try
{
chatClient = _modelProvider.GetIChatClient(modelReference);
}
catch (InvalidOperationException ex)
{
// GetIChatClient throws InvalidOperationException for user-facing errors
var errorMessage = string.Format(System.Globalization.CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("FoundryLocal_UnableToLoadModel"), modelReference);
throw new PasteActionException(
$"Unable to load Foundry Local model: {modelReference}",
new InvalidOperationException("Chat client resolution failed"),
aiServiceMessage: "The model may not be downloaded or the Foundry Local service may not be running. Please check the model status in settings.");
errorMessage,
ex,
aiServiceMessage: ex.Message);
}
var userMessageContent = $"""
@@ -142,6 +146,7 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
var options = new ChatOptions
{
ModelId = modelReference,
MaxOutputTokens = 2048,
};
if (!string.IsNullOrWhiteSpace(systemPrompt))

View File

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

View File

@@ -160,10 +160,10 @@
<value>Active provider: {0}</value>
</data>
<data name="AIProvidersFlyoutHeader.Text" xml:space="preserve">
<value>AI providers</value>
<value>Configured models</value>
</data>
<data name="AIProvidersEmptyText.Text" xml:space="preserve">
<value>No AI providers configured</value>
<value>No models configured</value>
</data>
<data name="AIProvidersManageButtonContent.Content" xml:space="preserve">
<value>Configure models in Settings</value>
@@ -364,8 +364,12 @@
<data name="CustomEndpointWarning" xml:space="preserve">
<value>You are using a custom endpoint. Verify all answers.</value>
</data>
<data name="LocalModelBadge" xml:space="preserve">
<data name="LocalModelBadge.Text" xml:space="preserve">
<value>Local</value>
<comment>Badge label displayed next to local AI model providers (e.g., Ollama, Foundry Local) to indicate the model runs locally</comment>
</data>
<data name="FoundryLocal_UnableToLoadModel" xml:space="preserve">
<value>Unable to load Foundry Local model: {0}</value>
<comment>{0} is the model identifier. Do not translate {0}.</comment>
</data>
</root>

View File

@@ -271,7 +271,6 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
if (wait == WAIT_OBJECT_0 + (hParent ? (hManualOverride ? 3 : 2) : 2))
{
Logger::info(L"[LightSwitchService] Settings file changed event detected.");
ResetEvent(hSettingsChanged);
LightSwitchSettings::instance().LoadSettings();
stateManager.OnSettingsChanged();

View File

@@ -17,12 +17,10 @@ LightSwitchStateManager::LightSwitchStateManager()
void LightSwitchStateManager::OnSettingsChanged()
{
std::lock_guard<std::mutex> lock(_stateMutex);
Logger::info(L"[LightSwitchStateManager] Settings changed event received");
// If manual override was active, clear it so new settings take effect
if (_state.isManualOverride)
{
Logger::info(L"[LightSwitchStateManager] Clearing manual override due to settings update.");
_state.isManualOverride = false;
}
@@ -33,7 +31,6 @@ void LightSwitchStateManager::OnSettingsChanged()
void LightSwitchStateManager::OnTick(int currentMinutes)
{
std::lock_guard<std::mutex> lock(_stateMutex);
Logger::debug(L"[LightSwitchStateManager] Tick received: {}", currentMinutes);
EvaluateAndApplyIfNeeded();
}
@@ -51,7 +48,7 @@ void LightSwitchStateManager::OnManualOverride()
_state.isAppsLightActive = GetCurrentAppsTheme();
Logger::info(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
(_state.isSystemLightActive ? L"light" : L"dark"),
(_state.isAppsLightActive ? L"light" : L"dark"));
}
@@ -79,9 +76,9 @@ void LightSwitchStateManager::SyncInitialThemeState()
std::lock_guard<std::mutex> lock(_stateMutex);
_state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme();
Logger::info(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
_state.isSystemLightActive ? L"light" : L"dark");
Logger::info(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
_state.isAppsLightActive ? L"light" : L"dark");
}
@@ -127,7 +124,6 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
// Early exit: OFF mode just pauses activity
if (_currentSettings.scheduleMode == ScheduleMode::Off)
{
Logger::debug(L"[LightSwitchStateManager] Mode is OFF — pausing service logic.");
_state.lastTickMinutes = now;
return;
}
@@ -145,7 +141,6 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
if (newDay || modeChangedToSun)
{
Logger::info(L"[LightSwitchStateManager] Recalculating sun times (mode/day change).");
auto [newLightTime, newDarkTime] = update_sun_times(_currentSettings);
_state.lastEvaluatedDay = st.wDay;
_state.effectiveLightMinutes = newLightTime + _currentSettings.sunrise_offset;
@@ -188,12 +183,10 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
if (crossedBoundary)
{
Logger::info(L"[LightSwitchStateManager] Manual override cleared after crossing boundary.");
_state.isManualOverride = false;
}
else
{
Logger::debug(L"[LightSwitchStateManager] Manual override active — skipping auto apply.");
_state.lastTickMinutes = now;
return;
}
@@ -206,7 +199,7 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);
Logger::debug(
/* Logger::debug(
L"[LightSwitchStateManager] now = {:02d}:{:02d}, light boundary = {:02d}:{:02d} ({}), dark boundary = {:02d}:{:02d} ({})",
now / 60,
now % 60,
@@ -215,12 +208,12 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
_state.effectiveLightMinutes,
_state.effectiveDarkMinutes / 60,
_state.effectiveDarkMinutes % 60,
_state.effectiveDarkMinutes);
_state.effectiveDarkMinutes); */
Logger::debug("should be light = {}, apps needs change = {}, system needs change = {}",
/* Logger::debug("should be light = {}, apps needs change = {}, system needs change = {}",
shouldBeLight ? "true" : "false",
appsNeedsToChange ? "true" : "false",
systemNeedsToChange ? "true" : "false");
systemNeedsToChange ? "true" : "false"); */
// Only apply theme if there's a change or no override active
if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange))
@@ -230,10 +223,6 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
_state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme();
Logger::debug(L"[LightSwitchStateManager] Synced post-apply theme state — System: {}, Apps: {}",
_state.isSystemLightActive ? L"light" : L"dark",
_state.isAppsLightActive ? L"light" : L"dark");
}
_state.lastTickMinutes = now;

View File

@@ -39,6 +39,10 @@ type_pEnableThemeDialogTexture pEnableThemeDialogTexture;
#define WIN7_VERSION 0x106
#define WIN10_VERSION 0x206
// Default recording format frame rates
#define RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE 15
#define RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE 30
// Time that we'll cache live zoom window to avoid flicker
// of live zooming on Vista/ws2k8
#define LIVEZOOM_WINDOW_TIMEOUT 2*3600*1000

View File

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

View File

@@ -44,11 +44,11 @@ LOGFONT g_LogFont;
BOOLEAN g_DemoTypeUserDriven = false;
TCHAR g_DemoTypeFile[MAX_PATH] = {0};
DWORD g_DemoTypeSpeedSlider = static_cast<int>(((MIN_TYPING_SPEED - MAX_TYPING_SPEED) / 2) + MAX_TYPING_SPEED);
DWORD g_RecordFrameRate = 30;
DWORD g_RecordFrameRate = 30; // We default to 30 here, but g_RecordFrameRate can be different depending on recording format and gets set accordingly
DWORD g_RecordScaling = 100;
DWORD g_RecordScalingGIF = 50;
DWORD g_RecordScalingMP4 = 100;
RecordingFormat g_RecordingFormat = RecordingFormat::GIF;
RecordingFormat g_RecordingFormat = RecordingFormat::MP4;
BOOLEAN g_CaptureAudio = FALSE;
TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0};
@@ -87,8 +87,7 @@ REG_SETTING RegSettings[] = {
{ L"SnapToGrid", SETTING_TYPE_BOOLEAN, 0, &g_SnapToGrid, static_cast<DOUBLE>(g_SnapToGrid) },
{ L"ZoominSliderLevel", SETTING_TYPE_DWORD, 0, &g_SliderZoomLevel, static_cast<DOUBLE>(g_SliderZoomLevel) },
{ L"Font", SETTING_TYPE_BINARY, sizeof g_LogFont, &g_LogFont, static_cast<DOUBLE>(0) },
{ L"RecordFrameRate", SETTING_TYPE_DWORD, 0, &g_RecordFrameRate, static_cast<DOUBLE>(g_RecordFrameRate) },
{ L"RecordingFormat", SETTING_TYPE_DWORD, 0, &g_RecordingFormat, static_cast<DOUBLE>(0) },
{ L"RecordingFormat", SETTING_TYPE_DWORD, 0, &g_RecordingFormat, static_cast<DOUBLE>(g_RecordingFormat) },
{ L"RecordScalingGIF", SETTING_TYPE_DWORD, 0, &g_RecordScalingGIF, static_cast<DOUBLE>(g_RecordScalingGIF) },
{ L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast<DOUBLE>(g_RecordScalingMP4) },
{ L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) },

View File

@@ -168,6 +168,7 @@ BOOL g_RecordToggle = FALSE;
BOOL g_RecordCropping = FALSE;
SelectRectangle g_SelectRectangle;
std::wstring g_RecordingSaveLocation;
std::wstring g_RecordingSaveLocationGIF;
winrt::IDirect3DDevice g_RecordDevice{ nullptr };
std::shared_ptr<VideoRecordingSession> g_RecordingSession = nullptr;
std::shared_ptr<GifRecordingSession> g_GifRecordingSession = nullptr;
@@ -2173,7 +2174,10 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
CheckDlgButton( g_OptionsTabs[RECORD_PAGE].hPage, IDC_CAPTURE_AUDIO,
g_CaptureAudio ? BST_CHECKED: BST_UNCHECKED );
for (int i = 0; i < _countof(g_FramerateOptions); i++) {
//
// The framerate drop down list is not used in the current version (might be added in the future)
//
/*for (int i = 0; i < _countof(g_FramerateOptions); i++) {
_stprintf(text, L"%d", g_FramerateOptions[i]);
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), static_cast<UINT>(CB_ADDSTRING),
@@ -2182,7 +2186,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), CB_SETCURSEL, static_cast<WPARAM>(i), static_cast<LPARAM>(0));
}
}
}*/
// Add the recording format to the combo box and set the current selection
size_t selection = 0;
@@ -2345,17 +2349,8 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
text[2] = 0;
newTimeout = _tstoi( text );
if( g_RecordingFormat == RecordingFormat::GIF )
{
// Hardcode lower frame rate for GIFs
g_RecordFrameRate = 15;
}
else
{
g_RecordFrameRate = g_FramerateOptions[SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0))];
}
g_RecordingFormat = static_cast<RecordingFormat>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FORMAT), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)));
g_RecordFrameRate = (g_RecordingFormat == RecordingFormat::GIF) ? RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE : RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE;
g_RecordScaling = static_cast<int>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)) * 10 + 10);
// Get the selected microphone
@@ -3536,7 +3531,16 @@ void StopRecording()
//----------------------------------------------------------------------------
auto GetUniqueRecordingFilename()
{
std::filesystem::path path{ g_RecordingSaveLocation };
std::filesystem::path path;
if (g_RecordingFormat == RecordingFormat::GIF)
{
path = g_RecordingSaveLocationGIF;
}
else
{
path = g_RecordingSaveLocation;
}
// Chop off index if it's there
auto base = std::regex_replace( path.stem().wstring(), std::wregex( L" [(][0-9]+[)]$" ), L"" );
@@ -3591,6 +3595,7 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
auto stream = co_await file.OpenAsync( winrt::FileAccessMode::ReadWrite );
// Create the appropriate recording session based on format
OutputDebugStringW((L"Starting recording session. Framerate: " + std::to_wstring(g_RecordFrameRate) + L" scaling: " + std::to_wstring(g_RecordScaling) + L" Format: " + (g_RecordingFormat == RecordingFormat::GIF ? L"GIF" : L"MP4") + L"\n").c_str());
if (g_RecordingFormat == RecordingFormat::GIF)
{
g_GifRecordingSession = GifRecordingSession::Create(
@@ -3657,18 +3662,44 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes );
}
if( g_RecordingSaveLocation.size() == 0) {
// Peek the folder Windows has chosen to display
static std::filesystem::path lastSaveFolder;
wil::unique_cotaskmem_string chosenFolderPath;
wil::com_ptr<IShellItem> currentSelectedFolder;
bool bFolderChanged = false;
if (SUCCEEDED(saveDialog->GetFolder(currentSelectedFolder.put())))
{
if (SUCCEEDED(currentSelectedFolder->GetDisplayName(SIGDN_FILESYSPATH, chosenFolderPath.put())))
{
if (lastSaveFolder != chosenFolderPath.get())
{
lastSaveFolder = chosenFolderPath.get() ? chosenFolderPath.get() : std::filesystem::path{};
bFolderChanged = true;
}
}
}
if( (g_RecordingFormat == RecordingFormat::GIF && g_RecordingSaveLocationGIF.size() == 0) || (g_RecordingFormat == RecordingFormat::MP4 && g_RecordingSaveLocation.size() == 0) || (bFolderChanged)) {
wil::com_ptr<IShellItem> shellItem;
wil::unique_cotaskmem_string folderPath;
if (SUCCEEDED(saveDialog->GetFolder(shellItem.put())) && SUCCEEDED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, folderPath.put())))
g_RecordingSaveLocation = folderPath.get();
if (SUCCEEDED(saveDialog->GetFolder(shellItem.put())) && SUCCEEDED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, folderPath.put()))) {
if (g_RecordingFormat == RecordingFormat::GIF) {
g_RecordingSaveLocationGIF = folderPath.get();
std::filesystem::path currentPath{ g_RecordingSaveLocationGIF };
g_RecordingSaveLocationGIF = currentPath / DEFAULT_GIF_RECORDING_FILE;
}
else {
g_RecordingSaveLocation = folderPath.get();
if (g_RecordingFormat == RecordingFormat::MP4) {
std::filesystem::path currentPath{ g_RecordingSaveLocation };
g_RecordingSaveLocation = currentPath / DEFAULT_RECORDING_FILE;
}
}
}
}
// Always use appropriate default filename based on current format
std::filesystem::path currentPath{ g_RecordingSaveLocation };
const wchar_t* defaultFile = (g_RecordingFormat == RecordingFormat::GIF) ? DEFAULT_GIF_RECORDING_FILE : DEFAULT_RECORDING_FILE;
g_RecordingSaveLocation = currentPath.parent_path() / defaultFile;
auto suggestedName = GetUniqueRecordingFilename();
saveDialog->SetFileName( suggestedName.c_str() );
@@ -3696,9 +3727,15 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
}
else {
co_await file.MoveAndReplaceAsync( destFile );
g_RecordingSaveLocation = file.Path();
SaveToClipboard(g_RecordingSaveLocation.c_str(), hWnd);
co_await file.MoveAndReplaceAsync(destFile);
if (g_RecordingFormat == RecordingFormat::GIF) {
g_RecordingSaveLocationGIF = file.Path();
SaveToClipboard(g_RecordingSaveLocationGIF.c_str(), hWnd);
}
else {
g_RecordingSaveLocation = file.Path();
SaveToClipboard(g_RecordingSaveLocation.c_str(), hWnd);
}
}
g_bSaveInProgress = false;
@@ -4039,8 +4076,10 @@ LRESULT APIENTRY MainWndProc(
// Set g_RecordScaling based on the current recording format
if (g_RecordingFormat == RecordingFormat::GIF) {
g_RecordScaling = g_RecordScalingGIF;
g_RecordFrameRate = RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE;
} else {
g_RecordScaling = g_RecordScalingMP4;
g_RecordFrameRate = RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE;
}
// to support migrating from
@@ -6332,6 +6371,17 @@ LRESULT APIENTRY MainWndProc(
{
// Reload the settings. This message is called from PowerToys after a setting is changed by the user.
reg.ReadRegSettings(RegSettings);
if (g_RecordingFormat == RecordingFormat::GIF)
{
g_RecordScaling = g_RecordScalingGIF;
g_RecordFrameRate = RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE;
}
else
{
g_RecordScaling = g_RecordScalingMP4;
g_RecordFrameRate = RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE;
}
// Apply tray icon setting
EnableDisableTrayIcon(hWnd, g_ShowTrayIcon);

View File

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

View File

@@ -11,15 +11,15 @@ public class GalleryGridPropertiesViewModel : IGridPropertiesViewModel
{
private readonly ExtensionObject<IGalleryGridLayout> _model;
public bool ShowTitle { get; private set; }
public bool ShowSubtitle { get; private set; }
public GalleryGridPropertiesViewModel(IGalleryGridLayout galleryGridLayout)
{
_model = new(galleryGridLayout);
}
public bool ShowTitle { get; set; }
public bool ShowSubtitle { get; set; }
public void InitializeProperties()
{
var model = _model.Unsafe;

View File

@@ -6,5 +6,9 @@ namespace Microsoft.CmdPal.Core.ViewModels;
public interface IGridPropertiesViewModel
{
bool ShowTitle { get; }
bool ShowSubtitle { get; }
void InitializeProperties();
}

View File

@@ -10,10 +10,9 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
public partial class ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
: CommandItemViewModel(new(model), context)
public partial class ListItemViewModel : CommandItemViewModel
{
public new ExtensionObject<IListItem> Model { get; } = new(model);
public new ExtensionObject<IListItem> Model { get; }
public List<TagViewModel>? Tags { get; set; }
@@ -32,6 +31,40 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
public string AccessibleName { get; private set; } = string.Empty;
public bool ShowTitle { get; private set; }
public bool ShowSubtitle { get; private set; }
public bool LayoutShowsTitle
{
get;
set
{
if (SetProperty(ref field, value))
{
UpdateShowsTitle();
}
}
}
public bool LayoutShowsSubtitle
{
get;
set
{
if (SetProperty(ref field, value))
{
UpdateShowsSubtitle();
}
}
}
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
: base(new(model), context)
{
Model = new ExtensionObject<IListItem>(model);
}
public override void InitializeProperties()
{
if (IsInitialized)
@@ -93,16 +126,18 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
switch (propertyName)
{
case nameof(Tags):
case nameof(model.Tags):
UpdateTags(model.Tags);
break;
case nameof(TextToSuggest):
this.TextToSuggest = model.TextToSuggest ?? string.Empty;
case nameof(model.TextToSuggest):
TextToSuggest = model.TextToSuggest ?? string.Empty;
UpdateProperty(nameof(TextToSuggest));
break;
case nameof(Section):
this.Section = model.Section ?? string.Empty;
case nameof(model.Section):
Section = model.Section ?? string.Empty;
UpdateProperty(nameof(Section));
break;
case nameof(Details):
case nameof(model.Details):
var extensionDetails = model.Details;
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
Details?.InitializeProperties();
@@ -110,16 +145,24 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
UpdateProperty(nameof(HasDetails));
UpdateShowDetailsCommand();
break;
case nameof(MoreCommands):
case nameof(model.MoreCommands):
UpdateProperty(nameof(MoreCommands));
AddShowDetailsCommands();
break;
case nameof(Title):
case nameof(Subtitle):
case nameof(model.Title):
UpdateProperty(nameof(Title));
UpdateShowsTitle();
UpdateAccessibleName();
break;
case nameof(model.Subtitle):
UpdateProperty(nameof(Subtitle));
UpdateShowsSubtitle();
UpdateAccessibleName();
break;
default:
UpdateProperty(propertyName);
break;
}
UpdateProperty(propertyName);
}
// TODO: Do we want filters to match descriptions and other properties? Tags, etc... Yes?
@@ -206,11 +249,32 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
// many COM exception issues.
Tags = [.. newTags];
UpdateProperty(nameof(Tags));
UpdateProperty(nameof(HasTags));
// We're already in UI thread, so just raise the events
OnPropertyChanged(nameof(Tags));
OnPropertyChanged(nameof(HasTags));
});
}
private void UpdateShowsTitle()
{
var oldShowTitle = ShowTitle;
ShowTitle = LayoutShowsTitle;
if (oldShowTitle != ShowTitle)
{
UpdateProperty(nameof(ShowTitle));
}
}
private void UpdateShowsSubtitle()
{
var oldShowSubtitle = ShowSubtitle;
ShowSubtitle = LayoutShowsSubtitle && !string.IsNullOrWhiteSpace(Subtitle);
if (oldShowSubtitle != ShowSubtitle)
{
UpdateProperty(nameof(ShowSubtitle));
}
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();

View File

@@ -24,8 +24,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change
// https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support
[ObservableProperty]
public partial ObservableCollection<ListItemViewModel> FilteredItems { get; set; } = [];
public ObservableCollection<ListItemViewModel> FilteredItems { get; } = [];
public FiltersViewModel? Filters { get; set; }
@@ -224,6 +223,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
// TODO we can probably further optimize this by also keeping a
// HashSet of every ExtensionObject we currently have, and only
// building new viewmodels for the ones we haven't already built.
var showsTitle = GridProperties?.ShowTitle ?? true;
var showsSubtitle = GridProperties?.ShowSubtitle ?? true;
foreach (var item in newItems)
{
// Check for cancellation during item processing
@@ -237,6 +238,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
// If an item fails to load, silently ignore it.
if (viewModel.SafeFastInit())
{
viewModel.LayoutShowsTitle = showsTitle;
viewModel.LayoutShowsSubtitle = showsSubtitle;
newViewModels.Add(viewModel);
}
}
@@ -583,6 +586,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
GridProperties?.InitializeProperties();
UpdateProperty(nameof(GridProperties));
ApplyLayoutToItems();
ShowDetails = model.ShowDetails;
UpdateProperty(nameof(ShowDetails));
@@ -608,22 +612,15 @@ public partial class ListViewModel : PageViewModel, IDisposable
model.ItemsChanged += Model_ItemsChanged;
}
private IGridPropertiesViewModel? LoadGridPropertiesViewModel(IGridProperties? gridProperties)
private static IGridPropertiesViewModel? LoadGridPropertiesViewModel(IGridProperties? gridProperties)
{
if (gridProperties is IMediumGridLayout mediumGridLayout)
return gridProperties switch
{
return new MediumGridPropertiesViewModel(mediumGridLayout);
}
else if (gridProperties is IGalleryGridLayout galleryGridLayout)
{
return new GalleryGridPropertiesViewModel(galleryGridLayout);
}
else if (gridProperties is ISmallGridLayout smallGridLayout)
{
return new SmallGridPropertiesViewModel(smallGridLayout);
}
return null;
IMediumGridLayout mediumGridLayout => new MediumGridPropertiesViewModel(mediumGridLayout),
IGalleryGridLayout galleryGridLayout => new GalleryGridPropertiesViewModel(galleryGridLayout),
ISmallGridLayout smallGridLayout => new SmallGridPropertiesViewModel(smallGridLayout),
_ => null,
};
}
public void LoadMoreIfNeeded()
@@ -685,6 +682,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
GridProperties?.InitializeProperties();
UpdateProperty(nameof(IsGridView));
ApplyLayoutToItems();
break;
case nameof(ShowDetails):
ShowDetails = model.ShowDetails;
@@ -730,6 +728,21 @@ public partial class ListViewModel : PageViewModel, IDisposable
});
}
private void ApplyLayoutToItems()
{
lock (_listLock)
{
var showsTitle = GridProperties?.ShowTitle ?? true;
var showsSubtitle = GridProperties?.ShowSubtitle ?? true;
foreach (var item in Items)
{
item.LayoutShowsTitle = showsTitle;
item.LayoutShowsSubtitle = showsSubtitle;
}
}
}
public void Dispose()
{
GC.SuppressFinalize(this);

View File

@@ -11,13 +11,15 @@ public class MediumGridPropertiesViewModel : IGridPropertiesViewModel
{
private readonly ExtensionObject<IMediumGridLayout> _model;
public bool ShowTitle { get; private set; }
public bool ShowSubtitle => false;
public MediumGridPropertiesViewModel(IMediumGridLayout mediumGridLayout)
{
_model = new(mediumGridLayout);
}
public bool ShowTitle { get; set; }
public void InitializeProperties()
{
var model = _model.Unsafe;

View File

@@ -64,6 +64,8 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
public string Title { get => string.IsNullOrEmpty(field) ? Name : field; protected set; } = string.Empty;
public string Id { get; protected set; } = string.Empty;
// This property maps to `IPage.IsLoading`, but we want to expose our own
// `IsLoading` property as a combo of this value and `IsInitialized`
public bool ModelIsLoading { get; protected set; } = true;
@@ -142,6 +144,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
return; // throw?
}
Id = page.Id;
Name = page.Name;
ModelIsLoading = page.IsLoading;
Title = page.Title;

View File

@@ -11,6 +11,10 @@ public class SmallGridPropertiesViewModel : IGridPropertiesViewModel
{
private readonly ExtensionObject<ISmallGridLayout> _model;
public bool ShowTitle => false;
public bool ShowSubtitle => false;
public SmallGridPropertiesViewModel(ISmallGridLayout smallGridLayout)
{
_model = new(smallGridLayout);

View File

@@ -36,6 +36,7 @@ public partial class MainListPage : DynamicListPage,
"com.microsoft.cmdpal.builtin.websearch",
"com.microsoft.cmdpal.builtin.windowssettings",
"com.microsoft.cmdpal.builtin.datetime",
"com.microsoft.cmdpal.builtin.remotedesktop",
];
private readonly IServiceProvider _serviceProvider;
@@ -160,7 +161,7 @@ public partial class MainListPage : DynamicListPage,
{
return _tlcManager
.TopLevelCommands
.Where(tlc => !string.IsNullOrEmpty(tlc.Title))
.Where(tlc => !tlc.IsFallback && !string.IsNullOrEmpty(tlc.Title))
.ToArray();
}
}

View File

@@ -6,7 +6,9 @@ using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
@@ -15,6 +17,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class SettingsModel : ObservableObject
{
private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome";
[JsonIgnore]
public static readonly string FilePath;
@@ -30,8 +34,6 @@ public partial class SettingsModel : ObservableObject
public bool ShowAppDetails { get; set; }
public bool HotkeyGoesHome { get; set; }
public bool BackspaceGoesBack { get; set; }
public bool SingleClickActivates { get; set; }
@@ -56,6 +58,8 @@ public partial class SettingsModel : ObservableObject
public WindowPosition? LastWindowPosition { get; set; }
public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan;
// END SETTINGS
///////////////////////////////////////////////////////////////////////////
@@ -98,12 +102,29 @@ public partial class SettingsModel : ObservableObject
{
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel) ?? new();
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel);
var migratedAny = false;
try
{
if (JsonNode.Parse(jsonContent) is JsonObject root)
{
migratedAny |= ApplyMigrations(root, loaded);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Migration check failed: {ex}");
}
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
Debug.WriteLine("Loaded settings file");
return loaded ?? new();
if (migratedAny)
{
SaveSettings(loaded);
}
return loaded;
}
catch (Exception ex)
{
@@ -113,6 +134,51 @@ public partial class SettingsModel : ObservableObject
return new();
}
private static bool ApplyMigrations(JsonObject root, SettingsModel model)
{
var migrated = false;
// Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)
// The old 'HotkeyGoesHome' boolean indicated whether the "go home" action should happen immediately (true) or never (false).
// The new 'AutoGoHomeInterval' uses a TimeSpan: 'TimeSpan.Zero' means immediate, 'Timeout.InfiniteTimeSpan' means never.
migrated |= TryMigrate(
"Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)",
root,
model,
nameof(AutoGoHomeInterval),
DeprecatedHotkeyGoesHomeKey,
(settingsModel, goesHome) => settingsModel.AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan,
JsonSerializationContext.Default.Boolean);
return migrated;
}
private static bool TryMigrate<T>(string migrationName, JsonObject root, SettingsModel model, string newKey, string oldKey, Action<SettingsModel, T> apply, JsonTypeInfo<T> jsonTypeInfo)
{
try
{
// If new key already present, skip migration
if (root.ContainsKey(newKey) && root[newKey] is not null)
{
return false;
}
// If old key present, try to deserialize and apply
if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null)
{
var value = oldNode.Deserialize<T>(jsonTypeInfo);
apply(model, value!);
return true;
}
}
catch (Exception ex)
{
Logger.LogError($"Error during migration {migrationName}.", ex);
}
return false;
}
public static void SaveSettings(SettingsModel model)
{
if (string.IsNullOrEmpty(FilePath))
@@ -139,6 +205,9 @@ public partial class SettingsModel : ObservableObject
savedSettings[item.Key] = item.Value?.DeepClone();
}
// Remove deprecated keys
savedSettings.Remove(DeprecatedHotkeyGoesHomeKey);
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
File.WriteAllText(FilePath, serialized);

View File

@@ -11,6 +11,19 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class SettingsViewModel : INotifyPropertyChanged
{
private static readonly List<TimeSpan> AutoGoHomeIntervals =
[
Timeout.InfiniteTimeSpan,
TimeSpan.Zero,
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(20),
TimeSpan.FromSeconds(30),
TimeSpan.FromSeconds(60),
TimeSpan.FromSeconds(90),
TimeSpan.FromSeconds(120),
TimeSpan.FromSeconds(180),
];
private readonly SettingsModel _settings;
private readonly IServiceProvider _serviceProvider;
@@ -58,16 +71,6 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
public bool HotkeyGoesHome
{
get => _settings.HotkeyGoesHome;
set
{
_settings.HotkeyGoesHome = value;
Save();
}
}
public bool BackspaceGoesBack
{
get => _settings.BackspaceGoesBack;
@@ -138,6 +141,25 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
public int AutoGoBackIntervalIndex
{
get
{
var index = AutoGoHomeIntervals.IndexOf(_settings.AutoGoHomeInterval);
return index >= 0 ? index : 0;
}
set
{
if (value >= 0 && value < AutoGoHomeIntervals.Count)
{
_settings.AutoGoHomeInterval = AutoGoHomeIntervals[value];
}
Save();
}
}
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
public SettingsExtensionsViewModel Extensions { get; }

View File

@@ -2,21 +2,52 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Graphics;
namespace Microsoft.CmdPal.UI.ViewModels;
public sealed class WindowPosition
{
/// <summary>
/// Gets or sets left position in device pixels.
/// </summary>
public int X { get; set; }
/// <summary>
/// Gets or sets top position in device pixels.
/// </summary>
public int Y { get; set; }
/// <summary>
/// Gets or sets width in device pixels.
/// </summary>
public int Width { get; set; }
/// <summary>
/// Gets or sets height in device pixels.
/// </summary>
public int Height { get; set; }
/// <summary>
/// Gets or sets width of the screen in device pixels where the window is located.
/// </summary>
public int ScreenWidth { get; set; }
/// <summary>
/// Gets or sets height of the screen in device pixels where the window is located.
/// </summary>
public int ScreenHeight { get; set; }
/// <summary>
/// Gets or sets DPI (dots per inch) of the display where the window is located.
/// </summary>
public int Dpi { get; set; }
/// <summary>
/// Converts the window position properties to a <see cref="RectInt32"/> structure representing the physical window rectangle.
/// </summary>
public RectInt32 ToPhysicalWindowRectangle()
{
return new RectInt32(X, Y, Width, Height);
}
}

View File

@@ -13,6 +13,7 @@ using Microsoft.CmdPal.Ext.Calc;
using Microsoft.CmdPal.Ext.ClipboardHistory;
using Microsoft.CmdPal.Ext.Indexer;
using Microsoft.CmdPal.Ext.Registry;
using Microsoft.CmdPal.Ext.RemoteDesktop;
using Microsoft.CmdPal.Ext.Shell;
using Microsoft.CmdPal.Ext.System;
using Microsoft.CmdPal.Ext.TimeDate;
@@ -151,6 +152,7 @@ public partial class App : Application
services.AddSingleton<ICommandProvider, BuiltInsCommandProvider>();
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
// Models
services.AddSingleton<TopLevelCommandManager>();

View File

@@ -0,0 +1,257 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.DevRibbon"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
mc:Ignorable="d">
<UserControl.Resources>
<DataTemplate x:Key="LogEntryTemplate" x:DataType="viewModels:LogEntryViewModel">
<controls:SettingsExpander Description="{x:Bind Description}" Header="{x:Bind Header}">
<controls:SettingsExpander.HeaderIcon>
<FontIcon Glyph="{x:Bind SeverityGlyph}" />
</controls:SettingsExpander.HeaderIcon>
<controls:SettingsExpander.Items>
<controls:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Vertical">
<ScrollViewer
MaxWidth="1160"
HorizontalScrollMode="Auto"
VerticalScrollMode="Auto">
<TextBlock
FontFamily="Consolas"
FontSize="11"
Foreground="{ThemeResource SystemControlPageTextBaseMediumBrush}"
IsTextSelectionEnabled="True"
Text="{x:Bind Details}"
TextWrapping="NoWrap" />
</ScrollViewer>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
</DataTemplate>
<converters:BoolToVisibilityConverter
x:Key="InvertedBoolToVisibilityConverter"
FalseValue="Visible"
TrueValue="Collapsed" />
</UserControl.Resources>
<Grid>
<Border
x:Name="RootBorder"
Height="26"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="{ThemeResource SettingsCardBackground}"
BorderBrush="{ThemeResource SurfaceStrokeColorFlyoutBrush}"
BorderThickness="1,0,1,1"
CornerRadius="0,0,8,8"
Opacity="0.3">
<Button
Padding="0"
CornerRadius="0,0,8,8"
FontSize="11"
PointerEntered="DevRibbonButton_PointerEntered"
PointerExited="DevRibbonButton_PointerExited">
<StackPanel Orientation="Horizontal">
<StackPanel
Padding="8,4"
VerticalAlignment="Center"
Background="DarkOrange"
Orientation="Horizontal"
Visibility="{x:Bind VisibleIfGreaterThanZero(ViewModel.WarningCount), Mode=OneWay}">
<FontIcon
Margin="0,0,8,0"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Glyph="&#xE7BA;" />
<TextBlock VerticalAlignment="Center">
<Run Text="{x:Bind ViewModel.WarningCount, Mode=OneWay}" />
</TextBlock>
</StackPanel>
<StackPanel
Padding="8,4"
VerticalAlignment="Center"
Background="Maroon"
Orientation="Horizontal"
Visibility="{x:Bind VisibleIfGreaterThanZero(ViewModel.ErrorCount), Mode=OneWay}">
<FontIcon
Margin="0,0,8,0"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Glyph="&#xEA39;" />
<TextBlock VerticalAlignment="Center">
<Run Text="{x:Bind ViewModel.ErrorCount, Mode=OneWay}" />
</TextBlock>
</StackPanel>
<Border Padding="8,4">
<Border.Background>
<SolidColorBrush Color="{x:Bind ViewModel.TagColor}" />
</Border.Background>
<TextBlock Padding="4" VerticalAlignment="Center">
<Run Text="{x:Bind ViewModel.Tag}" />
</TextBlock>
</Border>
</StackPanel>
<Button.Flyout>
<Flyout
Placement="Bottom"
ShouldConstrainToRootBounds="False"
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
<Flyout.FlyoutPresenterStyle>
<Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
<Setter Property="MinWidth" Value="600" />
<Setter Property="MaxWidth" Value="1200" />
<Setter Property="Padding" Value="0" />
</Style>
</Flyout.FlyoutPresenterStyle>
<Grid x:Name="FlyoutContent">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Padding="16" Spacing="8">
<!-- Logs section -->
<TextBlock
Margin="1,0,0,6"
Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}"
Text="Logs" />
<ItemsControl ItemTemplate="{StaticResource LogEntryTemplate}" ItemsSource="{x:Bind ViewModel.LatestLogs, Mode=OneWay}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Command="{x:Bind ViewModel.OpenLogFileCommand}" Content="Open Log File" />
<Button Command="{x:Bind ViewModel.OpenLogFolderCommand}" Content="Open Log Folder" />
<Button Command="{x:Bind ViewModel.ResetErrorCountersCommand}" Content="Clear Counters" />
</StackPanel>
<!-- Build info section -->
<TextBlock Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}" Text="Build Info" />
<Border
Padding="16"
Background="{ThemeResource SettingsCardBackground}"
BorderBrush="{ThemeResource SettingsCardBorderBrush}"
BorderThickness="1">
<Grid ColumnSpacing="8">
<Grid.Resources>
<Style
x:Key="KeyTextBlockStyle"
BasedOn="{StaticResource CaptionTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
</Style>
<Style
x:Key="ValueTextBlockStyle"
BasedOn="{StaticResource CaptionTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextAlignment" Value="Right" />
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Style="{StaticResource KeyTextBlockStyle}"
Text="Configuration:" />
<TextBlock
Grid.Row="0"
Grid.Column="1"
Style="{StaticResource ValueTextBlockStyle}"
Text="{x:Bind ViewModel.BuildConfiguration, Mode=OneWay}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Style="{StaticResource KeyTextBlockStyle}"
Text="AOT:" />
<TextBlock
Grid.Row="1"
Grid.Column="1"
Style="{StaticResource ValueTextBlockStyle}"
Text="{x:Bind ViewModel.IsAot, Mode=OneWay}" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Style="{StaticResource KeyTextBlockStyle}"
Text="Trimmed:" />
<TextBlock
Grid.Row="2"
Grid.Column="1"
Style="{StaticResource ValueTextBlockStyle}"
Text="{x:Bind ViewModel.IsPublishTrimmed, Mode=OneWay}" />
</Grid>
</Border>
</StackPanel>
<!-- Footer -->
<Border
Grid.Row="1"
Padding="16"
Background="{ThemeResource SettingsCardBackground}"
BorderBrush="{ThemeResource SettingsCardBorderBrush}"
BorderThickness="1"
CornerRadius="{ThemeResource ControlCornerRadius}"
Visibility="{x:Bind ViewModel.IsAotReleaseConfiguration, Mode=OneWay, Converter={StaticResource InvertedBoolToVisibilityConverter}}">
<TextBlock Text="Warning: Test in Release/AOT configuration to verify everything works." TextWrapping="Wrap" />
</Border>
</Grid>
</Flyout>
</Button.Flyout>
</Button>
</Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="RootBorder"
Storyboard.TargetProperty="Opacity"
To="1.0"
Duration="0:0:0.1" />
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="SeverityStates">
<VisualState x:Name="NoLog" />
<VisualState x:Name="WarningLog">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SeverityIcon" Storyboard.TargetProperty="Glyph">
<DiscreteObjectKeyFrame KeyTime="0" Value="&#xE7BA;" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="ErrorLog">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SeverityIcon" Storyboard.TargetProperty="Glyph">
<DiscreteObjectKeyFrame KeyTime="0" Value="&#xEA39;" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
namespace Microsoft.CmdPal.UI.Controls;
internal sealed partial class DevRibbon : UserControl
{
public ViewModels.DevRibbonViewModel ViewModel { get; }
public DevRibbon()
{
InitializeComponent();
ViewModel = new ViewModels.DevRibbonViewModel();
if (FlyoutContent != null)
{
FlyoutContent.DataContext = ViewModel;
}
}
private void DevRibbonButton_PointerEntered(object sender, PointerRoutedEventArgs e)
{
VisualStateManager.GoToState(this, "PointerOver", true);
}
private void DevRibbonButton_PointerExited(object sender, PointerRoutedEventArgs e)
{
VisualStateManager.GoToState(this, "Normal", true);
}
private Visibility VisibleIfGreaterThanZero(int value)
{
return value > 0 ? Visibility.Visible : Visibility.Collapsed;
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI;
internal sealed partial class GridItemContainerStyleSelector : StyleSelector
{
public IGridPropertiesViewModel? GridProperties { get; set; }
public Style? Small { get; set; }
public Style? Medium { get; set; }
public Style? Gallery { get; set; }
protected override Style? SelectStyleCore(object item, DependencyObject container)
{
return GridProperties switch
{
SmallGridPropertiesViewModel => Small,
MediumGridPropertiesViewModel => Medium,
GalleryGridPropertiesViewModel => Gallery,
_ => Medium,
};
}
}

View File

@@ -20,21 +20,12 @@ internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
{
DataTemplate? dataTemplate = Medium;
if (GridProperties is SmallGridPropertiesViewModel)
return GridProperties switch
{
dataTemplate = Small;
}
else if (GridProperties is MediumGridPropertiesViewModel)
{
dataTemplate = Medium;
}
else if (GridProperties is GalleryGridPropertiesViewModel)
{
dataTemplate = Gallery;
}
return dataTemplate;
SmallGridPropertiesViewModel => Small,
MediumGridPropertiesViewModel => Medium,
GalleryGridPropertiesViewModel => Gallery,
_ => Medium,
};
}
}

View File

@@ -15,9 +15,12 @@ public class OpenPage : EventBase, IEvent
{
public int PageDepth { get; set; }
public OpenPage(int pageDepth)
public string Id { get; set; }
public OpenPage(int pageDepth, string id)
{
PageDepth = pageDepth;
Id = id;
EventName = "CmdPal_OpenPage";
}

View File

@@ -5,33 +5,151 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
x:Name="PageRoot"
Background="Transparent"
DataContext="{x:Bind ViewModel, Mode=OneWay}"
mc:Ignorable="d">
<Page.Resources>
<!-- TODO: Figure out what we want to do here for filtering/grouping and where -->
<!-- https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.data.collectionviewsource -->
<!--<CollectionViewSource
x:Name="ItemsCVS"
IsSourceGrouped="True"
Source="{x:Bind ViewModel.Items, Mode=OneWay}" />-->
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:StringVisibilityConverter
x:Key="StringVisibilityConverter"
EmptyValue="Collapsed"
NotEmptyValue="Visible" />
<!--
GridViewItemCornerRadius is the corner radius defined in GridView template; make
it bigger to match the radii of the gallery
-->
<CornerRadius x:Key="GalleryGridViewItemContainerCornerRadius">6</CornerRadius>
<CornerRadius x:Key="IconGridViewItemContainerCornerRadius">4</CornerRadius>
<CornerRadius x:Key="GalleryGridViewItemRadius">4</CornerRadius>
<CornerRadius x:Key="SmallGridViewItemCornerRadius">8</CornerRadius>
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
<Style x:Key="IconGridViewItemStyle" TargetType="GridViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewItem">
<ListViewItemPresenter
x:Name="Root"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
CheckBoxBorderBrush="{ThemeResource GridViewItemCheckBoxBorderBrush}"
CheckBoxBrush="{ThemeResource GridViewItemCheckBoxBrush}"
CheckBoxCornerRadius="{ThemeResource GridViewItemCheckBoxCornerRadius}"
CheckBoxDisabledBorderBrush="{ThemeResource GridViewItemCheckBoxDisabledBorderBrush}"
CheckBoxDisabledBrush="{ThemeResource GridViewItemCheckBoxDisabledBrush}"
CheckBoxPointerOverBorderBrush="{ThemeResource GridViewItemCheckBoxPointerOverBorderBrush}"
CheckBoxPointerOverBrush="{ThemeResource GridViewItemCheckBoxPointerOverBrush}"
CheckBoxPressedBorderBrush="{ThemeResource GridViewItemCheckBoxPressedBorderBrush}"
CheckBoxPressedBrush="{ThemeResource GridViewItemCheckBoxPressedBrush}"
CheckBoxSelectedBrush="{ThemeResource GridViewItemCheckBoxSelectedBrush}"
CheckBoxSelectedDisabledBrush="{ThemeResource GridViewItemCheckBoxSelectedDisabledBrush}"
CheckBoxSelectedPointerOverBrush="{ThemeResource GridViewItemCheckBoxSelectedPointerOverBrush}"
CheckBoxSelectedPressedBrush="{ThemeResource GridViewItemCheckBoxSelectedPressedBrush}"
CheckBrush="{ThemeResource GridViewItemCheckBrush}"
CheckDisabledBrush="{ThemeResource GridViewItemCheckDisabledBrush}"
CheckMode="{ThemeResource GridViewItemCheckMode}"
CheckPressedBrush="{ThemeResource GridViewItemCheckPressedBrush}"
ContentMargin="{TemplateBinding Padding}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{StaticResource IconGridViewItemContainerCornerRadius}"
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
DragBackground="{ThemeResource GridViewItemDragBackground}"
DragForeground="{ThemeResource GridViewItemDragForeground}"
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
FocusBorderBrush="{ThemeResource GridViewItemFocusBorderBrush}"
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
FocusVisualPrimaryBrush="{TemplateBinding FocusVisualPrimaryBrush}"
FocusVisualPrimaryThickness="{TemplateBinding FocusVisualPrimaryThickness}"
FocusVisualSecondaryBrush="{TemplateBinding FocusVisualSecondaryBrush}"
FocusVisualSecondaryThickness="{TemplateBinding FocusVisualSecondaryThickness}"
PlaceholderBackground="{ThemeResource GridViewItemPlaceholderBackground}"
PointerOverBackground="{ThemeResource GridViewItemBackgroundPointerOver}"
PointerOverBorderBrush="{ThemeResource GridViewItemPointerOverBorderBrush}"
PointerOverForeground="{ThemeResource GridViewItemForegroundPointerOver}"
PressedBackground="{ThemeResource GridViewItemBackgroundPressed}"
ReorderHintOffset="{ThemeResource GridViewItemReorderHintThemeOffset}"
SelectedBackground="{ThemeResource GridViewItemBackgroundSelected}"
SelectedBorderBrush="{ThemeResource GridViewItemSelectedBorderBrush}"
SelectedBorderThickness="{ThemeResource GridViewItemSelectedBorderThickness}"
SelectedDisabledBackground="{ThemeResource GridViewItemBackgroundSelectedDisabled}"
SelectedDisabledBorderBrush="{ThemeResource GridViewItemSelectedDisabledBorderBrush}"
SelectedForeground="{ThemeResource GridViewItemForegroundSelected}"
SelectedInnerBorderBrush="{ThemeResource GridViewItemSelectedInnerBorderBrush}"
SelectedPointerOverBackground="{ThemeResource GridViewItemBackgroundSelectedPointerOver}"
SelectedPointerOverBorderBrush="{ThemeResource GridViewItemSelectedPointerOverBorderBrush}"
SelectedPressedBackground="{ThemeResource GridViewItemBackgroundSelectedPressed}"
SelectedPressedBorderBrush="{ThemeResource GridViewItemSelectedPressedBorderBrush}"
SelectionCheckMarkVisualEnabled="{ThemeResource GridViewItemSelectionCheckMarkVisualEnabled}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="GalleryGridViewItemStyle" TargetType="GridViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewItem">
<ListViewItemPresenter
x:Name="Root"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
CheckBoxBorderBrush="{ThemeResource GridViewItemCheckBoxBorderBrush}"
CheckBoxBrush="{ThemeResource GridViewItemCheckBoxBrush}"
CheckBoxCornerRadius="{ThemeResource GridViewItemCheckBoxCornerRadius}"
CheckBoxDisabledBorderBrush="{ThemeResource GridViewItemCheckBoxDisabledBorderBrush}"
CheckBoxDisabledBrush="{ThemeResource GridViewItemCheckBoxDisabledBrush}"
CheckBoxPointerOverBorderBrush="{ThemeResource GridViewItemCheckBoxPointerOverBorderBrush}"
CheckBoxPointerOverBrush="{ThemeResource GridViewItemCheckBoxPointerOverBrush}"
CheckBoxPressedBorderBrush="{ThemeResource GridViewItemCheckBoxPressedBorderBrush}"
CheckBoxPressedBrush="{ThemeResource GridViewItemCheckBoxPressedBrush}"
CheckBoxSelectedBrush="{ThemeResource GridViewItemCheckBoxSelectedBrush}"
CheckBoxSelectedDisabledBrush="{ThemeResource GridViewItemCheckBoxSelectedDisabledBrush}"
CheckBoxSelectedPointerOverBrush="{ThemeResource GridViewItemCheckBoxSelectedPointerOverBrush}"
CheckBoxSelectedPressedBrush="{ThemeResource GridViewItemCheckBoxSelectedPressedBrush}"
CheckBrush="{ThemeResource GridViewItemCheckBrush}"
CheckDisabledBrush="{ThemeResource GridViewItemCheckDisabledBrush}"
CheckMode="{ThemeResource GridViewItemCheckMode}"
CheckPressedBrush="{ThemeResource GridViewItemCheckPressedBrush}"
ContentMargin="{TemplateBinding Padding}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{StaticResource GalleryGridViewItemContainerCornerRadius}"
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
DragBackground="{ThemeResource GridViewItemDragBackground}"
DragForeground="{ThemeResource GridViewItemDragForeground}"
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
FocusBorderBrush="{ThemeResource GridViewItemFocusBorderBrush}"
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
FocusVisualPrimaryBrush="{TemplateBinding FocusVisualPrimaryBrush}"
FocusVisualPrimaryThickness="{TemplateBinding FocusVisualPrimaryThickness}"
FocusVisualSecondaryBrush="{TemplateBinding FocusVisualSecondaryBrush}"
FocusVisualSecondaryThickness="{TemplateBinding FocusVisualSecondaryThickness}"
PlaceholderBackground="{ThemeResource GridViewItemPlaceholderBackground}"
PointerOverBackground="{ThemeResource GridViewItemBackgroundPointerOver}"
PointerOverBorderBrush="{ThemeResource GridViewItemPointerOverBorderBrush}"
PointerOverForeground="{ThemeResource GridViewItemForegroundPointerOver}"
PressedBackground="{ThemeResource GridViewItemBackgroundPressed}"
ReorderHintOffset="{ThemeResource GridViewItemReorderHintThemeOffset}"
SelectedBackground="{ThemeResource GridViewItemBackgroundSelected}"
SelectedBorderBrush="{ThemeResource GridViewItemSelectedBorderBrush}"
SelectedBorderThickness="{ThemeResource GridViewItemSelectedBorderThickness}"
SelectedDisabledBackground="{ThemeResource GridViewItemBackgroundSelectedDisabled}"
SelectedDisabledBorderBrush="{ThemeResource GridViewItemSelectedDisabledBorderBrush}"
SelectedForeground="{ThemeResource GridViewItemForegroundSelected}"
SelectedInnerBorderBrush="{ThemeResource GridViewItemSelectedInnerBorderBrush}"
SelectedPointerOverBackground="{ThemeResource GridViewItemBackgroundSelectedPointerOver}"
SelectedPointerOverBorderBrush="{ThemeResource GridViewItemSelectedPointerOverBorderBrush}"
SelectedPressedBackground="{ThemeResource GridViewItemBackgroundSelectedPressed}"
SelectedPressedBorderBrush="{ThemeResource GridViewItemSelectedPressedBorderBrush}"
SelectionCheckMarkVisualEnabled="{ThemeResource GridViewItemSelectionCheckMarkVisualEnabled}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel">
<cpcontrols:Tag
@@ -48,10 +166,17 @@
x:Key="GridItemTemplateSelector"
x:DataType="coreViewModels:ListItemViewModel"
Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
GridProperties="{x:Bind ViewModel.GridProperties}"
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
Medium="{StaticResource MediumGridItemViewModelTemplate}"
Small="{StaticResource SmallGridItemViewModelTemplate}" />
<cmdpalUI:GridItemContainerStyleSelector
x:Key="GridItemContainerStyleSelector"
Gallery="{StaticResource GalleryGridViewItemStyle}"
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
Medium="{StaticResource IconGridViewItemStyle}"
Small="{StaticResource IconGridViewItemStyle}" />
<!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
<DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<Grid
@@ -94,7 +219,7 @@
Text="{x:Bind Subtitle, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"
Visibility="{x:Bind Subtitle, Mode=OneWay, Converter={StaticResource StringVisibilityConverter}}" />
Visibility="{x:Bind ShowSubtitle, Mode=OneWay}" />
</StackPanel>
<ItemsControl
@@ -124,11 +249,11 @@
Padding="8,16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}"
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
BorderThickness="0"
CornerRadius="8"
CornerRadius="{StaticResource SmallGridViewItemCornerRadius}"
Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}">
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
<cpcontrols:IconBox
x:Name="GridIconBorder"
@@ -145,23 +270,22 @@
</DataTemplate>
<DataTemplate x:Key="MediumGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<StackPanel
<Grid
Width="100"
Height="100"
Padding="8,16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}"
BorderThickness="0"
CornerRadius="8"
Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}">
Padding="8"
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
CornerRadius="{StaticResource MediumGridViewItemCornerRadius}"
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<cpcontrols:IconBox
x:Name="GridIconBorder"
Grid.Row="0"
Width="36"
Height="36"
Margin="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CharacterSpacing="12"
@@ -169,21 +293,20 @@
Foreground="{ThemeResource TextFillColorPrimary}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock
x:Name="TitleTextBlock"
MaxHeight="40"
Margin="0,8,0,4"
Grid.Row="1"
Height="32"
Margin="0,8,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CharacterSpacing="12"
FontSize="12"
Text="{x:Bind Title}"
Text="{x:Bind Title, Mode=OneWay}"
TextAlignment="Center"
TextTrimming="WordEllipsis"
TextWrapping="Wrap"
Visibility="{Binding ElementName=PageRoot, Path=DataContext.GridProperties.ShowTitle, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</StackPanel>
Visibility="{x:Bind LayoutShowsTitle, Mode=OneWay}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="GalleryGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
@@ -193,11 +316,11 @@
Padding="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}"
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
BorderThickness="0"
CornerRadius="4"
CornerRadius="{StaticResource GalleryGridViewItemRadius}"
Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}">
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
<Grid
Width="160"
@@ -205,12 +328,8 @@
Margin="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CornerRadius="4">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
CornerRadius="{StaticResource GalleryGridViewItemRadius}">
<Viewbox
Grid.Row="1"
HorizontalAlignment="Center"
Stretch="UniformToFill"
StretchDirection="Both">
@@ -222,35 +341,39 @@
</Viewbox>
</Grid>
<StackPanel Padding="4" Orientation="Vertical">
<StackPanel
Padding="4"
Orientation="Vertical"
Spacing="4"
Visibility="{x:Bind help:BindTransformers.VisibleWhenAny(ShowTitle, ShowSubtitle)}">
<TextBlock
x:Name="TitleTextBlock"
MaxWidth="152"
MaxHeight="40"
Margin="0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
CharacterSpacing="12"
FontSize="14"
Foreground="{ThemeResource TextFillColorPrimary}"
Text="{x:Bind Title}"
Text="{x:Bind Title, Mode=OneWay}"
TextAlignment="Center"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap" />
TextWrapping="NoWrap"
Visibility="{x:Bind ShowTitle, Mode=OneWay}" />
<TextBlock
x:Name="SubTitleTextBlock"
MaxWidth="152"
MaxHeight="40"
Margin="0,4,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
CharacterSpacing="11"
FontSize="11"
Foreground="{ThemeResource TextFillColorTertiary}"
Text="{x:Bind Subtitle}"
Text="{x:Bind Subtitle, Mode=OneWay}"
TextAlignment="Center"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap" />
TextWrapping="NoWrap"
Visibility="{x:Bind ShowSubtitle, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</DataTemplate>
@@ -295,6 +418,7 @@
IsDoubleTapEnabled="True"
IsItemClickEnabled="True"
ItemClick="Items_ItemClick"
ItemContainerStyleSelector="{StaticResource GridItemContainerStyleSelector}"
ItemTemplateSelector="{StaticResource GridItemTemplateSelector}"
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
RightTapped="Items_RightTapped"
@@ -302,6 +426,7 @@
<GridView.ItemContainerTransitions>
<TransitionCollection />
</GridView.ItemContainerTransitions>
<GridView.ItemContainerStyle />
</GridView>
</controls:Case>
</controls:SwitchPresenter>

View File

@@ -15,4 +15,7 @@ internal static class BindTransformers
public static Visibility EmptyOrWhitespaceToCollapsed(string? input)
=> string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible;
public static Visibility VisibleWhenAny(bool value1, bool value2)
=> (value1 || value2) ? Visibility.Visible : Visibility.Collapsed;
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Microsoft.CmdPal.UI.Helpers;
internal static class BuildInfo
{
#if DEBUG
public const string Configuration = "Debug";
#else
public const string Configuration = "Release";
#endif
// Runtime AOT detection
public static bool IsNativeAot => !RuntimeFeature.IsDynamicCodeSupported;
// From assembly metadata (build-time values)
public static bool PublishTrimmed => GetBoolMetadata("PublishTrimmed", false);
// From assembly metadata (build-time values)
public static bool PublishAot => GetBoolMetadata("PublishAot", false);
public static bool IsCiBuild => GetBoolMetadata("CIBuild", false);
private static string? GetMetadata(string key) =>
Assembly.GetExecutingAssembly()
.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(a => a.Key == key)?.Value;
private static bool GetBoolMetadata(string key, bool defaultValue) =>
bool.TryParse(GetMetadata(key), out var result) ? result : defaultValue;
}

View File

@@ -14,5 +14,7 @@
Activated="MainWindow_Activated"
Closed="MainWindow_Closed"
mc:Ignorable="d">
<pages:ShellPage x:Name="RootShellPage" />
<Grid x:Name="RootElement">
<pages:ShellPage />
</Grid>
</winuiex:WindowEx>

View File

@@ -11,6 +11,7 @@ using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.Events;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
@@ -18,6 +19,7 @@ using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Input;
@@ -33,6 +35,8 @@ using Windows.UI.WindowManagement;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.HiDpi;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging;
using WinRT;
@@ -48,10 +52,14 @@ public sealed partial class MainWindow : WindowEx,
IRecipient<QuitMessage>,
IDisposable
{
private const int DefaultWidth = 800;
private const int DefaultHeight = 480;
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
private readonly uint WM_TASKBAR_RESTART;
private readonly HWND _hwnd;
private readonly DispatcherTimer _autoGoHomeTimer;
private readonly WNDPROC? _hotkeyWndProc;
private readonly WNDPROC? _originalWndProc;
private readonly List<TopLevelHotkey> _hotkeys = [];
@@ -62,6 +70,7 @@ public sealed partial class MainWindow : WindowEx,
private DesktopAcrylicController? _acrylicController;
private SystemBackdropConfiguration? _configurationSource;
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
private WindowPosition _currentWindowPosition = new();
@@ -69,6 +78,9 @@ public sealed partial class MainWindow : WindowEx,
{
InitializeComponent();
_autoGoHomeTimer = new DispatcherTimer();
_autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick;
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
unsafe
@@ -102,7 +114,7 @@ public sealed partial class MainWindow : WindowEx,
ExtendsContentIntoTitleBar = true;
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
SizeChanged += WindowSizeChanged;
RootShellPage.Loaded += RootShellPage_Loaded;
RootElement.Loaded += RootElementLoaded;
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
@@ -119,7 +131,7 @@ public sealed partial class MainWindow : WindowEx,
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
// Make sure that we update the acrylic theme when the OS theme changes
RootShellPage.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic);
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic);
// Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
@@ -135,6 +147,15 @@ public sealed partial class MainWindow : WindowEx,
HideWindow();
}
private void OnAutoGoHomeTimerOnTick(object? s, object e)
{
_autoGoHomeTimer.Stop();
// BEAR LOADING: Focus Search must be suppressed here; otherwise it may steal focus (for example, from the system tray icon)
// and prevent the user from opening its context menu.
WeakReferenceMessenger.Default.Send(new GoHomeMessage(WithAnimation: false, FocusSearch: false));
}
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
{
if (e.Key == VirtualKey.GoBack)
@@ -145,11 +166,18 @@ public sealed partial class MainWindow : WindowEx,
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings();
private void RootShellPage_Loaded(object sender, RoutedEventArgs e) =>
private void RootElementLoaded(object sender, RoutedEventArgs e)
{
// Now that our content has loaded, we can update our draggable regions
UpdateRegionsForCustomTitleBar();
// Add dev ribbon if enabled
if (!BuildInfo.IsCiBuild)
{
RootElement.Children.Add(new DevRibbon { Margin = new Thickness(-1, -1, 120, -1) });
}
}
private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
private void PositionCentered()
@@ -173,22 +201,8 @@ public sealed partial class MainWindow : WindowEx,
return;
}
AppWindow.Resize(new SizeInt32 { Width = savedPosition.Width, Height = savedPosition.Height });
var savedRect = new RectInt32(savedPosition.X, savedPosition.Y, savedPosition.Width, savedPosition.Height);
var displayArea = DisplayArea.GetFromRect(savedRect, DisplayAreaFallback.Nearest);
var workArea = displayArea.WorkArea;
var maxX = workArea.X + Math.Max(0, workArea.Width - savedPosition.Width);
var maxY = workArea.Y + Math.Max(0, workArea.Height - savedPosition.Height);
var targetPoint = new PointInt32
{
X = Math.Clamp(savedPosition.X, workArea.X, maxX),
Y = Math.Clamp(savedPosition.Y, workArea.Y, maxY),
};
AppWindow.Move(targetPoint);
var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi);
AppWindow.MoveAndResize(newRect);
}
private void PositionCentered(DisplayArea displayArea)
@@ -207,12 +221,16 @@ public sealed partial class MainWindow : WindowEx,
private void UpdateWindowPositionInMemory()
{
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
_currentWindowPosition = new WindowPosition
{
X = AppWindow.Position.X,
Y = AppWindow.Position.Y,
Width = AppWindow.Size.Width,
Height = AppWindow.Size.Height,
Dpi = (int)this.GetDpiForWindow(),
ScreenWidth = displayArea.WorkArea.Width,
ScreenHeight = displayArea.WorkArea.Height,
};
}
@@ -224,6 +242,9 @@ public sealed partial class MainWindow : WindowEx,
App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon);
_ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
_autoGoHomeInterval = settings.AutoGoHomeInterval;
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
}
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
@@ -283,6 +304,8 @@ public sealed partial class MainWindow : WindowEx,
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
{
StopAutoGoHome();
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
// Remember, IsIconic == "minimized", which is entirely different state
@@ -300,8 +323,8 @@ public sealed partial class MainWindow : WindowEx,
if (target == MonitorBehavior.ToLast)
{
AppWindow.Resize(new SizeInt32 { Width = _currentWindowPosition.Width, Height = _currentWindowPosition.Height });
AppWindow.Move(new PointInt32 { X = _currentWindowPosition.X, Y = _currentWindowPosition.Y });
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
AppWindow.MoveAndResize(newRect);
}
else
{
@@ -330,6 +353,114 @@ public sealed partial class MainWindow : WindowEx,
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
}
/// <summary>
/// Ensures that the window rectangle is visible on-screen.
/// </summary>
/// <param name="windowRect">The window rectangle in physical pixels.</param>
/// <param name="originalScreen">The desktop area the window was positioned on.</param>
/// <param name="originalDpi">The window's original DPI.</param>
/// <returns>
/// A window rectangle in physical pixels, moved to the nearest display and resized
/// if the DPI has changed.
/// </returns>
private static RectInt32 EnsureWindowIsVisible(RectInt32 windowRect, SizeInt32 originalScreen, int originalDpi)
{
var displayArea = DisplayArea.GetFromRect(windowRect, DisplayAreaFallback.Nearest);
if (displayArea is null)
{
return windowRect;
}
var workArea = displayArea.WorkArea;
if (workArea.Width <= 0 || workArea.Height <= 0)
{
// Fallback, nothing reasonable to do
return windowRect;
}
var effectiveDpi = GetEffectiveDpiFromDisplayId(displayArea);
if (originalDpi <= 0)
{
originalDpi = effectiveDpi; // use current DPI as baseline (no scaling adjustment needed)
}
var hasInvalidSize = windowRect.Width <= 0 || windowRect.Height <= 0;
if (hasInvalidSize)
{
windowRect = new RectInt32(windowRect.X, windowRect.Y, DefaultWidth, DefaultHeight);
}
// If we have a DPI change, scale the window rectangle accordingly
if (effectiveDpi != originalDpi)
{
var scalingFactor = effectiveDpi / (double)originalDpi;
windowRect = new RectInt32(
(int)Math.Round(windowRect.X * scalingFactor),
(int)Math.Round(windowRect.Y * scalingFactor),
(int)Math.Round(windowRect.Width * scalingFactor),
(int)Math.Round(windowRect.Height * scalingFactor));
}
var targetWidth = Math.Min(windowRect.Width, workArea.Width);
var targetHeight = Math.Min(windowRect.Height, workArea.Height);
// Ensure at least some minimum visible area (e.g., 100 pixels)
// This helps prevent the window from being entirely offscreen, regardless of display scaling.
const int minimumVisibleSize = 100;
var isOffscreen =
windowRect.X + minimumVisibleSize > workArea.X + workArea.Width ||
windowRect.X + windowRect.Width - minimumVisibleSize < workArea.X ||
windowRect.Y + minimumVisibleSize > workArea.Y + workArea.Height ||
windowRect.Y + windowRect.Height - minimumVisibleSize < workArea.Y;
// if the work area size has changed, re-center the window
var workAreaSizeChanged =
originalScreen.Width != workArea.Width ||
originalScreen.Height != workArea.Height;
int targetX;
int targetY;
var recenter = isOffscreen || workAreaSizeChanged || hasInvalidSize;
if (recenter)
{
targetX = workArea.X + ((workArea.Width - targetWidth) / 2);
targetY = workArea.Y + ((workArea.Height - targetHeight) / 2);
}
else
{
targetX = windowRect.X;
targetY = windowRect.Y;
}
return new RectInt32(targetX, targetY, targetWidth, targetHeight);
}
private static int GetEffectiveDpiFromDisplayId(DisplayArea displayArea)
{
var effectiveDpi = 96;
var hMonitor = (HMONITOR)Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId);
if (!hMonitor.IsNull)
{
var hr = PInvoke.GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _);
if (hr == 0)
{
effectiveDpi = (int)dpiX;
}
else
{
Logger.LogWarning($"GetDpiForMonitor failed with HRESULT: 0x{hr.Value:X8} on display {displayArea.DisplayId}");
}
}
if (effectiveDpi <= 0)
{
effectiveDpi = 96;
}
return effectiveDpi;
}
private DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
{
// Leaving a note here, in case we ever need it:
@@ -429,6 +560,25 @@ public sealed partial class MainWindow : WindowEx,
// If the window was not cloaked, then leave it hidden.
// Sure, it's not ideal, but at least it's not visible.
}
// Start auto-go-home timer
RestartAutoGoHome();
}
private void StopAutoGoHome()
{
_autoGoHomeTimer.Stop();
}
private void RestartAutoGoHome()
{
if (_autoGoHomeInterval == Timeout.InfiniteTimeSpan)
{
return;
}
_autoGoHomeTimer.Stop();
_autoGoHomeTimer.Start();
}
private bool Cloak()
@@ -479,6 +629,9 @@ public sealed partial class MainWindow : WindowEx,
Y = _currentWindowPosition.Y,
Width = _currentWindowPosition.Width,
Height = _currentWindowPosition.Height,
Dpi = _currentWindowPosition.Dpi,
ScreenWidth = _currentWindowPosition.ScreenWidth,
ScreenHeight = _currentWindowPosition.ScreenHeight,
};
SettingsModel.SaveSettings(settings);
@@ -513,28 +666,28 @@ public sealed partial class MainWindow : WindowEx,
private void UpdateRegionsForCustomTitleBar()
{
// Specify the interactive regions of the title bar.
var scaleAdjustment = RootShellPage.XamlRoot.RasterizationScale;
var scaleAdjustment = RootElement.XamlRoot.RasterizationScale;
// Get the rectangle around our XAML content. We're going to mark this
// rectangle as "Passthrough", so that the normal window operations
// (resizing, dragging) don't apply in this space.
var transform = RootShellPage.TransformToVisual(null);
var transform = RootElement.TransformToVisual(null);
// Reserve 16px of space at the top for dragging.
var topHeight = 16;
var bounds = transform.TransformBounds(new Rect(
0,
topHeight,
RootShellPage.ActualWidth,
RootShellPage.ActualHeight));
RootElement.ActualWidth,
RootElement.ActualHeight));
var contentRect = GetRect(bounds, scaleAdjustment);
var rectArray = new RectInt32[] { contentRect };
var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(this.AppWindow.Id);
nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rectArray);
// Add a drag-able region on top
var w = RootShellPage.ActualWidth;
_ = RootShellPage.ActualHeight;
var w = RootElement.ActualWidth;
_ = RootElement.ActualHeight;
var dragSides = new RectInt32[]
{
GetRect(new Rect(0, 0, w, topHeight), scaleAdjustment), // the top, {topHeight=16} tall

View File

@@ -15,6 +15,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<LangVersion>preview</LangVersion>
<Version>$(CmdPalVersion)</Version>
@@ -25,10 +26,10 @@
</PropertyGroup>
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
<!-- <PropertyGroup>
<!--<PropertyGroup>
<EnableCmdPalAOT>true</EnableCmdPalAOT>
<CIBuild>true</CIBuild>
</PropertyGroup> -->
<GeneratePackageLocally>true</GeneratePackageLocally>
</PropertyGroup>-->
<PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'">
<SelfContained>true</SelfContained>
@@ -37,7 +38,7 @@
<PublishAot>true</PublishAot>
</PropertyGroup>
<PropertyGroup Condition="'$(CIBuild)'=='true'">
<PropertyGroup Condition="'$(CIBuild)' == 'true' or '$(GeneratePackageLocally)' == 'true'">
<GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild>
<AppxBundle>Never</AppxBundle>
<AppxPackageTestDir>$(OutputPath)\AppPackages\Microsoft.CmdPal.UI_$(Version)_Test\</AppxPackageTestDir>
@@ -66,6 +67,7 @@
<ItemGroup>
<None Remove="Controls\ActionBar.xaml" />
<None Remove="Controls\DevRibbon.xaml" />
<None Remove="Controls\KeyVisual\KeyCharPresenter.xaml" />
<None Remove="Controls\SearchBar.xaml" />
<None Remove="IsEnabledTextBlock.xaml" />
@@ -118,6 +120,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
@@ -167,6 +170,9 @@
<Page Update="Controls\SearchBar.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\DevRibbon.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\TextBox.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
@@ -234,4 +240,24 @@
</ItemGroup>
<!-- </AdaptiveCardsWorkaround> -->
<!-- Metadata for build information -->
<ItemGroup>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>PublishTrimmed</_Parameter1>
<_Parameter2>$(PublishTrimmed)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>PublishAot</_Parameter1>
<_Parameter2>$(PublishAot)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>CIBuild</_Parameter1>
<_Parameter2>$(CIBuild)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>CommandPaletteBranding</_Parameter1>
<_Parameter2>$(CommandPaletteBranding)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Project>

View File

@@ -21,7 +21,6 @@ using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Animation;
@@ -160,7 +159,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
new AsyncNavigationRequest(message.Page, message.CancellationToken),
message.WithAnimation ? DefaultPageAnimation : _noAnimation);
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth));
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth, message.Page.Id));
if (!ViewModel.IsNested)
{
@@ -346,7 +345,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
// Depending on the settings, either
// * Go home, or
// * Select the search text (if we should remain open on this page)
if (settings.HotkeyGoesHome)
if (settings.AutoGoHomeInterval == TimeSpan.Zero)
{
GoHome(false);
}
@@ -655,15 +654,15 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
e.Handled = true;
break;
default:
{
// The CommandBar is responsible for handling all the item keybindings,
// since the bound context item may need to then show another
// context menu
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
WeakReferenceMessenger.Default.Send(msg);
e.Handled = msg.Handled;
break;
}
{
// The CommandBar is responsible for handling all the item keybindings,
// since the bound context item may need to then show another
// context menu
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
WeakReferenceMessenger.Default.Send(msg);
e.Handled = msg.Handled;
break;
}
}
}

View File

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

View File

@@ -51,8 +51,18 @@
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="Settings_GeneralPage_GoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE80F;}">
<ToggleSwitch IsOn="{x:Bind viewModel.HotkeyGoesHome, Mode=TwoWay}" />
<controls:SettingsCard x:Uid="Settings_GeneralPage_AutoGoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE80F;}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind viewModel.AutoGoBackIntervalIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Never" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Immediately" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After10Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After20Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After30Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After60Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After90Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After120Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After180Seconds" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_HighlightSearch_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE933;}">
<ToggleSwitch IsOn="{x:Bind viewModel.HighlightSearchOnActivate, Mode=TwoWay}" />

View File

@@ -344,12 +344,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard.Description" xml:space="preserve">
<value>Preventing disruption of the program running in fullscreen by unintentional activation of shortcut</value>
</data>
<data name="Settings_GeneralPage_GoHome_SettingsCard.Header" xml:space="preserve">
<value>Go home when activated</value>
</data>
<data name="Settings_GeneralPage_GoHome_SettingsCard.Description" xml:space="preserve">
<value>Automatically opens the home page upon activation</value>
</data>
<data name="Settings_GeneralPage_HighlightSearch_SettingsCard.Header" xml:space="preserve">
<value>Highlight search on activate</value>
</data>
@@ -523,4 +517,37 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="GlobalErrorHandler_CrashMessageBox_Caption" xml:space="preserve">
<value>Command Palette - Fatal error</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_Never.Content" xml:space="preserve">
<value>Never</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_Immediately.Content" xml:space="preserve">
<value>Immediately</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After10Seconds.Content" xml:space="preserve">
<value>10 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After20Seconds.Content" xml:space="preserve">
<value>20 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After30Seconds.Content" xml:space="preserve">
<value>30 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After60Seconds.Content" xml:space="preserve">
<value>60 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After90Seconds.Content" xml:space="preserve">
<value>90 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After120Seconds.Content" xml:space="preserve">
<value>2 minutes</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After180Seconds.Content" xml:space="preserve">
<value>3 minutes</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_SettingsCard.Header" xml:space="preserve">
<value>Automatically return home</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_SettingsCard.Description" xml:space="preserve">
<value>Automatically returns to home page after a period of inactivity when Command Palette is closed</value>
</data>
</root>

View File

@@ -0,0 +1,190 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ManagedCommon;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.UI;
using Windows.System;
using Windows.UI;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
namespace Microsoft.CmdPal.UI.ViewModels;
internal sealed partial class DevRibbonViewModel : ObservableObject
{
private const int MaxLogEntries = 2;
private const string Release = "Release";
private const string Debug = "Debug";
private static readonly Color ReleaseAotColor = ColorHelper.FromArgb(255, 124, 58, 237);
private static readonly Color ReleaseColor = ColorHelper.FromArgb(255, 51, 65, 85);
private static readonly Color DebugAotColor = ColorHelper.FromArgb(255, 99, 102, 241);
private static readonly Color DebugColor = ColorHelper.FromArgb(255, 107, 114, 128);
private readonly DispatcherQueue _dispatcherQueue;
public DevRibbonViewModel()
{
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
Trace.Listeners.Add(new DevRibbonTraceListener(this));
var configLabel = BuildConfiguration == Release ? "RLS" : "DBG"; /* #no-spell-check-line */
var aotLabel = BuildInfo.IsNativeAot ? "⚡AOT" : "NO AOT";
Tag = $"{configLabel} | {aotLabel}";
TagColor = (BuildConfiguration, BuildInfo.IsNativeAot) switch
{
(Release, true) => ReleaseAotColor,
(Release, false) => ReleaseColor,
(Debug, true) => DebugAotColor,
(Debug, false) => DebugColor,
_ => Colors.Fuchsia,
};
}
public string BuildConfiguration => BuildInfo.Configuration;
public bool IsAotReleaseConfiguration => BuildConfiguration == Release && BuildInfo.IsNativeAot;
public bool IsAot => BuildInfo.IsNativeAot;
public bool IsPublishTrimmed => BuildInfo.PublishTrimmed;
public ObservableCollection<LogEntryViewModel> LatestLogs { get; } = [];
[ObservableProperty]
public partial int WarningCount { get; private set; }
[ObservableProperty]
public partial int ErrorCount { get; private set; }
[ObservableProperty]
public partial string Tag { get; private set; }
[ObservableProperty]
public partial Color TagColor { get; private set; }
[RelayCommand]
private async Task OpenLogFileAsync()
{
var logPath = Logger.CurrentLogFile;
if (File.Exists(logPath))
{
await Launcher.LaunchUriAsync(new Uri(logPath));
}
}
[RelayCommand]
private async Task OpenLogFolderAsync()
{
var logFolderPath = Logger.CurrentVersionLogDirectoryPath;
if (Directory.Exists(logFolderPath))
{
await Launcher.LaunchFolderPathAsync(logFolderPath);
}
}
[RelayCommand]
private void ResetErrorCounters()
{
WarningCount = 0;
ErrorCount = 0;
LatestLogs.Clear();
}
private sealed partial class DevRibbonTraceListener(DevRibbonViewModel viewModel) : TraceListener
{
private const string TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff";
[GeneratedRegex(@"^\[(?<timestamp>.*?)\] \[(?<severity>.*?)\] (?<message>.*)")]
private static partial Regex LogRegex();
private readonly Lock _lock = new();
private LogEntryViewModel? _latestLogEntry;
public override void Write(string? message)
{
// Not required for this scenario.
}
public override void WriteLine(string? message)
{
if (message is null)
{
return;
}
lock (_lock)
{
var match = LogRegex().Match(message);
if (match.Success)
{
var severity = match.Groups["severity"].Value;
var isWarning = severity.Equals("Warning", StringComparison.OrdinalIgnoreCase);
var isError = severity.Equals("Error", StringComparison.OrdinalIgnoreCase);
if (isWarning || isError)
{
var timestampStr = match.Groups["timestamp"].Value;
var timestamp = DateTimeOffset.TryParseExact(
timestampStr,
TimestampFormat,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeLocal,
out var parsed)
? parsed
: DateTimeOffset.Now;
var logEntry = new LogEntryViewModel(
timestamp,
severity,
match.Groups["message"].Value,
string.Empty);
_latestLogEntry = logEntry;
viewModel._dispatcherQueue.TryEnqueue(() =>
{
if (isWarning)
{
viewModel.WarningCount++;
}
else
{
viewModel.ErrorCount++;
}
viewModel.LatestLogs.Insert(0, logEntry);
while (viewModel.LatestLogs.Count > MaxLogEntries)
{
viewModel.LatestLogs.RemoveAt(viewModel.LatestLogs.Count - 1);
}
});
}
else
{
_latestLogEntry = null;
}
return;
}
if (IndentLevel > 0 && _latestLogEntry is { } latest)
{
viewModel._dispatcherQueue.TryEnqueue(() =>
{
latest.AppendDetails(message);
});
}
}
}
}
}

View File

@@ -0,0 +1,77 @@
// 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.Globalization;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Microsoft.CmdPal.UI.ViewModels;
internal sealed partial class LogEntryViewModel : ObservableObject
{
private const int HeaderMaxLength = 80;
private const string WarningGlyph = "\uE7BA";
private const string ErrorGlyph = "\uEA39";
private const string TimestampFormat = "HH:mm:ss";
private DateTimeOffset Timestamp { get; }
private string Severity { get; }
private string Message { get; }
private string FormattedTimestamp { get; }
public string SeverityGlyph { get; }
[ObservableProperty]
public partial string Header { get; private set; }
[ObservableProperty]
public partial string Description { get; private set; }
[ObservableProperty]
public partial string Details { get; private set; }
public LogEntryViewModel(DateTimeOffset timestamp, string severity, string message, string details)
{
Timestamp = timestamp;
Severity = severity;
Message = message;
Details = details;
SeverityGlyph = severity.ToUpperInvariant() switch
{
"WARNING" => WarningGlyph,
"ERROR" => ErrorGlyph,
_ => string.Empty,
};
FormattedTimestamp = timestamp.ToString(TimestampFormat, CultureInfo.CurrentCulture);
Description = $"{FormattedTimestamp} • {Message}";
Header = Message;
}
public void AppendDetails(string? message)
{
if (string.IsNullOrEmpty(message))
{
return;
}
Details += Environment.NewLine + message;
// Make header the second line of details (because that's actually the message itself):
var detailsLines = Details.Split([Environment.NewLine], StringSplitOptions.None);
if (detailsLines.Length < 2)
{
return;
}
Header = detailsLines[1].Trim();
if (Header.Length > HeaderMaxLength)
{
Header = Header[..(HeaderMaxLength - 1)] + "…";
}
}
}

View File

@@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Reflection;
using System.Text;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
[TestClass]
public class FallbackRemoteDesktopItemTests
{
private static readonly CompositeFormat OpenHostCompositeFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host);
[TestMethod]
public void UpdateQuery_WhenMatchingConnectionExists_UsesConnectionName()
{
var connectionName = "my-rdp-server";
// Arrange
var setup = CreateFallback(connectionName);
var fallback = setup.Fallback;
// Act
fallback.UpdateQuery("my-rdp-server");
// Assert
Assert.AreEqual(connectionName, fallback.Title);
var expectedSubtitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, connectionName);
Assert.AreEqual(expectedSubtitle, fallback.Subtitle);
var command = fallback.Command as OpenRemoteDesktopCommand;
Assert.IsNotNull(command);
Assert.AreEqual(Resources.remotedesktop_command_connect, command.Name);
Assert.AreEqual(connectionName, GetCommandHost(command));
}
[TestMethod]
public void UpdateQuery_WhenQueryIsValidHostWithoutExistingConnection_UsesQuery()
{
// Arrange
var setup = CreateFallback();
var fallback = setup.Fallback;
const string hostname = "test.corp";
// Act
fallback.UpdateQuery(hostname);
// Assert
var expectedTitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostname);
Assert.AreEqual(expectedTitle, fallback.Title);
Assert.AreEqual(Resources.remotedesktop_title, fallback.Subtitle);
var command = fallback.Command as OpenRemoteDesktopCommand;
Assert.IsNotNull(command);
Assert.AreEqual(Resources.remotedesktop_command_connect, command.Name);
Assert.AreEqual(hostname, GetCommandHost(command));
}
[TestMethod]
public void UpdateQuery_WhenQueryIsWhitespace_ResetsCommand()
{
// Arrange
var setup = CreateFallback("rdp-server-two");
var fallback = setup.Fallback;
// Act
fallback.UpdateQuery(" ");
// Assert
Assert.AreEqual(string.Empty, fallback.Title);
Assert.AreEqual(string.Empty, fallback.Subtitle);
var command = fallback.Command as OpenRemoteDesktopCommand;
Assert.IsNull(command);
}
[TestMethod]
public void UpdateQuery_WhenQueryIsInvalidHost_ClearsCommand()
{
// Arrange
var setup = CreateFallback("rdp-server-three");
var fallback = setup.Fallback;
// Act
fallback.UpdateQuery("not a valid host");
// Assert
Assert.AreEqual(string.Empty, fallback.Title);
Assert.AreEqual(string.Empty, fallback.Subtitle);
var command = fallback.Command as OpenRemoteDesktopCommand;
Assert.IsNull(command);
}
private static string GetCommandHost(OpenRemoteDesktopCommand command)
{
var field = typeof(OpenRemoteDesktopCommand).GetField("_rdpHost", BindingFlags.NonPublic | BindingFlags.Instance);
if (field is null)
{
return string.Empty;
}
return field.GetValue(command) as string ?? string.Empty;
}
private static (FallbackRemoteDesktopItem Fallback, IRdpConnectionsManager Manager) CreateFallback(params string[] connectionNames)
{
var settingsManager = new MockSettingsManager(connectionNames);
var connectionsManager = new MockRdpConnectionsManager(settingsManager);
var fallback = new FallbackRemoteDesktopItem(connectionsManager);
return (fallback, connectionsManager);
}
}

View File

@@ -0,0 +1,24 @@
<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>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
internal sealed class MockRdpConnectionsManager : IRdpConnectionsManager
{
private readonly List<ConnectionListItem> _connections = new();
public IReadOnlyCollection<ConnectionListItem> Connections => _connections.AsReadOnly();
public MockRdpConnectionsManager(ISettingsInterface settingsManager)
{
_connections.AddRange(settingsManager.PredefinedConnections.Select(ConnectionHelpers.MapToResult));
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
using ToolkitSettings = Microsoft.CommandPalette.Extensions.Toolkit.Settings;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
internal sealed class MockSettingsManager : ISettingsInterface
{
private readonly List<string> _connections;
public IReadOnlyCollection<string> PredefinedConnections => _connections;
public ToolkitSettings Settings { get; } = new();
public MockSettingsManager(params string[] predefinedConnections)
{
_connections = new(predefinedConnections);
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
[TestClass]
public class RdpConnectionsManagerTests
{
[TestMethod]
public void Constructor_AddsOpenCommandItem()
{
// Act
var manager = new RdpConnectionsManager(new MockSettingsManager(["test.local"]));
// Assert
Assert.IsTrue(manager.Connections.Any(item => string.IsNullOrEmpty(item.ConnectionName)));
}
[TestMethod]
public void FindConnection_ReturnsExactMatch()
{
// Arrange
var connectionName = "rdp-test";
var connection = new ConnectionListItem(connectionName);
// Act
var result = ConnectionHelpers.FindConnection(connectionName, new[] { connection });
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(connectionName, result.ConnectionName);
}
[TestMethod]
public void FindConnection_ReturnsNullForWhitespaceQuery()
{
// Arrange
var connection = new ConnectionListItem("rdp-test");
// Act
var result = ConnectionHelpers.FindConnection(" ", new[] { connection });
// Assert
Assert.IsNull(result);
}
}

View File

@@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
[TestClass]
public class RemoteDesktopCommandProviderTests
{
[TestMethod]
public void ProviderHasCorrectId()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Assert
Assert.AreEqual("com.microsoft.cmdpal.builtin.remotedesktop", provider.Id);
}
[TestMethod]
public void ProviderHasDisplayName()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Assert
Assert.IsNotNull(provider.DisplayName);
Assert.IsTrue(provider.DisplayName.Length > 0);
}
[TestMethod]
public void ProviderHasIcon()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Assert
Assert.IsNotNull(provider.Icon);
}
[TestMethod]
public void TopLevelCommandsNotEmpty()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
[TestMethod]
public void FallbackCommandsNotEmpty()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Act
var commands = provider.FallbackCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
[TestMethod]
public void TopLevelCommandsContainListPageCommand()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.AreEqual(1, commands.Length);
Assert.IsInstanceOfType(commands.Single().Command, typeof(RemoteDesktopListPage));
}
[TestMethod]
public void FallbackCommandsContainFallbackItem()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Act
var commands = provider.FallbackCommands();
// Assert
Assert.AreEqual(1, commands.Length);
Assert.IsInstanceOfType(commands.Single(), typeof(FallbackRemoteDesktopItem));
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
public class MockBrowserInfoService : IBrowserInfoService
{
public BrowserInfo GetDefaultBrowser() => new() { Name = "mocked browser", Path = "C:\\mockery\\mock.exe" };
}

View File

@@ -18,6 +18,8 @@ public class MockSettingsInterface : ISettingsInterface
public int HistoryItemCount { get; set; }
public string CustomSearchUri { get; }
public IReadOnlyList<HistoryItem> HistoryItems => _historyItems;
public MockSettingsInterface(int historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)

View File

@@ -26,8 +26,9 @@ public class QueryTests : CommandPaletteUnitTestBase
{
// Setup
var settings = new MockSettingsInterface();
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings);
var page = new WebSearchListPage(settings, browserInfoService);
// Act
page.UpdateSearchText(string.Empty, query);
@@ -55,8 +56,9 @@ public class QueryTests : CommandPaletteUnitTestBase
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings);
var page = new WebSearchListPage(settings, browserInfoService);
// Act
page.UpdateSearchText("abcdef", string.Empty);
@@ -90,8 +92,9 @@ public class QueryTests : CommandPaletteUnitTestBase
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings);
var page = new WebSearchListPage(settings, browserInfoService);
mockHistoryItems.Add(new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)));
@@ -123,8 +126,9 @@ public class QueryTests : CommandPaletteUnitTestBase
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings);
var page = new WebSearchListPage(settings, browserInfoService);
// Act
page.UpdateSearchText("abcdef", string.Empty);

View File

@@ -20,7 +20,9 @@ public class SettingsManagerTests : CommandPaletteUnitTestBase
{
// Setup
var settings = new MockSettingsInterface(historyItemCount: 5);
var page = new WebSearchListPage(settings);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings, browserInfoService);
var eventRaised = false;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,21 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3.55078" width="9.36537" height="9.36537" rx="0.720413" fill="url(#paint0_linear_2155_27162)"/>
<rect x="1" y="2" width="13" height="9" rx="0.722222" fill="url(#paint1_linear_2155_27162)"/>
<circle cx="11.4" cy="9.4" r="4.4" fill="url(#paint2_radial_2155_27162)"/>
<path d="M13.8703 11.2497C13.964 11.3434 14.116 11.3434 14.2097 11.2497C14.3034 11.156 14.3034 11.004 14.2097 10.9103L12.4594 9.16L14.2097 7.40971C14.3034 7.31598 14.3034 7.16402 14.2097 7.07029C14.116 6.97657 13.964 6.97657 13.8703 7.07029L11.9503 8.9903C11.8566 9.08402 11.8566 9.23598 11.9503 9.32971L13.8703 11.2497ZM9.40971 8.0303C9.31598 7.93657 9.16402 7.93657 9.07029 8.0303C8.97657 8.12402 8.97657 8.27598 9.07029 8.36971L10.8206 10.12L9.07029 11.8703C8.97657 11.964 8.97657 12.116 9.07029 12.2097C9.16402 12.3034 9.31598 12.3034 9.40971 12.2097L11.3297 10.2897C11.4234 10.196 11.4234 10.044 11.3297 9.95031L9.40971 8.0303Z" fill="#666666" stroke="#666666" stroke-width="0.146667"/>
<defs>
<linearGradient id="paint0_linear_2155_27162" x1="3.22298" y1="3.55078" x2="8.52487" y2="6.68847" gradientUnits="userSpaceOnUse">
<stop stop-color="#246FB0"/>
<stop offset="1" stop-color="#14518A"/>
</linearGradient>
<linearGradient id="paint1_linear_2155_27162" x1="1.15476" y1="1.66667" x2="14.867" y2="9.90553" gradientUnits="userSpaceOnUse">
<stop stop-color="#86D6F9"/>
<stop offset="1" stop-color="#1FA3E4"/>
</linearGradient>
<radialGradient id="paint2_radial_2155_27162" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.9111 6.22222) rotate(90) scale(7.57778)">
<stop stop-color="#E7ECF1"/>
<stop offset="0.84" stop-color="#D2D4D6"/>
<stop offset="1" stop-color="#A9ABAC"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Text;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
internal sealed partial class ConnectionListItem : ListItem
{
public ConnectionListItem(string connectionName)
{
ConnectionName = connectionName;
if (string.IsNullOrEmpty(connectionName))
{
Title = Resources.remotedesktop_open_rdp;
Subtitle = Resources.remotedesktop_subtitle;
}
else
{
Title = connectionName;
CompositeFormat remoteDesktopOpenHostFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host);
Subtitle = string.Format(CultureInfo.CurrentCulture, remoteDesktopOpenHostFormat, connectionName);
}
Icon = Icons.RDPIcon;
Command = new OpenRemoteDesktopCommand(connectionName);
}
public string ConnectionName { get; }
}

View File

@@ -0,0 +1,77 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
internal sealed partial class FallbackRemoteDesktopItem : FallbackCommandItem
{
private const string _id = "com.microsoft.cmdpal.builtin.remotedesktop.fallback";
private static readonly UriHostNameType[] ValidUriHostNameTypes = [
UriHostNameType.IPv6,
UriHostNameType.IPv4,
UriHostNameType.Dns
];
private static readonly CompositeFormat RemoteDesktopOpenHostFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host);
private readonly IRdpConnectionsManager _rdpConnectionsManager;
private readonly NoOpCommand _emptyCommand = new NoOpCommand();
public FallbackRemoteDesktopItem(IRdpConnectionsManager rdpConnectionsManager)
: base(Resources.remotedesktop_title)
{
_rdpConnectionsManager = rdpConnectionsManager;
Command = _emptyCommand;
Title = string.Empty;
Subtitle = string.Empty;
Icon = Icons.RDPIcon;
}
public override void UpdateQuery(string query)
{
if (string.IsNullOrWhiteSpace(query))
{
Title = string.Empty;
Subtitle = string.Empty;
Command = _emptyCommand;
return;
}
var connections = _rdpConnectionsManager.Connections.Where(w => !string.IsNullOrWhiteSpace(w.ConnectionName));
var queryConnection = ConnectionHelpers.FindConnection(query, connections);
if (queryConnection is not null && !string.IsNullOrWhiteSpace(queryConnection.ConnectionName))
{
var connectionName = queryConnection.ConnectionName;
Command = new OpenRemoteDesktopCommand(connectionName);
Title = connectionName;
Subtitle = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName);
}
else if (ValidUriHostNameTypes.Contains(Uri.CheckHostName(query)))
{
var connectionName = query.Trim();
Command = new OpenRemoteDesktopCommand(connectionName);
Title = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName);
Subtitle = Resources.remotedesktop_title;
}
else
{
Title = string.Empty;
Subtitle = string.Empty;
Command = _emptyCommand;
}
}
}

View File

@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Text;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
internal sealed partial class OpenRemoteDesktopCommand : BaseObservable, IInvokableCommand
{
private static readonly CompositeFormat ProcessErrorFormat =
CompositeFormat.Parse(Resources.remotedesktop_log_mstsc_error);
private static readonly CompositeFormat InvalidHostnameFormat =
CompositeFormat.Parse(Resources.remotedesktop_log_invalid_hostname);
public string Name { get; }
public string Id { get; } = "com.microsoft.cmdpal.builtin.remotedesktop.openrdp";
public IIconInfo Icon => Icons.RDPIcon;
private readonly string _rdpHost;
public OpenRemoteDesktopCommand(string rdpHost)
{
_rdpHost = rdpHost;
Name = string.IsNullOrWhiteSpace(_rdpHost) ?
Resources.remotedesktop_command_open :
Resources.remotedesktop_command_connect;
}
public ICommandResult Invoke(object sender)
{
using var process = new Process();
process.StartInfo.UseShellExecute = false;
process.StartInfo.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
process.StartInfo.FileName = "mstsc";
if (!string.IsNullOrWhiteSpace(_rdpHost))
{
// validate that _rdpHost is a proper hostname or IP address
if (Uri.CheckHostName(_rdpHost) == UriHostNameType.Unknown)
{
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(
System.Globalization.CultureInfo.CurrentCulture,
InvalidHostnameFormat,
_rdpHost),
Result = CommandResult.KeepOpen(),
});
}
process.StartInfo.Arguments = $"/v:{_rdpHost}";
}
try
{
process.Start();
return CommandResult.Dismiss();
}
catch (Exception ex)
{
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(
System.Globalization.CultureInfo.CurrentCulture,
ProcessErrorFormat,
ex.Message),
Result = CommandResult.KeepOpen(),
});
}
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
internal static class ConnectionHelpers
{
public static ConnectionListItem MapToResult(string item) => new(item);
public static ConnectionListItem? FindConnection(string query, IEnumerable<ConnectionListItem> connections)
{
if (string.IsNullOrWhiteSpace(query))
{
return null;
}
var matchedConnection = ListHelpers.FilterList(
connections,
query,
(s, i) => ListHelpers.ScoreListItem(s, i))
.FirstOrDefault();
return matchedConnection;
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
internal interface IRdpConnectionsManager
{
IReadOnlyCollection<ConnectionListItem> Connections { get; }
}

View File

@@ -0,0 +1,89 @@
// 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.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
using Microsoft.Win32;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
internal class RdpConnectionsManager : IRdpConnectionsManager
{
private readonly ISettingsInterface _settingsManager;
private readonly ConnectionListItem _openRdpCommandListItem = new(string.Empty);
private ReadOnlyCollection<ConnectionListItem> _connections = new(Array.Empty<ConnectionListItem>());
private const int MinutesToCache = 1;
private DateTime? _connectionsLastLoaded;
public RdpConnectionsManager(ISettingsInterface settingsManager)
{
_settingsManager = settingsManager;
_settingsManager.Settings.SettingsChanged += (s, e) =>
{
_connectionsLastLoaded = null;
};
}
public IReadOnlyCollection<ConnectionListItem> Connections
{
get
{
if (!_connectionsLastLoaded.HasValue ||
(DateTime.Now - _connectionsLastLoaded.Value).TotalMinutes >= MinutesToCache)
{
var registryConnections = GetRdpConnectionsFromRegistry();
var predefinedConnections = GetPredefinedConnectionsFromSettings();
_connectionsLastLoaded = DateTime.Now;
var newConnections = new List<ConnectionListItem>(registryConnections.Count + predefinedConnections.Count + 1);
newConnections.AddRange(registryConnections);
newConnections.AddRange(predefinedConnections);
newConnections.Insert(0, _openRdpCommandListItem);
Interlocked.Exchange(ref _connections, new ReadOnlyCollection<ConnectionListItem>(newConnections));
}
return _connections;
}
}
private List<ConnectionListItem> GetRdpConnectionsFromRegistry()
{
using var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Terminal Server Client\Default");
var validConnections = new List<ConnectionListItem>();
if (key is not null)
{
validConnections = key.GetValueNames()
.Select(name => key.GetValue(name))
.OfType<string>() // Keep only string values
.Select(v => v.Trim()) // Normalize
.Where(v => !string.IsNullOrWhiteSpace(v))
.Distinct() // Remove dupes if any
.Select(ConnectionHelpers.MapToResult)
.ToList();
}
return validConnections;
}
private List<ConnectionListItem> GetPredefinedConnectionsFromSettings()
{
var validConnections = _settingsManager.PredefinedConnections
.Select(s => s.Trim())
.Where(value => !string.IsNullOrWhiteSpace(value))
.Select(ConnectionHelpers.MapToResult)
.ToList();
return validConnections;
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop;
internal static class Icons
{
internal static IconInfo RDPIcon { get; } = IconHelpers.FromRelativePath("Assets\\RemoteDesktop.svg");
}

View File

@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<Import Project="..\Common.ExtDependencies.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.RemoteDesktop</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>Microsoft.CmdPal.Ext.RemoteDesktop.pri</ProjectPriFileName>
<nullable>enable</nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\RemoteDesktop.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\RemoteDesktop.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
internal sealed partial class RemoteDesktopListPage : ListPage
{
private readonly IRdpConnectionsManager _rdpConnectionsManager;
public RemoteDesktopListPage(IRdpConnectionsManager rdpConnectionsManager)
{
Icon = Icons.RDPIcon;
Name = Resources.remotedesktop_title;
Id = "com.microsoft.cmdpal.builtin.remotedesktop";
_rdpConnectionsManager = rdpConnectionsManager;
}
public override IListItem[] GetItems() => _rdpConnectionsManager.Connections.ToArray();
}

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.RemoteDesktop.UnitTests")]

View File

@@ -0,0 +1,153 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.RemoteDesktop.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Connect.
/// </summary>
public static string remotedesktop_command_connect {
get {
return ResourceManager.GetString("remotedesktop_command_connect", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open.
/// </summary>
public static string remotedesktop_command_open {
get {
return ResourceManager.GetString("remotedesktop_command_open", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The hostname &apos;{0}&apos; was invalid. Ensure you&apos;re using a valid hostname or IP address..
/// </summary>
public static string remotedesktop_log_invalid_hostname {
get {
return ResourceManager.GetString("remotedesktop_log_invalid_hostname", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0}.
/// </summary>
public static string remotedesktop_log_mstsc_error {
get {
return ResourceManager.GetString("remotedesktop_log_mstsc_error", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connect to {0}.
/// </summary>
public static string remotedesktop_open_host {
get {
return ResourceManager.GetString("remotedesktop_open_host", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Remote Desktop Client.
/// </summary>
public static string remotedesktop_open_rdp {
get {
return ResourceManager.GetString("remotedesktop_open_rdp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A list of connections to include in the query results by default.
/// </summary>
public static string remotedesktop_settings_predefined_connections_description {
get {
return ResourceManager.GetString("remotedesktop_settings_predefined_connections_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Predefined connections.
/// </summary>
public static string remotedesktop_settings_predefined_connections_title {
get {
return ResourceManager.GetString("remotedesktop_settings_predefined_connections_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Establish Remote Desktop connections.
/// </summary>
public static string remotedesktop_subtitle {
get {
return ResourceManager.GetString("remotedesktop_subtitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remote Desktop.
/// </summary>
public static string remotedesktop_title {
get {
return ResourceManager.GetString("remotedesktop_title", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="remotedesktop_title" xml:space="preserve">
<value>Remote Desktop</value>
</data>
<data name="remotedesktop_subtitle" xml:space="preserve">
<value>Establish Remote Desktop connections</value>
</data>
<data name="remotedesktop_command_open" xml:space="preserve">
<value>Open</value>
</data>
<data name="remotedesktop_open_host" xml:space="preserve">
<value>Connect to {0}</value>
</data>
<data name="remotedesktop_command_connect" xml:space="preserve">
<value>Connect</value>
</data>
<data name="remotedesktop_open_rdp" xml:space="preserve">
<value>Open Remote Desktop Client</value>
</data>
<data name="remotedesktop_settings_predefined_connections_title" xml:space="preserve">
<value>Predefined connections</value>
</data>
<data name="remotedesktop_settings_predefined_connections_description" xml:space="preserve">
<value>A list of connections to include in the query results by default</value>
</data>
<data name="remotedesktop_log_mstsc_error" xml:space="preserve">
<value>Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0}</value>
</data>
<data name="remotedesktop_log_invalid_hostname" xml:space="preserve">
<value>The hostname '{0}' was invalid. Ensure you're using a valid hostname or IP address.</value>
</data>
</root>

View File

@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop;
public partial class RemoteDesktopCommandProvider : CommandProvider
{
private readonly CommandItem listPageCommand;
private readonly FallbackRemoteDesktopItem fallback;
public RemoteDesktopCommandProvider()
{
Id = "com.microsoft.cmdpal.builtin.remotedesktop";
DisplayName = Resources.remotedesktop_title;
Icon = Icons.RDPIcon;
var settingsManager = new SettingsManager();
var rdpConnectionsManager = new RdpConnectionsManager(settingsManager);
var listPage = new RemoteDesktopListPage(rdpConnectionsManager);
fallback = new FallbackRemoteDesktopItem(rdpConnectionsManager);
listPageCommand = new CommandItem(listPage)
{
Subtitle = Resources.remotedesktop_subtitle,
Icon = Icons.RDPIcon,
MoreCommands = [
new CommandContextItem(settingsManager.Settings.SettingsPage),
],
};
}
public override ICommandItem[] TopLevelCommands() => [listPageCommand];
public override IFallbackCommandItem[] FallbackCommands() => [fallback];
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using ToolkitSettings = Microsoft.CommandPalette.Extensions.Toolkit.Settings;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
internal interface ISettingsInterface
{
public IReadOnlyCollection<string> PredefinedConnections { get; }
public ToolkitSettings Settings { get; }
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
internal class SettingsManager : JsonSettingsManager, ISettingsInterface
{
// Line break character used in WinUI3 TextBox and TextBlock.
private const char TEXTBOXNEWLINE = '\r';
private static readonly string _namespace = "com.microsoft.cmdpal.builtin.remotedesktop";
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
private readonly TextSetting _predefinedConnections = new(
Namespaced(nameof(PredefinedConnections)),
Resources.remotedesktop_settings_predefined_connections_title,
Resources.remotedesktop_settings_predefined_connections_description,
string.Empty)
{
Multiline = true,
Placeholder = $"server1.domain.com{TEXTBOXNEWLINE}server2.domain.com{TEXTBOXNEWLINE}192.168.1.1",
};
public IReadOnlyCollection<string> PredefinedConnections => _predefinedConnections.Value?.Split(TEXTBOXNEWLINE).ToList() ?? [];
internal static string SettingsJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, "settings.json");
}
public SettingsManager()
{
FilePath = SettingsJsonPath();
Settings.Add(_predefinedConnections);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();
}
}

View File

@@ -2,32 +2,28 @@
// 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.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
internal sealed partial class OpenURLCommand : InvokableCommand
{
private readonly IBrowserInfoService _browserInfoService;
public string Url { get; internal set; } = string.Empty;
internal OpenURLCommand(string url)
internal OpenURLCommand(string url, IBrowserInfoService browserInfoService)
{
_browserInfoService = browserInfoService;
Url = url;
BrowserInfo.UpdateIfTimePassed();
Icon = Icons.WebSearch;
Name = string.Empty;
}
public override CommandResult Invoke()
{
if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"{Url}"))
{
// TODO GH# 138 --> actually display feedback from the extension somewhere.
return CommandResult.KeepOpen();
}
return CommandResult.Dismiss();
// TODO GH# 138 --> actually display feedback from the extension somewhere.
return _browserInfoService.Open(Url) ? CommandResult.Dismiss() : CommandResult.KeepOpen();
}
}

View File

@@ -4,36 +4,39 @@
using System;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
internal sealed partial class SearchWebCommand : InvokableCommand
{
private readonly ISettingsInterface _settingsManager;
private readonly IBrowserInfoService _browserInfoService;
public string Arguments { get; internal set; } = string.Empty;
public string Arguments { get; internal set; }
internal SearchWebCommand(string arguments, ISettingsInterface settingsManager)
internal SearchWebCommand(string arguments, ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
{
Arguments = arguments;
BrowserInfo.UpdateIfTimePassed();
Icon = Icons.WebSearch;
Name = Properties.Resources.open_in_default_browser;
Name = Resources.open_in_default_browser;
_settingsManager = settingsManager;
_browserInfoService = browserInfoService;
}
public override CommandResult Invoke()
{
if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"? {Arguments}"))
var uri = BuildUri();
if (!_browserInfoService.Open(uri))
{
// TODO GH# 138 --> actually display feedback from the extension somewhere.
return CommandResult.KeepOpen();
}
// remember only the query, not the full URI
if (_settingsManager.HistoryItemCount != 0)
{
_settingsManager.AddHistoryItem(new HistoryItem(Arguments, DateTime.Now));
@@ -41,4 +44,28 @@ internal sealed partial class SearchWebCommand : InvokableCommand
return CommandResult.Dismiss();
}
private string BuildUri()
{
if (string.IsNullOrWhiteSpace(_settingsManager.CustomSearchUri))
{
return $"? " + Arguments;
}
// if the custom search URI contains query placeholder, replace it with the actual query
// otherwise append the query to the end of the URI
// support {query}, %query% or %s as placeholder
var placeholderVariants = new[] { "{query}", "%query%", "%s" };
foreach (var placeholder in placeholderVariants)
{
if (_settingsManager.CustomSearchUri.Contains(placeholder, StringComparison.OrdinalIgnoreCase))
{
return _settingsManager.CustomSearchUri.Replace(placeholder, Uri.EscapeDataString(Arguments), StringComparison.OrdinalIgnoreCase);
}
}
// is this too smart?
var separator = _settingsManager.CustomSearchUri.Contains('?') ? '&' : '?';
return $"{_settingsManager.CustomSearchUri}{separator}q={Uri.EscapeDataString(Arguments)}";
}
}

View File

@@ -5,9 +5,9 @@
using System.Globalization;
using System.Text;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
@@ -16,25 +16,34 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem
private readonly SearchWebCommand _executeItem;
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
private static readonly CompositeFormat SubtitleText = System.Text.CompositeFormat.Parse(Properties.Resources.web_search_fallback_subtitle);
private string _title;
public FallbackExecuteSearchItem(SettingsManager settings)
: base(new SearchWebCommand(string.Empty, settings) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
private readonly IBrowserInfoService _browserInfoService;
public FallbackExecuteSearchItem(ISettingsInterface settings, IBrowserInfoService browserInfoService)
: base(new SearchWebCommand(string.Empty, settings, browserInfoService) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
{
_executeItem = (SearchWebCommand)this.Command!;
_executeItem = (SearchWebCommand)Command!;
_browserInfoService = browserInfoService;
Title = string.Empty;
Subtitle = string.Empty;
_executeItem.Name = string.Empty;
_title = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
Icon = Icons.WebSearch;
}
private static string UpdateBrowserName(IBrowserInfoService browserInfoService)
{
var browserName = browserInfoService.GetDefaultBrowser()?.Name;
return string.IsNullOrWhiteSpace(browserName)
? Resources.open_in_default_browser
: string.Format(CultureInfo.CurrentCulture, PluginOpen, browserName);
}
public override void UpdateQuery(string query)
{
_executeItem.Arguments = query;
var isEmpty = string.IsNullOrEmpty(query);
_executeItem.Name = isEmpty ? string.Empty : Properties.Resources.open_in_default_browser;
Title = isEmpty ? string.Empty : _title;
_executeItem.Name = isEmpty ? string.Empty : Resources.open_in_default_browser;
Title = isEmpty ? string.Empty : UpdateBrowserName(_browserInfoService);
Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query);
}
}

View File

@@ -7,21 +7,26 @@ using System.Globalization;
using System.Text;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch;
internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
{
private readonly IBrowserInfoService _browserInfoService;
private readonly OpenURLCommand _executeItem;
private static readonly CompositeFormat PluginOpenURL = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url);
private static readonly CompositeFormat PluginOpenUrlInBrowser = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url_in_browser);
public FallbackOpenURLItem(SettingsManager settings)
: base(new OpenURLCommand(string.Empty), Properties.Resources.open_url_fallback_title)
public FallbackOpenURLItem(ISettingsInterface settings, IBrowserInfoService browserInfoService)
: base(new OpenURLCommand(string.Empty, browserInfoService), Resources.open_url_fallback_title)
{
_executeItem = (OpenURLCommand)this.Command!;
ArgumentNullException.ThrowIfNull(browserInfoService);
_browserInfoService = browserInfoService;
_executeItem = (OpenURLCommand)Command!;
Title = string.Empty;
_executeItem.Name = string.Empty;
Subtitle = string.Empty;
@@ -39,7 +44,7 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
return;
}
var success = Uri.TryCreate(query, UriKind.Absolute, out var uri);
var success = Uri.TryCreate(query, UriKind.Absolute, out _);
// if url not contain schema, add http:// by default.
if (!success)
@@ -48,13 +53,15 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
}
_executeItem.Url = query;
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Properties.Resources.open_in_default_browser;
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Resources.open_in_default_browser;
Title = string.Format(CultureInfo.CurrentCulture, PluginOpenURL, query);
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
var browserName = _browserInfoService.GetDefaultBrowser()?.Name;
Subtitle = string.IsNullOrWhiteSpace(browserName) ? Resources.open_in_default_browser : string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, browserName);
}
public static bool IsValidUrl(string url)
private static bool IsValidUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
{

View File

@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
public record BrowserInfo
{
public required string Path { get; init; }
public required string Name { get; init; }
public string? ArgumentsPattern { get; init; }
}

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 Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
/// <summary>
/// Extension methods for <see cref="IBrowserInfoService"/>.
/// </summary>
/// <seealso cref="IBrowserInfoService"/>
internal static class BrowserInfoServiceExtensions
{
/// <summary>
/// Opens the specified URL in the system's default web browser.
/// </summary>
/// <param name="browserInfoService">The browser information service used to resolve the system's default browser.</param>
/// <param name="url">The URL to open.</param>
/// <returns>
/// <see langword="true"/> if a default browser is found and the URL launch command is issued successfully;
/// otherwise, <see langword="false"/>.
/// </returns>
/// <remarks>
/// Returns <see langword="false"/> if the default browser cannot be determined.
/// </remarks>
public static bool Open(this IBrowserInfoService browserInfoService, string url)
{
var defaultBrowser = browserInfoService.GetDefaultBrowser();
return defaultBrowser != null && ShellHelpers.OpenCommandInShell(defaultBrowser.Path, defaultBrowser.ArgumentsPattern, url);
}
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading;
using ManagedCommon;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
/// <summary>
/// Service to get information about the default browser.
/// </summary>
internal class DefaultBrowserInfoService : IBrowserInfoService
{
private static readonly IDefaultBrowserProvider[] Providers =
[
new ShellAssociationProvider(),
new LegacyRegistryAssociationProvider(),
new FallbackMsEdgeBrowserProvider(),
];
private readonly Lock _updateLock = new();
private readonly Dictionary<Type, string> _lastLoggedErrors = [];
private const long UpdateTimeout = 3000;
private long _lastUpdateTickCount = -UpdateTimeout;
private BrowserInfo? _defaultBrowser;
public BrowserInfo? GetDefaultBrowser()
{
try
{
UpdateIfTimePassed();
}
catch (Exception)
{
// exception is already logged at this point
}
return _defaultBrowser;
}
/// <summary>
/// Updates only if at least more than 3000ms has passed since the last update, to avoid multiple calls to <see cref="UpdateCore"/>.
/// (because of multiple plugins calling update at the same time.)
/// </summary>
private void UpdateIfTimePassed()
{
lock (_updateLock)
{
var curTickCount = Environment.TickCount64;
if (curTickCount - _lastUpdateTickCount < UpdateTimeout && _defaultBrowser != null)
{
return;
}
var newDefaultBrowser = UpdateCore();
_defaultBrowser = newDefaultBrowser;
_lastUpdateTickCount = curTickCount;
}
}
/// <summary>
/// Consider using <see cref="UpdateIfTimePassed"/> to avoid updating multiple times.
/// (because of multiple plugins calling update at the same time.)
/// </summary>
private BrowserInfo UpdateCore()
{
foreach (var provider in Providers)
{
try
{
var result = provider.GetDefaultBrowserInfo();
#if DEBUG
result = result with { Name = result.Name + " (" + provider.GetType().Name + ")" };
#endif
return result;
}
catch (Exception ex)
{
// since we run this fairly often, avoid logging the same error multiple times
var lastLoggedError = _lastLoggedErrors.GetValueOrDefault(provider.GetType());
var error = ex.ToString();
if (error != lastLoggedError)
{
_lastLoggedErrors[provider.GetType()] = error;
Logger.LogError($"Exception when retrieving browser using provider {provider.GetType()}", ex);
}
}
}
throw new InvalidOperationException("Unable to determine default browser");
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
/// <summary>
/// Provides functionality to retrieve information about the system's default web browser.
/// </summary>
public interface IBrowserInfoService
{
/// <summary>
/// Gets information about the system's default web browser.
/// </summary>
/// <returns></returns>
BrowserInfo? GetDefaultBrowser();
}

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.
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
internal record AssociatedApp(string? Command, string? FriendlyName);

View File

@@ -0,0 +1,154 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using Windows.Win32;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
/// <summary>
/// Base class for providers that determine the default browser via application associations.
/// </summary>
internal abstract class AssociationProviderBase : IDefaultBrowserProvider
{
protected abstract AssociatedApp? FindAssociation();
public BrowserInfo GetDefaultBrowserInfo()
{
var appAssociation = FindAssociation();
if (appAssociation is null)
{
throw new ArgumentNullException(nameof(appAssociation), "Could not determine default browser application.");
}
var commandPattern = appAssociation.Command;
var appAndArgs = SplitAppAndArgs(commandPattern);
if (string.IsNullOrEmpty(appAndArgs.Path))
{
throw new ArgumentOutOfRangeException(nameof(appAndArgs.Path), "Default browser program path could not be determined.");
}
// Packaged applications could be an URI. Example: shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App
if (!Path.Exists(appAndArgs.Path) && !Uri.TryCreate(appAndArgs.Path, UriKind.Absolute, out _))
{
throw new ArgumentException($"Command validation failed: {commandPattern}", nameof(commandPattern));
}
return new BrowserInfo
{
Path = appAndArgs.Path,
Name = appAssociation.FriendlyName ?? Path.GetFileNameWithoutExtension(appAndArgs.Path),
ArgumentsPattern = appAndArgs.Arguments,
};
}
private static (string? Path, string? Arguments) SplitAppAndArgs(string? commandPattern)
{
if (string.IsNullOrEmpty(commandPattern))
{
throw new ArgumentOutOfRangeException(nameof(commandPattern), "Default browser program command is not specified.");
}
commandPattern = GetIndirectString(commandPattern);
// HACK: for firefox installed through Microsoft store
// When installed through Microsoft Firefox the commandPattern does not have
// quotes for the path. As the Program Files does have a space
// the extracted path would be invalid, here we add the quotes to fix it
const string FirefoxExecutableName = "firefox.exe";
if (commandPattern.Contains(FirefoxExecutableName) && commandPattern.Contains(@"\WindowsApps\") &&
!commandPattern.StartsWith('\"'))
{
var pathEndIndex = commandPattern.IndexOf(FirefoxExecutableName, StringComparison.Ordinal) +
FirefoxExecutableName.Length;
commandPattern = commandPattern.Insert(pathEndIndex, "\"");
commandPattern = commandPattern.Insert(0, "\"");
}
if (commandPattern.StartsWith('\"'))
{
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
if (endQuoteIndex != -1)
{
return (commandPattern[1..endQuoteIndex], commandPattern[(endQuoteIndex + 1)..].Trim());
}
}
else
{
var spaceIndex = commandPattern.IndexOf(' ');
if (spaceIndex != -1)
{
return (commandPattern[..spaceIndex], commandPattern[(spaceIndex + 1)..].Trim());
}
}
return (null, null);
}
protected static string GetIndirectString(string str)
{
if (string.IsNullOrEmpty(str) || str[0] != '@')
{
return str;
}
const int initialCapacity = 128;
const int maxCapacity = 8192; // Reasonable upper limit
int hresult;
unsafe
{
// Try with stack allocation first for common cases
var stackBuffer = stackalloc char[initialCapacity];
fixed (char* pszSource = str)
{
hresult = PInvoke.SHLoadIndirectString(
pszSource,
stackBuffer,
initialCapacity,
null);
// S_OK (0) means success
if (hresult == 0)
{
return new string(stackBuffer);
}
// STRSAFE_E_INSUFFICIENT_BUFFER (0x8007007A) means buffer too small
// Try with progressively larger heap buffers
if (unchecked((uint)hresult) == 0x8007007A)
{
for (var capacity = initialCapacity * 2; capacity <= maxCapacity; capacity *= 2)
{
var heapBuffer = new char[capacity];
fixed (char* pBuffer = heapBuffer)
{
hresult = PInvoke.SHLoadIndirectString(
pszSource,
pBuffer,
(uint)capacity,
null);
if (hresult == 0)
{
return new string(pBuffer);
}
if (unchecked((uint)hresult) != 0x8007007A)
{
break; // Different error, stop retrying
}
}
}
}
}
}
throw new InvalidOperationException(
$"Could not load indirect string. HRESULT: 0x{unchecked((uint)hresult):X8}");
}
}

View File

@@ -0,0 +1,31 @@
// 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.WebSearch.Helpers.Browser.Providers;
/// <summary>
/// Provides a fallback implementation of the default browser provider that returns information for Microsoft Edge.
/// </summary>
/// <remarks>This class is used when no other default browser provider is available. It supplies the path,
/// arguments pattern, and name for Microsoft Edge as the default browser information.</remarks>
internal sealed class FallbackMsEdgeBrowserProvider : IDefaultBrowserProvider
{
private const string MsEdgeArgumentsPattern = "--single-argument %1";
private const string MsEdgeName = "Microsoft Edge";
private static string MsEdgePath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
@"Microsoft\Edge\Application\msedge.exe");
public BrowserInfo GetDefaultBrowserInfo() => new()
{
Path = MsEdgePath,
ArgumentsPattern = MsEdgeArgumentsPattern,
Name = MsEdgeName,
};
}

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