Compare commits

...

8 Commits

Author SHA1 Message Date
Gordon Lam (SH)
97b262919b We should include this first 2025-09-24 15:14:39 +08:00
Gordon Lam (SH)
3af7f39daf Fix spelling check 2025-09-24 13:26:11 +08:00
Gordon Lam (SH)
25008c083c Only Nightly CI build will do update build cache 2025-09-24 12:28:41 +08:00
Gordon Lam (SH)
e0a16409d4 Only use cache for main's daily build, others should be readonly 2025-09-24 12:16:20 +08:00
Jiří Polášek
830a32fc1f CmdPal: Add option to choose default order for Terminal profiles (#41945)
## Summary of the Pull Request

This PR adds a new drop-down to the Windows Terminal extension settings
page that allows the user to choose the default sort order of profiles.
The available options are:
- Default (currently alphabetical, but may change in the future)
- Alphabetical
- Most recently used

It also extends the app data to store AppSettings, including a list of
up to 64 recently used profiles, stored as pairs of profile name and
terminal app ID.

Pictures? Pictures!

<img width="1821" height="1097" alt="image"
src="https://github.com/user-attachments/assets/c751dcbf-e638-4207-a3e4-6dd283c5239c"
/>

<img width="1694" height="521" alt="image"
src="https://github.com/user-attachments/assets/914c0498-98fa-4ed7-997d-f988253c923c"
/>

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-23 16:23:58 -05:00
Jiří Polášek
0edf06bb5f CmdPal: Window Walker - detect UWP apps and prevent "Unresponsive" tag on them (#41938)
## Summary of the Pull Request

This PR introduces detection of UWP processes and skips evaluation of
the Process.Responding property for them.

The Process.Responding property is only reliable for Win32 apps. For UWP
processes, relying on this property can produce incorrect results. With
this change, UWP apps will no longer be flagged with an Unresponsive tag
due to misleading property values.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-23 16:20:06 -05:00
Jiří Polášek
f995e414b7 CmdPal: Add setting to choose primary action for Clipboard History items (#41863)
## Summary of the Pull Request

Allows users to set a preference for the primary action—Paste or
Copy—when interacting with Clipboard History entries.

- Introduce `ClipboardListItem` as a subclass of `ListItem`
- Build the item's details panel lazily to improve performance
- Order Paste/Copy commands based on the selected preference
- Update icons to visually reflect the chosen primary action

Pictures? Pictures!

<img width="1802" height="1137" alt="image"
src="https://github.com/user-attachments/assets/f4d09902-2538-4103-92d5-41c43b313952"
/>

<img width="1731" height="1084" alt="image"
src="https://github.com/user-attachments/assets/08354312-6ef9-433a-9893-31fe3a233fbf"
/>


<img width="3324" height="742" alt="image"
src="https://github.com/user-attachments/assets/0431145e-c084-4996-93d6-4eb84b7d6177"
/>


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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-09-23 10:39:08 -05:00
Sam Rueby
aae601aa36 IconMarginConverter class now partial to resolve AOT warning (#41943)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This resolves the issue brought up in PR 41851 where the new class
generated a warning for AOT builds.

Please see
https://github.com/microsoft/PowerToys/pull/41851#issuecomment-3320083888
2025-09-23 09:21:49 -05:00
45 changed files with 817 additions and 153 deletions

View File

@@ -27,6 +27,7 @@ admx
advancedpaste
advancedpasteui
advancedpasteuishortcut
advapi32
advfirewall
AFeature
affordances

View File

@@ -0,0 +1,38 @@
# .pipelines/v2/nightly-prewarm.yml
# Nightly pre-warm that reuses your existing ci.yml as-is
trigger: none
pr: none
# (18:00 UTC) — adjust as you like
schedules:
- cron: "0 18 * * *" # UTC
displayName: Nightly pre-warm (main)
branches:
include:
- main
always: true
name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr)
parameters:
- name: buildPlatforms
type: object
default:
- x64
- arm64
- name: enableMsBuildCaching
type: boolean
displayName: "Enable MSBuild Caching"
default: true
- name: msBuildCacheIsReadOnly
type: boolean
displayName: "MSBuild Cache Read Only"
default: false
extends:
template: templates/pipeline-ci-build.yml
parameters:
buildPlatforms: ${{ parameters.buildPlatforms }}
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
msBuildCacheIsReadOnly: ${{ parameters.msBuildCacheIsReadOnly }}

View File

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

View File

@@ -50,6 +50,9 @@ parameters:
- name: enableMsBuildCaching
type: boolean
default: false
- name: msBuildCacheIsReadOnly
type: boolean
default: true
- name: runTests
type: boolean
default: true
@@ -154,6 +157,11 @@ jobs:
$MSBuildCacheParameters += " -reportfileaccesses"
$MSBuildCacheParameters += " -p:MSBuildCacheEnabled=true"
$MSBuildCacheParameters += " -p:MSBuildCacheLogDirectory=$(LogOutputDirectory)\MSBuildCacheLogs"
# Cache read-only policy controlled by parameter
$cacheIsReadOnly = "${{ parameters.msBuildCacheIsReadOnly }}"
if ($cacheIsReadOnly -eq "True") {
$MSBuildCacheParameters += " /p:MSBuildCacheRemoteCacheIsReadOnly=true"
}
Write-Host "MSBuildCacheParameters: $MSBuildCacheParameters"
Write-Host "##vso[task.setvariable variable=MSBuildCacheParameters]$MSBuildCacheParameters"
displayName: Prepare MSBuildCache variables
@@ -418,7 +426,7 @@ jobs:
}
if ($Packages.Count -gt 0) {
# Priority: Look for platform-specific MSIX (x64/arm64) first, then fall back to any
# Priority: Look for platform-specific MSIX (x64/arm64) first, then fallback to any
$PlatformPackage = $Packages | Where-Object { $_.Name -match "Microsoft\.CmdPal\.UI_.*_(x64|arm64)\.msix$" } | Select-Object -First 1
if ($PlatformPackage) {
$Package = $PlatformPackage

View File

@@ -13,6 +13,9 @@ parameters:
- name: enableMsBuildCaching
type: boolean
default: false
- name: msBuildCacheIsReadOnly
type: boolean
default: true
- name: runTests
type: boolean
default: true
@@ -52,6 +55,7 @@ stages:
buildConfigurations: [Release]
enablePackageCaching: true
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
msBuildCacheIsReadOnly: ${{ parameters.msBuildCacheIsReadOnly }}
runTests: ${{ parameters.runTests }}
useVSPreview: ${{ parameters.useVSPreview }}
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}

View File

@@ -7,7 +7,7 @@ using Microsoft.UI.Xaml.Data;
namespace Microsoft.CmdPal.UI.Controls;
public sealed class IconMarginConverter : IValueConverter
public sealed partial class IconMarginConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.08535 3C7.29127 2.4174 7.84689 2 8.5 2H11.5C12.1531 2 12.7087 2.4174 12.9146 3H14.5C15.3284 3 16 3.67157 16 4.5V16.5C16 17.3284 15.3284 18 14.5 18H5.5C4.67157 18 4 17.3284 4 16.5V4.5C4 3.67157 4.67157 3 5.5 3H7.08535ZM8.5 3C8.22386 3 8 3.22386 8 3.5C8 3.77614 8.22386 4 8.5 4H11.5C11.7761 4 12 3.77614 12 3.5C12 3.22386 11.7761 3 11.5 3H8.5ZM7.08535 4H5.5C5.22386 4 5 4.22386 5 4.5V16.5C5 16.7761 5.22386 17 5.5 17H14.5C14.7761 17 15 16.7761 15 16.5V4.5C15 4.22386 14.7761 4 14.5 4H12.9146C12.7087 4.5826 12.1531 5 11.5 5H8.5C7.84689 5 7.29127 4.5826 7.08535 4Z" fill="#D0D0D0"/>
</svg>

After

Width:  |  Height:  |  Size: 695 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.08535 3C7.29127 2.4174 7.84689 2 8.5 2H11.5C12.1531 2 12.7087 2.4174 12.9146 3H14.5C15.3284 3 16 3.67157 16 4.5V16.5C16 17.3284 15.3284 18 14.5 18H5.5C4.67157 18 4 17.3284 4 16.5V4.5C4 3.67157 4.67157 3 5.5 3H7.08535ZM8.5 3C8.22386 3 8 3.22386 8 3.5C8 3.77614 8.22386 4 8.5 4H11.5C11.7761 4 12 3.77614 12 3.5C12 3.22386 11.7761 3 11.5 3H8.5ZM7.08535 4H5.5C5.22386 4 5 4.22386 5 4.5V16.5C5 16.7761 5.22386 17 5.5 17H14.5C14.7761 17 15 16.7761 15 16.5V4.5C15 4.22386 14.7761 4 14.5 4H12.9146C12.7087 4.5826 12.1531 5 11.5 5H8.5C7.84689 5 7.29127 4.5826 7.08535 4Z" fill="#212121"/>
</svg>

After

Width:  |  Height:  |  Size: 695 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.08535 3C7.29127 2.4174 7.84689 2 8.5 2H11.5C12.1531 2 12.7087 2.4174 12.9146 3H14.5C15.3284 3 16 3.67157 16 4.5V9H15V4.5C15 4.22386 14.7761 4 14.5 4H12.9146C12.7087 4.5826 12.1531 5 11.5 5H8.5C7.84689 5 7.29127 4.5826 7.08535 4H5.5C5.22386 4 5 4.22386 5 4.5V16.5C5 16.7761 5.22386 17 5.5 17H9.03544C9.08595 17.3531 9.18915 17.6891 9.33682 18H5.5C4.67157 18 4 17.3284 4 16.5V4.5C4 3.67157 4.67157 3 5.5 3H7.08535ZM8.5 3C8.22386 3 8 3.22386 8 3.5C8 3.77614 8.22386 4 8.5 4H11.5C11.7761 4 12 3.77614 12 3.5C12 3.22386 11.7761 3 11.5 3H8.5ZM10 12.5C10 11.1193 11.1193 10 12.5 10H16.5C17.8807 10 19 11.1193 19 12.5V16.5C19 17.0095 18.8476 17.4835 18.5858 17.8787L15.5607 14.8536C14.9749 14.2678 14.0251 14.2678 13.4393 14.8536L10.4142 17.8787C10.1524 17.4835 10 17.0095 10 16.5V12.5ZM17 12.75C17 12.3358 16.6642 12 16.25 12C15.8358 12 15.5 12.3358 15.5 12.75C15.5 13.1642 15.8358 13.5 16.25 13.5C16.6642 13.5 17 13.1642 17 12.75ZM11.1213 18.5858C11.5165 18.8476 11.9905 19 12.5 19H16.5C17.0095 19 17.4835 18.8476 17.8787 18.5858L14.8536 15.5607C14.6583 15.3654 14.3417 15.3654 14.1464 15.5607L11.1213 18.5858Z" fill="#D0D0D0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.08535 3C7.29127 2.4174 7.84689 2 8.5 2H11.5C12.1531 2 12.7087 2.4174 12.9146 3H14.5C15.3284 3 16 3.67157 16 4.5V9H15V4.5C15 4.22386 14.7761 4 14.5 4H12.9146C12.7087 4.5826 12.1531 5 11.5 5H8.5C7.84689 5 7.29127 4.5826 7.08535 4H5.5C5.22386 4 5 4.22386 5 4.5V16.5C5 16.7761 5.22386 17 5.5 17H9.03544C9.08595 17.3531 9.18915 17.6891 9.33682 18H5.5C4.67157 18 4 17.3284 4 16.5V4.5C4 3.67157 4.67157 3 5.5 3H7.08535ZM8.5 3C8.22386 3 8 3.22386 8 3.5C8 3.77614 8.22386 4 8.5 4H11.5C11.7761 4 12 3.77614 12 3.5C12 3.22386 11.7761 3 11.5 3H8.5ZM10 12.5C10 11.1193 11.1193 10 12.5 10H16.5C17.8807 10 19 11.1193 19 12.5V16.5C19 17.0095 18.8476 17.4835 18.5858 17.8787L15.5607 14.8536C14.9749 14.2678 14.0251 14.2678 13.4393 14.8536L10.4142 17.8787C10.1524 17.4835 10 17.0095 10 16.5V12.5ZM17 12.75C17 12.3358 16.6642 12 16.25 12C15.8358 12 15.5 12.3358 15.5 12.75C15.5 13.1642 15.8358 13.5 16.25 13.5C16.6642 13.5 17 13.1642 17 12.75ZM11.1213 18.5858C11.5165 18.8476 11.9905 19 12.5 19H16.5C17.0095 19 17.4835 18.8476 17.8787 18.5858L14.8536 15.5607C14.6583 15.3654 14.3417 15.3654 14.1464 15.5607L11.1213 18.5858Z" fill="#212121"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.08535 3C7.29127 2.4174 7.84689 2 8.5 2H11.5C12.1531 2 12.7087 2.4174 12.9146 3H14.5C15.3284 3 16 3.67157 16 4.5V10.337L15.3698 8.89819C15.2826 8.69904 15.1554 8.52562 15 8.38568V4.5C15 4.22386 14.7761 4 14.5 4H12.9146C12.7087 4.5826 12.1531 5 11.5 5H8.5C7.84689 5 7.29127 4.5826 7.08535 4H5.5C5.22386 4 5 4.22386 5 4.5V16.5C5 16.7761 5.22386 17 5.5 17H9.08561C8.9672 17.3338 8.97447 17.6857 9.08567 18H5.5C4.67157 18 4 17.3284 4 16.5V4.5C4 3.67157 4.67157 3 5.5 3H7.08535ZM8.5 3C8.22386 3 8 3.22386 8 3.5C8 3.77614 8.22386 4 8.5 4H11.5C11.7761 4 12 3.77614 12 3.5C12 3.22386 11.7761 3 11.5 3H8.5ZM14.4538 9.2994C14.3741 9.11744 14.1942 8.99992 13.9956 9C13.797 9.00008 13.6172 9.11776 13.5376 9.29979L10.0417 17.2998C9.93114 17.5528 10.0466 17.8476 10.2997 17.9582C10.5527 18.0687 10.8475 17.9533 10.958 17.7002L12.138 15H15.859L17.0419 17.7006C17.1527 17.9535 17.4475 18.0688 17.7005 17.958C17.9534 17.8472 18.0687 17.5523 17.9579 17.2994L14.4538 9.2994ZM15.421 14H12.575L13.9963 10.7474L15.421 14Z" fill="#D0D0D0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.08535 3C7.29127 2.4174 7.84689 2 8.5 2H11.5C12.1531 2 12.7087 2.4174 12.9146 3H14.5C15.3284 3 16 3.67157 16 4.5V10.337L15.3698 8.89819C15.2826 8.69904 15.1554 8.52562 15 8.38568V4.5C15 4.22386 14.7761 4 14.5 4H12.9146C12.7087 4.5826 12.1531 5 11.5 5H8.5C7.84689 5 7.29127 4.5826 7.08535 4H5.5C5.22386 4 5 4.22386 5 4.5V16.5C5 16.7761 5.22386 17 5.5 17H9.08561C8.9672 17.3338 8.97447 17.6857 9.08567 18H5.5C4.67157 18 4 17.3284 4 16.5V4.5C4 3.67157 4.67157 3 5.5 3H7.08535ZM8.5 3C8.22386 3 8 3.22386 8 3.5C8 3.77614 8.22386 4 8.5 4H11.5C11.7761 4 12 3.77614 12 3.5C12 3.22386 11.7761 3 11.5 3H8.5ZM14.4538 9.2994C14.3741 9.11744 14.1942 8.99992 13.9956 9C13.797 9.00008 13.6172 9.11776 13.5376 9.29979L10.0417 17.2998C9.93114 17.5528 10.0466 17.8476 10.2997 17.9582C10.5527 18.0687 10.8475 17.9533 10.958 17.7002L12.138 15H15.859L17.0419 17.7006C17.1527 17.9535 17.4475 18.0688 17.7005 17.958C17.9534 17.8472 18.0687 17.5523 17.9579 17.2994L14.4538 9.2994ZM15.421 14H12.575L13.9963 10.7474L15.421 14Z" fill="#212121"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2C6.89543 2 6 2.89543 6 4V14C6 15.1046 6.89543 16 8 16H14C15.1046 16 16 15.1046 16 14V4C16 2.89543 15.1046 2 14 2H8ZM7 4C7 3.44772 7.44772 3 8 3H14C14.5523 3 15 3.44772 15 4V14C15 14.5523 14.5523 15 14 15H8C7.44772 15 7 14.5523 7 14V4ZM4 6.00001C4 5.25973 4.4022 4.61339 5 4.26758V14.5C5 15.8807 6.11929 17 7.5 17H13.7324C13.3866 17.5978 12.7403 18 12 18H7.5C5.567 18 4 16.433 4 14.5V6.00001Z" fill="#D0D0D0"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2C6.89543 2 6 2.89543 6 4V14C6 15.1046 6.89543 16 8 16H14C15.1046 16 16 15.1046 16 14V4C16 2.89543 15.1046 2 14 2H8ZM7 4C7 3.44772 7.44772 3 8 3H14C14.5523 3 15 3.44772 15 4V14C15 14.5523 14.5523 15 14 15H8C7.44772 15 7 14.5523 7 14V4ZM4 6.00001C4 5.25973 4.4022 4.61339 5 4.26758V14.5C5 15.8807 6.11929 17 7.5 17H13.7324C13.3866 17.5978 12.7403 18 12 18H7.5C5.567 18 4 16.433 4 14.5V6.00001Z" fill="#212121"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 4C6 2.89543 6.89543 2 8 2H11.5858C11.9836 2 12.3651 2.15804 12.6464 2.43934L16.5607 6.35355C16.842 6.63486 17 7.01639 17 7.41421V14C17 15.1046 16.1046 16 15 16H8C6.89543 16 6 15.1046 6 14V4ZM8 3C7.44772 3 7 3.44772 7 4V14C7 14.5523 7.44772 15 8 15H15C15.5523 15 16 14.5523 16 14V8H12.5C11.6716 8 11 7.32843 11 6.5V3H8ZM12 3.20711V6.5C12 6.77614 12.2239 7 12.5 7H15.7929L12 3.20711ZM4 5C4 4.44772 4.44772 4 5 4V14C5 15.6569 6.34315 17 8 17L15 17C15 17.5523 14.5523 18 14 18H7.93939C5.76373 18 4 16.2363 4 14.0606V5Z" fill="#D0D0D0"/>
</svg>

After

Width:  |  Height:  |  Size: 648 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 4C6 2.89543 6.89543 2 8 2H11.5858C11.9836 2 12.3651 2.15804 12.6464 2.43934L16.5607 6.35355C16.842 6.63486 17 7.01639 17 7.41421V14C17 15.1046 16.1046 16 15 16H8C6.89543 16 6 15.1046 6 14V4ZM8 3C7.44772 3 7 3.44772 7 4V14C7 14.5523 7.44772 15 8 15H15C15.5523 15 16 14.5523 16 14V8H12.5C11.6716 8 11 7.32843 11 6.5V3H8ZM12 3.20711V6.5C12 6.77614 12.2239 7 12.5 7H15.7929L12 3.20711ZM4 5C4 4.44772 4.44772 4 5 4V14C5 15.6569 6.34315 17 8 17L15 17C15 17.5523 14.5523 18 14 18H7.93939C5.76373 18 4 16.2363 4 14.0606V5Z" fill="#212121"/>
</svg>

After

Width:  |  Height:  |  Size: 648 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.49751 7.4966C9.04842 7.4966 9.49502 7.05 9.49502 6.4991C9.49502 5.94819 9.04842 5.50159 8.49751 5.50159C7.9466 5.50159 7.5 5.94819 7.5 6.4991C7.5 7.05 7.9466 7.4966 8.49751 7.4966ZM5 6C5 4.34315 6.34315 3 8 3H14C15.6569 3 17 4.34315 17 6V12C17 13.6569 15.6569 15 14 15H8C6.34315 15 5 13.6569 5 12V6ZM8 4C6.89543 4 6 4.89543 6 6V12C6 12.3709 6.10097 12.7182 6.27692 13.016L9.79085 9.50207C10.4586 8.83427 11.5414 8.83427 12.2092 9.50207L15.7231 13.016C15.899 12.7182 16 12.3709 16 12V6C16 4.89543 15.1046 4 14 4H8ZM15.016 13.7231L11.502 10.2092C11.2248 9.9319 10.7752 9.9319 10.498 10.2092L6.98403 13.7231C7.28178 13.899 7.6291 14 8 14H14C14.3709 14 14.7182 13.899 15.016 13.7231ZM12 17C12.8885 17 13.6868 16.6138 14.2361 16H7.5C5.68782 16 4.1973 14.6228 4.01807 12.8579C4.00612 12.7402 4 12.6208 4 12.5V5.76392C3.38625 6.31324 3 7.11152 3 8.00002V12.5C3 14.9853 5.01472 17 7.5 17H12Z" fill="#D0D0D0"/>
</svg>

After

Width:  |  Height:  |  Size: 1017 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.49751 7.4966C9.04842 7.4966 9.49502 7.05 9.49502 6.4991C9.49502 5.94819 9.04842 5.50159 8.49751 5.50159C7.9466 5.50159 7.5 5.94819 7.5 6.4991C7.5 7.05 7.9466 7.4966 8.49751 7.4966ZM5 6C5 4.34315 6.34315 3 8 3H14C15.6569 3 17 4.34315 17 6V12C17 13.6569 15.6569 15 14 15H8C6.34315 15 5 13.6569 5 12V6ZM8 4C6.89543 4 6 4.89543 6 6V12C6 12.3709 6.10097 12.7182 6.27692 13.016L9.79085 9.50207C10.4586 8.83427 11.5414 8.83427 12.2092 9.50207L15.7231 13.016C15.899 12.7182 16 12.3709 16 12V6C16 4.89543 15.1046 4 14 4H8ZM15.016 13.7231L11.502 10.2092C11.2248 9.9319 10.7752 9.9319 10.498 10.2092L6.98403 13.7231C7.28178 13.899 7.6291 14 8 14H14C14.3709 14 14.7182 13.899 15.016 13.7231ZM12 17C12.8885 17 13.6868 16.6138 14.2361 16H7.5C5.68782 16 4.1973 14.6228 4.01807 12.8579C4.00612 12.7402 4 12.6208 4 12.5V5.76392C3.38625 6.31324 3 7.11152 3 8.00002V12.5C3 14.9853 5.01472 17 7.5 17H12Z" fill="#212121"/>
</svg>

After

Width:  |  Height:  |  Size: 1017 B

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.
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
internal enum PrimaryAction
{
Default,
Paste,
Copy,
}

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using Microsoft.CmdPal.Ext.ClipboardHistory.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -26,10 +27,22 @@ internal sealed class SettingsManager : JsonSettingsManager, ISettingOptions
Resources.settings_confirm_delete_description!,
true);
private readonly ChoiceSetSetting _primaryAction = new(
Namespaced(nameof(PrimaryAction)),
Resources.settings_primary_action_title!,
Resources.settings_primary_action_description!,
[
new ChoiceSetSetting.Choice(Resources.settings_primary_action_default!, PrimaryAction.Default.ToString("G")),
new ChoiceSetSetting.Choice(Resources.settings_primary_action_paste!, PrimaryAction.Paste.ToString("G")),
new ChoiceSetSetting.Choice(Resources.settings_primary_action_copy!, PrimaryAction.Copy.ToString("G"))
]);
public bool KeepAfterPaste => _keepAfterPaste.Value;
public bool DeleteFromHistoryRequiresConfirmation => _confirmDelete.Value;
public PrimaryAction PrimaryAction => Enum.TryParse<PrimaryAction>(_primaryAction.Value, out var action) ? action : PrimaryAction.Default;
private static string SettingsJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
@@ -45,6 +58,7 @@ internal sealed class SettingsManager : JsonSettingsManager, ISettingOptions
Settings.Add(_keepAfterPaste);
Settings.Add(_confirmDelete);
Settings.Add(_primaryAction);
// Load settings from file upon initialization
LoadSettings();

View File

@@ -6,7 +6,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory;
internal sealed class Icons
internal static class Icons
{
internal static IconInfo CopyIcon { get; } = new("\xE8C8");
@@ -17,4 +17,21 @@ internal sealed class Icons
internal static IconInfo DeleteIcon { get; } = new("\uE74D");
internal static IconInfo ClipboardListIcon { get; } = IconHelpers.FromRelativePath("Assets\\ClipboardHistory.svg");
internal static IconInfo Clipboard { get; } = Create("ic_fluent_clipboard_20_regular");
internal static IconInfo ClipboardImage { get; } = Create("ic_fluent_clipboard_image_20_regular");
internal static IconInfo ClipboardLetter { get; } = Create("ic_fluent_clipboard_letter_20_regular");
internal static IconInfo Copy { get; } = Create(" ic_fluent_copy_20_regular");
internal static IconInfo DocumentCopy { get; } = Create("ic_fluent_document_copy_20_regular");
internal static IconInfo ImageCopy { get; } = Create("ic_fluent_image_copy_20_regular");
private static IconInfo Create(string name)
{
return IconHelpers.FromRelativePaths($"Assets\\Icons\\{name}.light.svg", $"Assets\\Icons\\{name}.dark.svg");
}
}

View File

@@ -39,5 +39,41 @@
<Content Update="Assets\ClipboardHistory.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Icons\ic_fluent_clipboard_20_regular.dark.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Icons\ic_fluent_clipboard_20_regular.light.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Icons\ic_fluent_clipboard_image_20_regular.dark.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Icons\ic_fluent_clipboard_image_20_regular.light.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Icons\ic_fluent_clipboard_letter_20_regular.dark.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Icons\ic_fluent_clipboard_letter_20_regular.light.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Icons\ic_fluent_copy_20_regular.dark.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Icons\ic_fluent_copy_20_regular.light.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Icons\ic_fluent_document_copy_20_regular.dark.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Icons\ic_fluent_document_copy_20_regular.light.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Icons\ic_fluent_image_copy_20_regular.dark.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Icons\ic_fluent_image_copy_20_regular.light.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -3,14 +3,8 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Microsoft.CmdPal.Common.Commands;
using Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage.Streams;
@@ -41,126 +35,8 @@ public class ClipboardItem
}
[MemberNotNullWhen(true, nameof(ImageData))]
private bool IsImage => ImageData is not null;
internal bool IsImage => ImageData is not null;
[MemberNotNullWhen(true, nameof(Content))]
private bool IsText => !string.IsNullOrEmpty(Content);
public static List<string> ShiftLinesLeft(List<string> lines)
{
// Determine the minimum leading whitespace
var minLeadingWhitespace = lines
.Where(line => !string.IsNullOrWhiteSpace(line))
.Min(line => line.TakeWhile(char.IsWhiteSpace).Count());
// Check if all lines have at least that much leading whitespace
if (lines.Any(line => line.TakeWhile(char.IsWhiteSpace).Count() < minLeadingWhitespace))
{
return lines; // Return the original lines if any line doesn't have enough leading whitespace
}
// Remove the minimum leading whitespace from each line
var shiftedLines = lines.Select(line => line.Substring(minLeadingWhitespace)).ToList();
return shiftedLines;
}
public static List<string> StripLeadingWhitespace(List<string> lines)
{
// Determine the minimum leading whitespace
var minLeadingWhitespace = lines
.Min(line => line.TakeWhile(char.IsWhiteSpace).Count());
// Remove the minimum leading whitespace from each line
var shiftedLines = lines.Select(line =>
line.Length >= minLeadingWhitespace
? line.Substring(minLeadingWhitespace)
: line).ToList();
return shiftedLines;
}
public ListItem ToListItem()
{
ListItem listItem;
List<DetailsElement> metadata = [];
metadata.Add(new DetailsElement()
{
Key = "Copied on",
Data = new DetailsLink(Item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
});
var deleteConfirmationCommand = new ConfirmableCommand()
{
Command = new DeleteItemCommand(this),
ConfirmationTitle = Properties.Resources.delete_confirmation_title!,
ConfirmationMessage = Properties.Resources.delete_confirmation_message!,
IsConfirmationRequired = () => Settings.DeleteFromHistoryRequiresConfirmation,
};
var deleteContextMenuItem = new CommandContextItem(deleteConfirmationCommand)
{
IsCritical = true,
RequestedShortcut = KeyChords.DeleteEntry,
};
if (IsImage)
{
var iconData = new IconData(ImageData);
var heroImage = new IconInfo(iconData, iconData);
listItem = new(new CopyCommand(this, ClipboardFormat.Image))
{
// Placeholder subtitle as theres no BitmapImage dimensions to retrieve
Title = "Image Data",
Details = new Details()
{
HeroImage = heroImage,
Title = GetDataType(),
Body = Timestamp.ToString(CultureInfo.InvariantCulture),
Metadata = metadata.ToArray(),
},
MoreCommands = [
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image, Settings)),
new Separator(),
deleteContextMenuItem,
],
};
}
else if (IsText)
{
var splitContent = Content.Split("\n");
var head = splitContent.AsSpan(0, Math.Min(3, splitContent.Length)).ToArray().ToList();
var preview2 = string.Join(
"\n",
StripLeadingWhitespace(head));
listItem = new(new CopyCommand(this, ClipboardFormat.Text))
{
Title = preview2,
Details = new Details
{
Title = GetDataType(),
Body = $"```text\n{Content}\n```",
Metadata = metadata.ToArray(),
},
MoreCommands = [
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text, Settings)),
new Separator(),
deleteContextMenuItem,
],
};
}
else
{
listItem = new(new NoOpCommand())
{
Title = "Unknown",
Subtitle = GetDataType(),
Details = new Details { Title = GetDataType() },
};
}
return listItem;
}
internal bool IsText => !string.IsNullOrEmpty(Content);
}

View File

@@ -148,7 +148,7 @@ internal sealed partial class ClipboardHistoryListPage : ListPage
var item = clipboardHistory[i];
if (item is not null)
{
listItems.Add(item.ToListItem());
listItems.Add(new ClipboardListItem(item, _settingsManager));
}
}

View File

@@ -0,0 +1,201 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.CmdPal.Common.Commands;
using Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
internal sealed partial class ClipboardListItem : ListItem
{
private readonly SettingsManager _settingsManager;
private readonly ClipboardItem _item;
private readonly CommandContextItem _deleteContextMenuItem;
private readonly CommandContextItem? _pasteCommand;
private readonly CommandContextItem? _copyCommand;
private readonly Lazy<Details> _lazyDetails;
public override IDetails? Details
{
get => _lazyDetails.Value;
set
{
}
}
public ClipboardListItem(ClipboardItem item, SettingsManager settingsManager)
{
_item = item;
_settingsManager = settingsManager;
_settingsManager.Settings.SettingsChanged += SettingsOnSettingsChanged;
_lazyDetails = new(() => CreateDetails());
var deleteConfirmationCommand = new ConfirmableCommand
{
Command = new DeleteItemCommand(_item),
ConfirmationTitle = Properties.Resources.delete_confirmation_title!,
ConfirmationMessage = Properties.Resources.delete_confirmation_message!,
IsConfirmationRequired = () => _settingsManager.DeleteFromHistoryRequiresConfirmation,
};
_deleteContextMenuItem = new CommandContextItem(deleteConfirmationCommand)
{
IsCritical = true,
RequestedShortcut = KeyChords.DeleteEntry,
};
if (item.IsImage)
{
Title = "Image";
_pasteCommand = new CommandContextItem(new PasteCommand(_item, ClipboardFormat.Image, _settingsManager));
_copyCommand = new CommandContextItem(new CopyCommand(_item, ClipboardFormat.Image));
}
else if (item.IsText)
{
var splitContent = _item.Content?.Split("\n") ?? [];
var head = splitContent.Take(3);
var preview2 = string.Join(
"\n",
StripLeadingWhitespace(head));
Title = preview2;
_pasteCommand = new CommandContextItem(new PasteCommand(_item, ClipboardFormat.Text, _settingsManager));
_copyCommand = new CommandContextItem(new CopyCommand(_item, ClipboardFormat.Text));
}
else
{
_pasteCommand = null;
_copyCommand = null;
}
RefreshCommands();
}
private void SettingsOnSettingsChanged(object sender, Settings args)
{
RefreshCommands();
}
private void RefreshCommands()
{
if (_item is { IsText: false, IsImage: false })
{
MoreCommands = [_deleteContextMenuItem];
Icon = _settingsManager.PrimaryAction == PrimaryAction.Paste ? Icons.Clipboard : Icons.Copy;
}
switch (_settingsManager.PrimaryAction)
{
case PrimaryAction.Paste:
Command = _pasteCommand?.Command;
MoreCommands =
[
_copyCommand!,
new Separator(),
_deleteContextMenuItem,
];
if (_item.IsText)
{
Icon = Icons.ClipboardLetter;
}
else if (_item.IsImage)
{
Icon = Icons.ClipboardImage;
}
else
{
Icon = Icons.ClipboardImage;
}
break;
case PrimaryAction.Default:
case PrimaryAction.Copy:
default:
Command = _copyCommand?.Command;
MoreCommands =
[
_pasteCommand!,
new Separator(),
_deleteContextMenuItem,
];
if (_item.IsText)
{
Icon = Icons.DocumentCopy;
}
else if (_item.IsImage)
{
Icon = Icons.ImageCopy;
}
else
{
Icon = Icons.Copy;
}
break;
}
}
private Details CreateDetails()
{
IDetailsElement[] metadata =
[
new DetailsElement
{
Key = "Copied on",
Data = new DetailsLink(_item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
}
];
if (_item.IsImage)
{
var iconData = new IconData(_item.ImageData);
var heroImage = new IconInfo(iconData);
return new Details
{
Title = _item.GetDataType(),
HeroImage = heroImage,
Metadata = metadata,
};
}
if (_item.IsText)
{
return new Details
{
Title = _item.GetDataType(),
Body = $"```text\n{_item.Content}\n```",
Metadata = metadata,
};
}
return new Details { Title = _item.GetDataType() };
}
private static List<string> StripLeadingWhitespace(IEnumerable<string> lines)
{
// Determine the minimum leading whitespace
var minLeadingWhitespace = lines
.Min(static line => line.TakeWhile(char.IsWhiteSpace).Count());
// Remove the minimum leading whitespace from each line
var shiftedLines = lines.Select(line =>
line.Length >= minLeadingWhitespace
? line[minLeadingWhitespace..]
: line).ToList();
return shiftedLines;
}
}

View File

@@ -212,5 +212,50 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
return ResourceManager.GetString("settings_keep_after_paste_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy to Clipboard.
/// </summary>
public static string settings_primary_action_copy {
get {
return ResourceManager.GetString("settings_primary_action_copy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Default (Copy to Clipboard).
/// </summary>
public static string settings_primary_action_default {
get {
return ResourceManager.GetString("settings_primary_action_default", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Primary action (Enter key).
/// </summary>
public static string settings_primary_action_description {
get {
return ResourceManager.GetString("settings_primary_action_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Paste.
/// </summary>
public static string settings_primary_action_paste {
get {
return ResourceManager.GetString("settings_primary_action_paste", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Primary action.
/// </summary>
public static string settings_primary_action_title {
get {
return ResourceManager.GetString("settings_primary_action_title", resourceCulture);
}
}
}
}

View File

@@ -168,4 +168,19 @@
<data name="delete_confirmation_message" xml:space="preserve">
<value>Are you sure you want to delete this item from clipboard history? This action cannot be undone.</value>
</data>
<data name="settings_primary_action_title" xml:space="preserve">
<value>Primary action</value>
</data>
<data name="settings_primary_action_description" xml:space="preserve">
<value>Primary action (Enter key)</value>
</data>
<data name="settings_primary_action_default" xml:space="preserve">
<value>Default (Copy to Clipboard)</value>
</data>
<data name="settings_primary_action_paste" xml:space="preserve">
<value>Paste</value>
</data>
<data name="settings_primary_action_copy" xml:space="preserve">
<value>Copy to Clipboard</value>
</data>
</root>

View File

@@ -30,7 +30,7 @@ internal sealed class ContextMenuHelper
// Hide menu if Explorer.exe is the shell process or the process name is ApplicationFrameHost.exe
// In the first case we would crash the windows ui and in the second case we would kill the generic process for uwp apps.
if (!windowData.Process.IsShellProcess && !(windowData.Process.IsUwpApp && string.Equals(windowData.Process.Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase))
if (!windowData.Process.IsShellProcess && !(windowData.Process.IsUwpAppFrameHost && string.Equals(windowData.Process.Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase))
&& !(windowData.Process.IsFullAccessDenied && SettingsManager.Instance.HideKillProcessOnElevatedProcesses))
{
contextMenu.Add(new CommandContextItem(new EndTaskCommand(windowData))

View File

@@ -23,7 +23,7 @@ internal sealed class WindowProcess
/// <summary>
/// An indicator if the window belongs to an 'Universal Windows Platform (UWP)' process
/// </summary>
private readonly bool _isUwpApp;
private readonly bool _isUwpAppFrameHost;
/// <summary>
/// Gets the id of the process
@@ -42,7 +42,8 @@ internal sealed class WindowProcess
{
try
{
return Process.GetProcessById((int)ProcessID).Responding;
// Process.Responding doesn't work on UWP apps
return ProcessType.Kind == ProcessPackagingKind.UwpApp || Process.GetProcessById((int)ProcessID).Responding;
}
catch (InvalidOperationException)
{
@@ -76,7 +77,7 @@ internal sealed class WindowProcess
/// <summary>
/// Gets a value indicating whether the window belongs to an 'Universal Windows Platform (UWP)' process
/// </summary>
internal bool IsUwpApp => _isUwpApp;
public bool IsUwpAppFrameHost => _isUwpAppFrameHost;
/// <summary>
/// Gets a value indicating whether this is the shell process or not
@@ -134,9 +135,12 @@ internal sealed class WindowProcess
internal WindowProcess(uint pid, uint tid, string name)
{
UpdateProcessInfo(pid, tid, name);
_isUwpApp = string.Equals(Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase);
ProcessType = ProcessPackagingInspector.Inspect((int)pid);
_isUwpAppFrameHost = string.Equals(Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase);
}
public ProcessPackagingInfo ProcessType { get; private set; }
/// <summary>
/// Updates the process information of the <see cref="WindowProcess"/> instance.
/// </summary>

View File

@@ -5,7 +5,7 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute;
#pragma warning disable SA1649, CA1051, CA1707, CA1028, CA1714, CA1069, SA1402
@@ -98,6 +98,25 @@ public static partial class NativeMethods
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetFirmwareType(ref FirmwareType FirmwareType);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool OpenProcessToken(SafeProcessHandle processHandle, TokenAccess desiredAccess, out SafeAccessTokenHandle tokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetTokenInformation(
SafeAccessTokenHandle tokenHandle,
TOKEN_INFORMATION_CLASS tokenInformationClass,
out int tokenInformation,
int tokenInformationLength,
out int returnLength);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "GetPackageFullName")]
internal static extern int GetPackageFullName(
SafeProcessHandle hProcess,
ref uint packageFullNameLength,
StringBuilder? packageFullName);
}
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "These are the names used by win32.")]
@@ -383,7 +402,7 @@ public enum ShowWindowCommand
/// <summary>
/// Displays a window in its most recent size and position. This value
/// is similar to <see cref="Win32.ShowWindowCommand.Normal"/>, except
/// is similar to <see cref="ShowWindowCommand.Normal"/>, except
/// the window is not activated.
/// </summary>
ShowNoActivate = 4,
@@ -401,14 +420,14 @@ public enum ShowWindowCommand
/// <summary>
/// Displays the window as a minimized window. This value is similar to
/// <see cref="Win32.ShowWindowCommand.ShowMinimized"/>, except the
/// <see cref="ShowWindowCommand.ShowMinimized"/>, except the
/// window is not activated.
/// </summary>
ShowMinNoActive = 7,
/// <summary>
/// Displays the window in its current size and position. This value is
/// similar to <see cref="Win32.ShowWindowCommand.Show"/>, except the
/// similar to <see cref="ShowWindowCommand.Show"/>, except the
/// window is not activated.
/// </summary>
ShowNA = 8,
@@ -1100,3 +1119,14 @@ public enum SIGDN : uint
FILESYSPATH = 0x80058000,
URL = 0x80068000,
}
internal enum TOKEN_INFORMATION_CLASS
{
TokenIsAppContainer = 29,
}
[Flags]
internal enum TokenAccess : uint
{
TOKEN_QUERY = 0x0008,
}

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.WindowWalker.Helpers;
internal sealed record ProcessPackagingInfo(
int Pid,
ProcessPackagingKind Kind,
bool HasPackageIdentity,
bool IsAppContainer,
string? PackageFullName,
int? LastError
);

View File

@@ -0,0 +1,123 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers;
internal static class ProcessPackagingInspector
{
#pragma warning disable SA1310 // Field names should not contain underscore
private const int ERROR_INSUFFICIENT_BUFFER = 122;
private const int APPMODEL_ERROR_NO_PACKAGE = 15700;
#pragma warning restore SA1310 // Field names should not contain underscore
/// <summary>
/// Inspect a process by PID and classify its packaging.
/// </summary>
public static ProcessPackagingInfo Inspect(int pid)
{
var hProcess = NativeMethods.OpenProcess(ProcessAccessFlags.QueryLimitedInformation, false, pid);
using var process = new SafeProcessHandle(hProcess, true);
if (process.IsInvalid)
{
return new ProcessPackagingInfo(
pid,
ProcessPackagingKind.Unknown,
HasPackageIdentity: false,
IsAppContainer: false,
PackageFullName: null,
LastError: Marshal.GetLastPInvokeError());
}
// 1) Check package identity
var hasPackage = TryGetPackageFullName(process, out var packageFullName, out _);
// 2) If packaged, check AppContainer -> strict UWP
var isAppContainer = false;
int? tokenErr = null;
if (hasPackage)
{
isAppContainer = TryIsAppContainer(process, out tokenErr);
}
var kind =
!hasPackage ? ProcessPackagingKind.UnpackagedWin32 :
isAppContainer ? ProcessPackagingKind.UwpApp :
ProcessPackagingKind.PackagedWin32;
return new ProcessPackagingInfo(
pid,
kind,
HasPackageIdentity: hasPackage,
IsAppContainer: isAppContainer,
PackageFullName: packageFullName,
LastError: null);
}
private static bool TryGetPackageFullName(SafeProcessHandle hProcess, out string? packageFullName, out int? lastError)
{
packageFullName = null;
lastError = null;
uint len = 0;
var rc = NativeMethods.GetPackageFullName(hProcess, ref len, null);
if (rc == APPMODEL_ERROR_NO_PACKAGE)
{
return false; // no package identity
}
if (rc != ERROR_INSUFFICIENT_BUFFER && rc != 0)
{
lastError = rc;
return false; // unexpected error
}
if (len == 0)
{
return false;
}
var sb = new StringBuilder((int)len);
rc = NativeMethods.GetPackageFullName(hProcess, ref len, sb);
if (rc == 0)
{
packageFullName = sb.ToString();
return true;
}
lastError = rc;
return false;
}
private static bool TryIsAppContainer(SafeProcessHandle hProcess, out int? lastError)
{
lastError = null;
if (!NativeMethods.OpenProcessToken(hProcess, TokenAccess.TOKEN_QUERY, out var token))
{
lastError = Marshal.GetLastPInvokeError();
return false; // can't decide; treat as not-UWP for classification
}
using (token)
{
if (!NativeMethods.GetTokenInformation(
token,
TOKEN_INFORMATION_CLASS.TokenIsAppContainer,
out var val,
sizeof(int),
out _))
{
lastError = Marshal.GetLastPInvokeError();
return false;
}
return val != 0; // true => AppContainer (UWP)
}
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers;
internal enum ProcessPackagingKind
{
Unknown = 0,
UnpackagedWin32,
PackagedWin32,
UwpApp,
}

View File

@@ -20,13 +20,15 @@ internal sealed partial class LaunchProfileAsAdminCommand : InvokableCommand
private readonly string _profile;
private readonly bool _openNewTab;
private readonly bool _openQuake;
private readonly AppSettingsManager _appSettingsManager;
internal LaunchProfileAsAdminCommand(string id, string profile, bool openNewTab, bool openQuake)
internal LaunchProfileAsAdminCommand(string id, string profile, bool openNewTab, bool openQuake, AppSettingsManager appSettingsManager)
{
this._id = id;
this._profile = profile;
this._openNewTab = openNewTab;
this._openQuake = openQuake;
this._appSettingsManager = appSettingsManager;
this.Name = Resources.launch_profile_as_admin;
this.Icon = Icons.AdminIcon;
@@ -59,6 +61,17 @@ internal sealed partial class LaunchProfileAsAdminCommand : InvokableCommand
//_context.API.ShowMsg(name, message, string.Empty);
Logger.LogError($"Failed to open Windows Terminal: {ex.Message}");
}
try
{
_appSettingsManager.Current.AddRecentlyUsedProfile(id, profile);
_appSettingsManager.Save();
}
catch (Exception ex)
{
// We don't want to fail the whole operation if we can't save the recently used profile
Logger.LogError($"Failed to save recently used profile: {ex.Message}");
}
}
#pragma warning restore IDE0059, CS0168, SA1005

View File

@@ -20,13 +20,15 @@ internal sealed partial class LaunchProfileCommand : InvokableCommand
private readonly string _profile;
private readonly bool _openNewTab;
private readonly bool _openQuake;
private readonly AppSettingsManager _appSettingsManager;
internal LaunchProfileCommand(string id, string profile, string iconPath, bool openNewTab, bool openQuake)
internal LaunchProfileCommand(string id, string profile, string iconPath, bool openNewTab, bool openQuake, AppSettingsManager appSettingsManager)
{
this._id = id;
this._profile = profile;
this._openNewTab = openNewTab;
this._openQuake = openQuake;
this._appSettingsManager = appSettingsManager;
this.Name = Resources.launch_profile;
this.Icon = new IconInfo(iconPath);
@@ -62,6 +64,17 @@ internal sealed partial class LaunchProfileCommand : InvokableCommand
// _context.API.ShowMsg(name, message, string.Empty);
Logger.LogError($"Failed to open Windows Terminal: {ex.Message}");
}
try
{
_appSettingsManager.Current.AddRecentlyUsedProfile(id, profile);
_appSettingsManager.Save();
}
catch (Exception ex)
{
// We don't want to fail the whole operation if we can't save the recently used profile
Logger.LogError($"Failed to save recently used profile: {ex.Message}");
}
}
#pragma warning restore IDE0059, CS0168

View File

@@ -4,7 +4,9 @@
#nullable enable
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers;
@@ -15,10 +17,30 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers;
/// </summary>
public sealed class AppSettings
{
private const int MaxRecentProfilesCount = 64;
/// <summary>
/// Gets or sets the last selected channel identifier for the Windows Terminal extension.
/// Empty string when no channel has been selected yet.
/// </summary>
[JsonPropertyName("lastSelectedChannel")]
public string LastSelectedChannel { get; set; } = string.Empty;
/// <summary>
/// Gets the list of recently used profile identifiers.
/// </summary>
[JsonPropertyName("recentlyUsedProfiles")]
public List<TerminalProfileKey> RecentlyUsedProfiles { get; init; } = [];
public void AddRecentlyUsedProfile(string appId, string profileName)
{
var key = new TerminalProfileKey(appId, profileName);
RecentlyUsedProfiles.Remove(key);
RecentlyUsedProfiles.Insert(0, key);
if (RecentlyUsedProfiles.Count > MaxRecentProfilesCount)
{
RecentlyUsedProfiles.RemoveRange(MaxRecentProfilesCount, RecentlyUsedProfiles.Count - MaxRecentProfilesCount);
}
}
}

View File

@@ -10,6 +10,4 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers;
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(AppSettings))]
internal sealed partial class AppSettingsJsonContext : JsonSerializerContext
{
}
internal sealed partial class AppSettingsJsonContext : JsonSerializerContext;

View File

@@ -12,8 +12,6 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers;
#nullable enable
public sealed class AppSettingsManager
{
private const string FileName = "appsettings.json";
@@ -42,7 +40,7 @@ public sealed class AppSettingsManager
if (File.Exists(_filePath))
{
var json = File.ReadAllText(_filePath);
var loaded = JsonSerializer.Deserialize(json, AppSettingsJsonContext.Default.AppSettings);
var loaded = JsonSerializer.Deserialize(json, AppSettingsJsonContext.Default.AppSettings!);
if (loaded is not null)
{
Current = loaded;
@@ -60,7 +58,7 @@ public sealed class AppSettingsManager
{
try
{
var json = JsonSerializer.Serialize(Current, AppSettingsJsonContext.Default.AppSettings);
var json = JsonSerializer.Serialize(Current, AppSettingsJsonContext.Default.AppSettings!);
File.WriteAllText(_filePath, json);
}
catch (Exception ex)

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.
#nullable enable
namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers;
public enum ProfileSortOrder
{
Default = 0,
Alphabetical = 1,
MostRecentlyUsed = 2,
}

View File

@@ -40,6 +40,16 @@ public class SettingsManager : JsonSettingsManager
Resources.save_last_selected_channel_description!,
false);
private readonly ChoiceSetSetting _profileSortOrder = new(
Namespaced(nameof(ProfileSortOrder)),
Resources.profile_sort_order!,
Resources.profile_sort_order_description!,
[
new ChoiceSetSetting.Choice(Resources.profile_sort_order_item_default!, ProfileSortOrder.Default.ToString("G")),
new ChoiceSetSetting.Choice(Resources.profile_sort_order_item_alphabetical!, ProfileSortOrder.Alphabetical.ToString("G")),
new ChoiceSetSetting.Choice(Resources.profile_sort_order_item_mru!, ProfileSortOrder.MostRecentlyUsed.ToString("G")),
]);
public bool ShowHiddenProfiles => _showHiddenProfiles.Value;
public bool OpenNewTab => _openNewTab.Value;
@@ -48,6 +58,8 @@ public class SettingsManager : JsonSettingsManager
public bool SaveLastSelectedChannel => _saveLastSelectedChannel.Value;
public ProfileSortOrder ProfileSortOrder => System.Enum.TryParse<ProfileSortOrder>(_profileSortOrder.Value, out var result) ? result : ProfileSortOrder.Default;
private static string SettingsJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
@@ -65,6 +77,7 @@ public class SettingsManager : JsonSettingsManager
Settings.Add(_openNewTab);
Settings.Add(_openQuake);
Settings.Add(_saveLastSelectedChannel);
Settings.Add(_profileSortOrder);
// Load settings from file upon initialization
LoadSettings();

View File

@@ -0,0 +1,8 @@
// 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.
#nullable enable
namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers;
public sealed record TerminalProfileKey(string AppId, string ProfileName);

View File

@@ -58,7 +58,7 @@ public class TerminalQuery : ITerminalQuery
profiles.AddRange(TerminalHelper.ParseSettings(terminal, settingsJson));
}
return profiles.OrderBy(p => p.Name);
return profiles;
}
public IEnumerable<TerminalPackage> GetTerminals()

View File

@@ -51,9 +51,16 @@ internal sealed partial class ProfilesListPage : ListPage, INotifyItemsChanged
Icon = Icons.TerminalIcon;
Name = Resources.profiles_list_page_name;
_terminalSettings = terminalSettings;
_terminalSettings.Settings.SettingsChanged += Settings_SettingsChanged;
_appSettingsManager = appSettingsManager;
}
private void Settings_SettingsChanged(object sender, Settings args)
{
EnsureInitialized();
RaiseItemsChanged();
}
private List<ListItem> Query()
{
EnsureInitialized();
@@ -62,7 +69,27 @@ internal sealed partial class ProfilesListPage : ListPage, INotifyItemsChanged
openNewTab = _terminalSettings.OpenNewTab;
openQuake = _terminalSettings.OpenQuake;
var profiles = _terminalQuery.GetProfiles();
var profiles = _terminalQuery.GetProfiles()!;
switch (_terminalSettings.ProfileSortOrder)
{
case ProfileSortOrder.MostRecentlyUsed:
var mru = _appSettingsManager.Current.RecentlyUsedProfiles ?? [];
profiles = profiles.OrderBy(p =>
{
var key = new TerminalProfileKey(p.Terminal?.AppUserModelId ?? string.Empty, p.Name ?? string.Empty);
var index = mru.IndexOf(key);
return index == -1 ? int.MaxValue : index;
})
.ThenBy(static p => p.Name, StringComparer.CurrentCultureIgnoreCase)
.ToList();
break;
case ProfileSortOrder.Default:
case ProfileSortOrder.Alphabetical:
default:
profiles = profiles.OrderBy(static p => p.Name, StringComparer.CurrentCultureIgnoreCase);
break;
}
if (terminalFilters?.IsAllSelected == false)
{
@@ -78,12 +105,12 @@ internal sealed partial class ProfilesListPage : ListPage, INotifyItemsChanged
continue;
}
result.Add(new ListItem(new LaunchProfileCommand(profile.Terminal.AppUserModelId, profile.Name, profile.Terminal.LogoPath, openNewTab, openQuake))
result.Add(new ListItem(new LaunchProfileCommand(profile.Terminal.AppUserModelId, profile.Name, profile.Terminal.LogoPath, openNewTab, openQuake, _appSettingsManager))
{
Title = profile.Name,
Subtitle = profile.Terminal.DisplayName,
MoreCommands = [
new CommandContextItem(new LaunchProfileAsAdminCommand(profile.Terminal.AppUserModelId, profile.Name, openNewTab, openQuake)),
new CommandContextItem(new LaunchProfileAsAdminCommand(profile.Terminal.AppUserModelId, profile.Name, openNewTab, openQuake, _appSettingsManager)),
],
});
}

View File

@@ -159,6 +159,60 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Profiles order.
/// </summary>
internal static string profile_sort_order {
get {
return ResourceManager.GetString("profile_sort_order", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Profiles order.
/// </summary>
internal static string profile_sort_order_description {
get {
return ResourceManager.GetString("profile_sort_order_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Alphabetical.
/// </summary>
internal static string profile_sort_order_item_alphabetical {
get {
return ResourceManager.GetString("profile_sort_order_item_alphabetical", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to As defined in Terminal.
/// </summary>
internal static string profile_sort_order_item_as_in_terminal {
get {
return ResourceManager.GetString("profile_sort_order_item_as_in_terminal", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Default (alphabetical).
/// </summary>
internal static string profile_sort_order_item_default {
get {
return ResourceManager.GetString("profile_sort_order_item_default", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Most recently used.
/// </summary>
internal static string profile_sort_order_item_mru {
get {
return ResourceManager.GetString("profile_sort_order_item_mru", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Windows Terminal Profiles.
/// </summary>

View File

@@ -173,4 +173,22 @@
<data name="save_last_selected_channel_description" xml:space="preserve">
<value>Remember the last selected channel instead of resetting to All Channels.</value>
</data>
<data name="profile_sort_order" xml:space="preserve">
<value>Profiles order</value>
</data>
<data name="profile_sort_order_description" xml:space="preserve">
<value>Profiles order</value>
</data>
<data name="profile_sort_order_item_default" xml:space="preserve">
<value>Default (alphabetical)</value>
</data>
<data name="profile_sort_order_item_mru" xml:space="preserve">
<value>Most recently used</value>
</data>
<data name="profile_sort_order_item_as_in_terminal" xml:space="preserve">
<value>As defined in Terminal</value>
</data>
<data name="profile_sort_order_item_alphabetical" xml:space="preserve">
<value>Alphabetical</value>
</data>
</root>