Merge remote-tracking branch 'origin/main' into dev/snickler/net10-upgrade

This commit is contained in:
Jeremy Sinclair
2025-09-04 22:20:02 -04:00
188 changed files with 4866 additions and 1417 deletions

View File

@@ -49,6 +49,7 @@ ALPHATYPE
AModifier AModifier
amr amr
ANDSCANS ANDSCANS
animatedvisuals
Animnate Animnate
ANull ANull
AOC AOC
@@ -69,6 +70,7 @@ APPMODEL
APPNAME APPNAME
appref appref
appsettings appsettings
appsfeatures
appwindow appwindow
appwiz appwiz
appxpackage appxpackage
@@ -304,6 +306,7 @@ CXVIRTUALSCREEN
CYSCREEN CYSCREEN
CYSMICON CYSMICON
CYVIRTUALSCREEN CYVIRTUALSCREEN
Czechia
cziplib cziplib
Dac Dac
dacl dacl
@@ -328,6 +331,7 @@ Deact
debugbreak debugbreak
decryptor decryptor
Dedup Dedup
Deduplicator
Deeplink Deeplink
DEFAULTBOOTSTRAPPERINSTALLFOLDER DEFAULTBOOTSTRAPPERINSTALLFOLDER
DEFAULTCOLOR DEFAULTCOLOR
@@ -433,6 +437,7 @@ EDITSHORTCUTS
EDITTEXT EDITTEXT
EFile EFile
ekus ekus
emojis
ENABLEDELAYEDEXPANSION ENABLEDELAYEDEXPANSION
ENABLEDPOPUP ENABLEDPOPUP
ENABLETAB ENABLETAB
@@ -797,6 +802,7 @@ KEYBOARDMANAGEREDITORLIBRARYWRAPPER
keyboardmanagerstate keyboardmanagerstate
keyboardmanagerui keyboardmanagerui
keyboardtester keyboardtester
keycap
KEYEVENTF KEYEVENTF
KEYIMAGE KEYIMAGE
keynum keynum
@@ -1024,8 +1030,6 @@ MYICON
NAMECHANGE NAMECHANGE
namespaceanddescendants namespaceanddescendants
nao nao
Navigatable
NavigatablePage
NCACTIVATE NCACTIVATE
ncc ncc
NCCALCSIZE NCCALCSIZE
@@ -1316,6 +1320,7 @@ PRODUCTVERSION
Progman Progman
programdata programdata
projectname projectname
projitems
PROPERTYKEY PROPERTYKEY
Propset Propset
PROPVARIANT PROPVARIANT
@@ -1395,6 +1400,7 @@ regkey
regroot regroot
regsvr regsvr
REINSTALLMODE REINSTALLMODE
releaseblog
reloadable reloadable
Relogger Relogger
remappings remappings
@@ -1778,10 +1784,13 @@ UACUI
UAL UAL
uap uap
UBR UBR
UBreak
ubrk
UCallback UCallback
ucrt ucrt
ucrtd ucrtd
uefi uefi
UError
uesc uesc
UFlags UFlags
UHash UHash
@@ -1851,6 +1860,7 @@ VFT
vget vget
vgetq vgetq
viewmodels viewmodels
virama
VIRTKEY VIRTKEY
VIRTUALDESK VIRTUALDESK
VISEGRADRELAY VISEGRADRELAY

View File

@@ -260,3 +260,7 @@ Process Process
# ZoomIt menu items with accelerator keys # ZoomIt menu items with accelerator keys
E&xit E&xit
St&yle St&yle
# This matches a relative clause where the relative pronoun "that" is omitted.
# Example: "Gets or sets the window the TitleBar should configure."
\bthe\s+\w+\s+the\b

View File

@@ -0,0 +1,38 @@
name: Manual Batch Issue Deduplication
on:
workflow_dispatch:
inputs:
issue_numbers:
description: "JSON array of issue numbers to deduplicate (e.g. [101,102,103])"
required: true
since:
description: "Only compare against issues created after this date (ISO 8601, e.g. 2019-05-05T00:00:00Z)"
required: false
default: "2019-05-05T00:00:00Z"
label_as_duplicate:
description: "Apply duplicate label if duplicates are found (true/false)"
required: false
default: "true"
permissions:
models: read
issues: write
jobs:
deduplicate:
runs-on: ubuntu-latest
strategy:
matrix:
issue: ${{ fromJson(github.event.inputs.issue_numbers) }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Run GenAI Issue Deduplicator
uses: pelikhan/action-genai-issue-dedup@v0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
github_issue: ${{ matrix.issue }}
label_as_duplicate: ${{ github.event.inputs.label_as_duplicate }}

View File

@@ -23,7 +23,6 @@
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" /> <PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.Markdown" Version="7.1.2" /> <PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.Markdown" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250703-build.2173" /> <PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250703-build.2173" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.TitleBar" Version="0.0.1-build.2206" />
<PackageVersion Include="ControlzEx" Version="6.0.0" /> <PackageVersion Include="ControlzEx" Version="6.0.0" />
<PackageVersion Include="HelixToolkit" Version="2.24.0" /> <PackageVersion Include="HelixToolkit" Version="2.24.0" />
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" /> <PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />

View File

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

199
README.md
View File

@@ -35,19 +35,19 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user. Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
<!-- items that need to be updated release to release --> <!-- 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.94%22 [github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.93%22 [github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.94%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-x64.exe [ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-arm64.exe [ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-x64.exe [ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-arm64.exe [ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-arm64.exe
| Description | Filename | | Description | Filename |
|----------------|----------| |----------------|----------|
| Per user - x64 | [PowerToysUserSetup-0.93.0-x64.exe][ptUserX64] | | Per user - x64 | [PowerToysUserSetup-0.94.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.93.0-arm64.exe][ptUserArm64] | | Per user - ARM64 | [PowerToysUserSetup-0.94.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.93.0-x64.exe][ptMachineX64] | | Machine wide - x64 | [PowerToysSetup-0.94.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.93.0-arm64.exe][ptMachineArm64] | | Machine wide - ARM64 | [PowerToysSetup-0.94.0-arm64.exe][ptMachineArm64] |
This is our preferred method. This is our preferred method.
@@ -93,118 +93,145 @@ For guidance on developing for PowerToys, please read the [developer docs](./doc
Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on. Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
### 0.93 - Aug 2025 Update ### 0.94 - Sep 2025 Update
In this release, we focused on new features, stability, optimization improvements, and automation. In this release, we focused on new features, stability, optimization improvements, and automation.
For an in-depth look at the latest changes, visit the [release blog](https://aka.ms/powertoys-releaseblog).
**✨Highlights** **✨Highlights**
- PowerToys settings debuts a modern, card-based dashboard with clearer descriptions and faster navigation for a streamlined user experience. - PowerToys Settings added a Settings search with fuzzy matching, suggestions, a results page, and UX polish to make finding options faster.
- Command Palette had over 99 issues resolved, including bringing back Clipboard History, adding context menu shortcuts, pinning favorite apps, and supporting history in Run. - A comprehensive hotkey conflict detection system was introduced in Settings to surface and help resolve conflicting shortcuts. Note that the default hotkey settings (Win+Ctrl+Shift+T, Win+Ctrl+V, Win+Ctrl+T, Win+Shift+T) may overlap with existing Windows system shortcuts. This is expected. You can resolve the conflict by assigning different hotkeys.
- Command Palette reduced its startup memory usage by ~15%, load time by ~40%, built-in extensions loading time by ~70%, and installation size by ~55%—all due to using the full Ahead-of-Time (AOT) compilation mode in Windows App SDK. - Mouse Utilities added a “Gliding cursor” accessibility feature to Mouse Pointer Crosshairs for singlebutton cursor movement and clicking. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
- Peek now supports instant previews and embedded thumbnails for Binary G-code (.bgcode) 3D printing files, making it easy to inspect models at a glance. Thanks [@pedrolamas](https://github.com/pedrolamas)! - The installer was upgraded to WiX 5 after WiX 3 reached end-of-life; this move improved installer security, reliability, and community support.
- Mouse Utilities introduces a new spotlight highlighting mode that dims the screen and draws attention to your cursor, perfect for presentations. - Tons of bug fixes and improvements for Command Palette, including visual updates and new support for filters on ListPages (handy for extension developers).
- Test coverage improvements for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename — ensuring better reliability and quality, with over 600 new unit tests (mostly for Command Palette) and doubled UI automation coverage. - Hosts Editor now has a “No leading spaces” option so active host entries can start at column 0 even if others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
- Context menu registration was moved from the installer to runtime to avoid loading disabled modules (runtime registrations).
- Quick Accent now supports Maltese, and frequently used accents appear first (and are remembered across sessions). Thanks [@rovercoder](https://github.com/rovercoder)! [@davidegiacometti](https://github.com/davidegiacometti)!
### Always On Top
- Fixed the border hover cursor so it shows the arrow instead of the wait cursor. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
### Command Palette ### Command Palette
- Ensured screen readers are notified when the selected item in the list changes for better accessibility. - Applied single-click activation only to pointer input; keyboard always activates immediately. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed command title changes not being properly notified to screen readers. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Let context menus open at the cursor by removing window-bound constraints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made icon controls excluded from keyboard navigation by default for better accessibility. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Made error messages clearer with timestamps, HRESULTs, and full details for easier diagnosis. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved UI design with better text sizing and alignment. - Prevented crashes and improved robustness when updating providers without commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed keyboard shortcuts to work better in text boxes and context menus. - Ensured the Settings window reliably comes to the front when opened. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added right-click context menus with critical command styling and separators. - Replaced the Clipboard History icon with a colorful Fluent icon. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved various context menu issues, improving item selection, handling of long titles, search bar text scaling, initial item behavior, and primary button functionality. - Hardened ContentIcon to avoid duplicate parenting and improve diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed context menu crashes with better type handling. - Standardized null checks using C# pattern matching for safer behavior.
- Fixed "Reload" command to work with both uppercase and lowercase letters. - Improved accessibility by focusing the activation shortcut dialog and making text reachable. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- Added mouse back button support for easier navigation. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Moved the extension SDK to a stable Windows SDK and cleaned up message namespaces.
- Fixed Alt+Left Arrow navigation not working when search box contains text. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Added path shortcuts: ~ to home, and / or \\ to system root, plus UNC support. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Updated back button tooltip to show keyboard shortcut information. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Fixed a race in cancellation handling to avoid InvalidOperationException. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed Command Palette window not appearing properly when activated. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Aligned separator styling with WinUI 3 for consistent visuals. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed Command Palette window staying hidden from taskbar after File Explorer restarts. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Added ARM64 PDBs to the Extensions SDK NuGet for better debugging.
- Fixed window focus not returning to previous app properly. - Added single-select filters to DynamicListPage and updated Windows Services sample.
- Fixed Command Palette window to always appear on top when shown and move to bottom when hidden. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Updated main page placeholder text to better describe what can be searched. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed window hiding to properly work on UI thread. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Removed explicit WinAppSDK/WebView2 dependencies from toolkit and API. Thanks [@rluengen](https://github.com/rluengen)!
- Fixed crashes and improved stability with better synchronization of Command list updates. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Added a local keyboard hook to handle the GoBack key reliably. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved extension disposal with better error handling to prevent crashes. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Propagated alias changes safely and resolved conflicts across view models.
- Improved stability by fixing a UI threading issue when loading more results, preventing possible crashes and ensuring the loading state resets if loading fails. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Allowed providers to override Dispose with a virtual method.
- Enhanced icon loading stability with better exception handling. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Fixed memory leaks by cleaning up removed or cancelled list items.
- Added thread safety to recent commands to prevent crashes. Thanks [@MaoShengelia](https://github.com/MaoShengelia)! - Sorted DateTime extension results by relevance for better usability.
- Fixed acrylic (frosted glass) system backdrop display issues by ensuring proper UI thread handling. Thanks [@jiripolasek](https://github.com/jiripolasek)! - Reduced search text “jiggling” by avoiding redundant change notifications.
- Centralized automation notifications in a UIHelper for better accessibility. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- Preserved Adaptive Card action types during trimming via DynamicDependency.
- Added an acrylic backdrop and refined styling to the context menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Prevented disposed pages and Settings windows from handling stale messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made the extension API easier to evolve without breaking clients.
- Added “evil” sample pages to help reproduce tricky bugs.
- Fixed WinGet trim-safety issues by replacing LINQ with manual iteration.
- Cancelled stale list fetches to avoid older results overwriting newer ones in CmdPal.
### Command Palette extensions ### Command Palette extensions
- Added settings to each provider to control which fallback commands are enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)! for fixing a regression in this feature. - Improved empty states and ranking logic for multiple extensions. Thanks [@htcfreek](https://github.com/htcfreek)!
- Added sample code showing how Command Palette extensions can track when their pages are loaded or unloaded. [Check it out here](./src/modules/cmdpal/ext/SamplePagesExtension/OnLoadPage.cs). - Added app icons to the All Apps "Run" context command when available.
- Fixed *Calculator* to accept regular spaces in numbers that use space separators. Thanks [@PesBandi](https://github.com/PesBandi)! - Restored missing builtin icons by standardizing extension dependencies.
- Added a new setting to *Calculator* to make "Copy" the primary button (replacing “Save”) and enable "Close on Enter", streamlining the workflow. Thanks [@PesBandi](https://github.com/PesBandi)! - Unblocked local deployment by adding WinAppSDK to two sample extensions.
- Improved *Apps* indexing error handling and removed obsolete code. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Prevented apps from showing in search when the *Apps* extension is disabled. Thanks [@jiripolasek](https://github.com/jiripolasek)! ### Hosts File Editor
- Added ability to pin/unpin *Apps* using Ctrl+P shortcut.
- Added keyboard shortcuts to the *Apps* context menu items for faster access. - Added a "No leading spaces" option so active hosts entries can start at column 0 even when others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
- Added all file context menu options to the *Apps* items context menu, making all file actions available there for better functionality.
- Streamlined All *Apps* extension settings by removing redundant descriptions, making the UI clearer. ### Image Resizer
- Added command history to the *Run* page for easier access to previous commands.
- Fixed directory path handling in *Run* fallback for better file navigation. - Fixed Image Resizer localization by installing satellite resources under the WinUI 3 apps culture path.
- Fixed URL fallback item hiding properly in *Web Search* extension when search query becomes invalid. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added proper empty state message for *Web Search* extension when no results found. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added fallback command to *Windows Settings* extension for better search results.
- Re-enabled *Clipboard History* feature with proper window handling.
- Improved *Add Bookmark* extension to automatically detect file, folder, or URL types without manual input.
- Updated terminology from "Kill process" to "End task" in *Window Walker* for consistency with Windows.
- Fixed minor grammar error in SamplePagesExtension code comments. Thanks [@purofle](https://github.com/purofle)!
### Mouse Utilities ### Mouse Utilities
- Added a new spotlight highlighting mode that creates a large transparent circle around your cursor with a backdrop effect, providing an alternative to the traditional circle highlight. Perfect for presentations where you want to focus attention on a specific area while dimming the rest of the screen. - Introduced "Gliding cursor" to control the pointer and click with a single hotkey for better accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
### Mouse Without Borders
- Blocked Easy Mouse from switching machines during fullscreen apps, with an allow-list for exceptions. Thanks [@dot-tb](https://github.com/dot-tb)!
### Peek ### Peek
- Added preview and thumbnail support for Binary G-code (.bgcode) files used in 3D printing. You can now see embedded thumbnails and preview these compressed 3D printing files directly in Peek and File Explorer. Thanks [@pedrolamas](https://github.com/pedrolamas)! - Added Visual Studio shared project file types to XML preview and fixed bgcode handler registration. Thanks [@rezanid](https://github.com/rezanid)!
- Fixes bgcode preview handler registration and events for reliable previews. Thanks [@pedrolamas](https://github.com/pedrolamas)!
### PowerRename
- Changed the Explorer accelerator key to PowErRename to avoid clashing with the New menu. Thanks [@aaron-ni](https://github.com/aaron-ni)!
### Quick Accent ### Quick Accent
- Added Vietnamese language support to Quick Accent, mappings for Vietnamese vowels (a, e, i, o, u, y) and the letter d. Thanks [@octastylos-pseudodipteros](https://github.com/octastylos-pseudodipteros)! - Remembered character usage across sessions so frequently used accents appear first. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Added Maltese language support with specific characters and the Euro symbol. Thanks [@rovercoder](https://github.com/rovercoder)!
- Reduced GPU usage issues by making the window Topmost only when the picker is visible. Thanks [@daverayment](https://github.com/daverayment)!
### Settings ### Settings
- Completely redesigned the Settings dashboard with a modern card-based layout featuring organized sections for quick actions and shortcuts overview, replacing the old module list. - Added telemetry to track usage of the new shortcut conflict detection workflow.
- Rewrote setting descriptions to be more concise and follow Windows writing style guidelines, making them easier to understand. - Moved the shutdown action from the title bar to a footer menu item with confirmation. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Improved formatting and readability of release notes in the "What's New" section with better typography and spacing. - Implemented comprehensive hotkey conflict detection with a dedicated resolution dialog.
- Added missing deep link support for various settings pages (Peek, Quick Accent, PowerToys Run, etc.) so you can jump directly to specific settings. - Added branded visuals for Office and Copilot keys in the KeyVisual control.
- Resolved an issue where the settings page header would drift away from its position when resizing the settings window. - Introduced Settings search with fuzzy matching and navigation to specific controls.
- Resolved a settings crash related to incompatible property names in ZoomIt configuration. - Corrected Spanish localization so product names like Awake remain in English across Settings and OOBE.
- Simplified the Advanced Paste description in Settings for quicker reading and consistent capitalization. Thanks [@OldUser101](https://github.com/OldUser101)!
- Localized conflict messages in the conflict window and dialog.
### Installer
- Upgraded the installer to WiX 5 with silent "Files in Use" handling for smoother winget installs.
- Switched Win10 context menu modules to runtime registration and added cleanup on uninstall to avoid stale entries.
### Documentation ### Documentation
- Added detailed step-by-step instructions for first-time developers building the Command Palette module, including prerequisites and Visual Studio setup guidance. Thanks [@chatasweetie](https://github.com/chatasweetie)! - Adds docs for building the installer locally and testing winget installs.
- **Fixed Broken SDK Link**: Corrected a broken markdown link in the Command Palette SDK README that was pointing to an incorrect directory path. Thanks [@ChrisGuzak](https://github.com/ChrisGuzak)! - Fixed a broken style guide link in developer documentation. Thanks [@denizmaral](https://github.com/denizmaral)!
- Added documentation for the "Open With Cursor" plugin that enables opening Visual Studio and VS Code recent files using Cursor AI. Thanks [@VictorNoxx](https://github.com/VictorNoxx)!
- Added documentation for two new community plugins - Hotkeys plugin for creating custom keyboard shortcuts, and RandomGen plugin for generating random data like passwords, colors, and placeholder text. Thanks [@ruslanlap](https://github.com/ruslanlap)!
### Development ### Development
- Updated .NET libraries to 9.0.8 for performance and security. Thanks [@snickler](https://github.com/snickler)! - Excluded test and coverage DLLs from BinSkim scans to cut false positives and speed up security analysis.
- Updated the spell check system to version 0.0.25 with better GitHub integration and SARIF reporting, plus fixed numerous spelling errors throughout the codebase including property names and documentation. Thanks [@jsoref](https://github.com/jsoref)! - Simplified NOTICE maintenance by removing version numbers and filtering out Microsoft/System packages.
- Cleaned up spelling check configuration to eliminate false positives and excessive noise that was appearing in every pull request, making the development process smoother. - Improved NuGet dependency validation to prevent package downgrades and catch issues during restore.
- Replaced NuGet feed with Azure Artifacts for better package management. - Updated UTF.Unknown to a modern version to improve compatibility without breaking changes. Thanks [@304NotModified](https://github.com/304NotModified)!
- Implemented configurable UI test pipeline that can use pre-built official releases instead of building everything from scratch, reducing test execution time from 2+ hours. - Refreshed package catalog in CI before installing dependencies to prevent Linux workflow failures.
- Replaced brittle pixel-by-pixel image comparison with perceptual hash (pHash) technology that's more robust to minor rendering differences - no more test failures due to anti-aliasing or compression artifacts. - Refactored CmdPal tests with dependency injection and added coverage for queries and settings.
- Reduced CI/fuzzing/UI test timeouts from 4 hours to 90 minutes, dramatically improving developer feedback loops and preventing long waits when builds get stuck. - Added unit tests to verify Close on Enter swaps Copy/Save as expected. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
- Standardized test project naming across the entire codebase and improved pipeline result identification by adding platform/install mode context to test run titles. Thanks [@khmyznikov](https://github.com/khmyznikov)! - Added accessibility IDs to CmdPal UI for stable UI tests.
- Added comprehensive UI test suites for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename - ensuring better reliability and quality. - Rewrote system command tests with a new test base and cleaner patterns.
- Enhanced UI test automation with command-line argument support, better session management, and improved element location methods using pattern matching to avoid failures from minor differences in exact matches. - Added unit tests for WebSearch and Shell extensions with mockable settings.
- Added unit tests and abstractions for Apps and Bookmarks extensions.
- Cleans up AIgenerated tests; adds meaningful query tests across extensions.
- Removed the obsolete debug dialog from Settings for a smoother developer loop.
### What is being planned over the next few releases ### What is being planned over the next few releases
For [v0.94][github-next-release-work], we'll work on the items below: For [v0.95][github-next-release-work], we'll work on the items below:
- Continued Command Palette polish - Continued Command Palette polish
- Working on Shortcut Guide v2 (Thanks [@noraa-junker](https://github.com/noraa-junker)!) - Working on Shortcut Guide v2 (Thanks [@noraa-junker](https://github.com/noraa-junker)!)
- Working on upgrading the installer to WiX 5
- Working on shortcut conflict detection
- Working on setting search
- Upgrading Keyboard Manager's editor UI - Upgrading Keyboard Manager's editor UI
- UI tweaking utility with day/night theme switcher
- DSC v3 support for top utilities
- New UI automation tests - New UI automation tests
- Stability, bug fixes - Stability, bug fixes

View File

@@ -76,6 +76,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
1. Windows 10 April 2018 Update (version 1803) or newer 1. Windows 10 April 2018 Update (version 1803) or newer
1. Visual Studio Community/Professional/Enterprise 2022 17.4 or newer 1. Visual Studio Community/Professional/Enterprise 2022 17.4 or newer
1. A local clone of the PowerToys repository 1. A local clone of the PowerToys repository
1. Enable long paths in Windows (see [Enable Long Paths](https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation#enabling-long-paths-in-windows-10-version-1607-and-later) for details)
### Install Visual Studio dependencies ### Install Visual Studio dependencies

View File

@@ -32,17 +32,8 @@ namespace ManagedCommon
/// <param name="isLocalLow">If the process using Logger is a low-privilege process.</param> /// <param name="isLocalLow">If the process using Logger is a low-privilege process.</param>
public static void InitializeLogger(string applicationLogPath, bool isLocalLow = false) public static void InitializeLogger(string applicationLogPath, bool isLocalLow = false)
{ {
string basePath; string versionedPath = LogDirectoryPath(applicationLogPath, isLocalLow);
if (isLocalLow) string basePath = Path.GetDirectoryName(versionedPath);
{
basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath;
}
else
{
basePath = Constants.AppDataPath() + applicationLogPath;
}
string versionedPath = Path.Combine(basePath, Version);
if (!Directory.Exists(versionedPath)) if (!Directory.Exists(versionedPath))
{ {
@@ -59,6 +50,22 @@ namespace ManagedCommon
Task.Run(() => DeleteOldVersionLogFolders(basePath, versionedPath)); Task.Run(() => DeleteOldVersionLogFolders(basePath, versionedPath));
} }
public static string LogDirectoryPath(string applicationLogPath, bool isLocalLow = false)
{
string basePath;
if (isLocalLow)
{
basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath;
}
else
{
basePath = Constants.AppDataPath() + applicationLogPath;
}
string versionedPath = Path.Combine(basePath, Version);
return versionedPath;
}
/// <summary> /// <summary>
/// Deletes old version log folders, keeping only the current version's folder. /// Deletes old version log folders, keeping only the current version's folder.
/// </summary> /// </summary>
@@ -115,13 +122,13 @@ namespace ManagedCommon
{ {
var exMessage = var exMessage =
message + Environment.NewLine + message + Environment.NewLine +
ex.GetType() + ": " + ex.Message + Environment.NewLine; ex.GetType() + " (" + ex.HResult + "): " + ex.Message + Environment.NewLine;
if (ex.InnerException != null) if (ex.InnerException != null)
{ {
exMessage += exMessage +=
"Inner exception: " + Environment.NewLine + "Inner exception: " + Environment.NewLine +
ex.InnerException.GetType() + ": " + ex.InnerException.Message + Environment.NewLine; ex.InnerException.GetType() + " (" + ex.HResult + "): " + ex.InnerException.Message + Environment.NewLine;
} }
exMessage += exMessage +=

View File

@@ -20,27 +20,14 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid <TitleBar x:Name="titleBar">
x:Name="titleBar" <!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
Height="32" <TitleBar.LeftHeader>
ColumnSpacing="16"> <ImageIcon
<Grid.ColumnDefinitions> Height="16"
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" /> Margin="16,0,0,0"
<ColumnDefinition x:Name="IconColumn" Width="Auto" /> Source="/Assets/EnvironmentVariables/EnvironmentVariables.ico" />
<ColumnDefinition x:Name="TitleColumn" Width="Auto" /> </TitleBar.LeftHeader>
<ColumnDefinition x:Name="RightPaddingColumn" Width="0" /> </TitleBar>
</Grid.ColumnDefinitions>
<Image
Grid.Column="1"
Width="16"
Height="16"
VerticalAlignment="Center"
Source="../Assets/EnvironmentVariables/EnvironmentVariables.ico" />
<TextBlock
x:Name="AppTitleTextBlock"
Grid.Column="2"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" />
</Grid>
</Grid> </Grid>
</winuiex:WindowEx> </winuiex:WindowEx>

View File

@@ -4,22 +4,19 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using EnvironmentVariables.Win32; using EnvironmentVariables.Win32;
using EnvironmentVariablesUILib; using EnvironmentVariablesUILib;
using EnvironmentVariablesUILib.Helpers; using EnvironmentVariablesUILib.Helpers;
using EnvironmentVariablesUILib.ViewModels; using EnvironmentVariablesUILib.ViewModels;
using ManagedCommon; using ManagedCommon;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using WinUIEx; using WinUIEx;
namespace EnvironmentVariables namespace EnvironmentVariables
{ {
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : WindowEx public sealed partial class MainWindow : WindowEx
{ {
private EnvironmentVariablesMainPage MainPage { get; } private EnvironmentVariablesMainPage MainPage { get; }
@@ -34,8 +31,9 @@ namespace EnvironmentVariables
AppWindow.SetIcon("Assets/EnvironmentVariables/EnvironmentVariables.ico"); AppWindow.SetIcon("Assets/EnvironmentVariables/EnvironmentVariables.ico");
var loader = ResourceLoaderInstance.ResourceLoader; var loader = ResourceLoaderInstance.ResourceLoader;
var title = App.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle"); var title = App.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
Title = title; Title = title;
AppTitleTextBlock.Text = title; titleBar.Title = title;
var handle = this.GetWindowHandle(); var handle = this.GetWindowHandle();
RegisterWindow(handle); RegisterWindow(handle);

View File

@@ -19,6 +19,26 @@
class FileLocksmithModule : public PowertoyModuleIface class FileLocksmithModule : public PowertoyModuleIface
{ {
private:
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::EnsureRegistered();
Logger::info(L"File Locksmith context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::Unregister();
Logger::info(L"File Locksmith context menu unregistered");
#endif
}
}
public: public:
FileLocksmithModule() FileLocksmithModule()
{ {
@@ -88,21 +108,16 @@ public:
package::RegisterSparsePackage(path, packageUri); package::RegisterSparsePackage(path, packageUri);
} }
} }
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::EnsureRegistered();
#endif
m_enabled = true; m_enabled = true;
UpdateRegistration(m_enabled);
} }
virtual void disable() override virtual void disable() override
{ {
Logger::info(L"File Locksmith disabled"); Logger::info(L"File Locksmith disabled");
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::Unregister();
Logger::info(L"File Locksmith context menu unregistered (Win10)");
#endif
m_enabled = false; m_enabled = false;
UpdateRegistration(m_enabled);
} }
virtual bool is_enabled() override virtual bool is_enabled() override
@@ -135,6 +150,7 @@ private:
{ {
m_enabled = FileLocksmithSettingsInstance().GetEnabled(); m_enabled = FileLocksmithSettingsInstance().GetEnabled();
m_extended_only = FileLocksmithSettingsInstance().GetShowInExtendedContextMenu(); m_extended_only = FileLocksmithSettingsInstance().GetShowInExtendedContextMenu();
UpdateRegistration(m_enabled);
Trace::EnableFileLocksmith(m_enabled); Trace::EnableFileLocksmith(m_enabled);
} }

View File

@@ -20,30 +20,15 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid <TitleBar x:Name="titleBar">
x:Name="AppTitleBar" <!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
Height="32" <TitleBar.LeftHeader>
ColumnSpacing="16"> <ImageIcon
<Grid.ColumnDefinitions> Height="16"
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" /> Margin="16,0,0,0"
<ColumnDefinition x:Name="IconColumn" Width="Auto" /> Source="/Assets/FileLocksmith/Icon.ico" />
<ColumnDefinition x:Name="TitleColumn" Width="Auto" /> </TitleBar.LeftHeader>
<ColumnDefinition x:Name="RightDragColumn" Width="*" /> </TitleBar>
<ColumnDefinition x:Name="RightPaddingColumn" Width="0" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="1"
Width="16"
Height="16"
VerticalAlignment="Center"
Source="../Assets/FileLocksmith/Icon.ico" />
<TextBlock
x:Name="AppTitleTextBlock"
Grid.Column="2"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" />
</Grid>
<views:MainPage x:Name="mainPage" Grid.Row="1" /> <views:MainPage x:Name="mainPage" Grid.Row="1" />
</Grid> </Grid>
</winuiex:WindowEx> </winuiex:WindowEx>

View File

@@ -18,30 +18,16 @@ namespace FileLocksmithUI
{ {
InitializeComponent(); InitializeComponent();
mainPage.ViewModel.IsElevated = isElevated; mainPage.ViewModel.IsElevated = isElevated;
SetTitleBar(titleBar);
ExtendsContentIntoTitleBar = true; ExtendsContentIntoTitleBar = true;
SetTitleBar(AppTitleBar); AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
Activated += MainWindow_Activated;
AppWindow.SetIcon("Assets/FileLocksmith/Icon.ico"); AppWindow.SetIcon("Assets/FileLocksmith/Icon.ico");
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(this.GetWindowHandle()); WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(this.GetWindowHandle());
var loader = ResourceLoaderInstance.ResourceLoader; var loader = ResourceLoaderInstance.ResourceLoader;
var title = isElevated ? loader.GetString("AppAdminTitle") : loader.GetString("AppTitle"); var title = isElevated ? loader.GetString("AppAdminTitle") : loader.GetString("AppTitle");
Title = title; Title = title;
AppTitleTextBlock.Text = title; titleBar.Title = title;
}
private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
AppTitleTextBlock.Foreground =
(SolidColorBrush)App.Current.Resources["WindowCaptionForegroundDisabled"];
}
else
{
AppTitleTextBlock.Foreground =
(SolidColorBrush)App.Current.Resources["WindowCaptionForeground"];
}
} }
public void Dispose() public void Dispose()

View File

@@ -190,7 +190,7 @@
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</ContentDialog> </ContentDialog>
<ContentDialog x:Name="ProcessFilesListDialog" x:Uid="ProcessFilesListDialog"> <ContentDialog x:Name="ProcessFilesListDialog" x:Uid="ProcessFilesListDialog">
<ScrollViewer Padding="15" HorizontalScrollBarVisibility="Auto"> <ScrollViewer Padding="16" HorizontalScrollBarVisibility="Auto">
<TextBlock <TextBlock
x:Name="ProcessFilesListDialogTextBlock" x:Name="ProcessFilesListDialogTextBlock"
x:Uid="ProcessFilesListDialogTextBlock" x:Uid="ProcessFilesListDialogTextBlock"

View File

@@ -20,27 +20,14 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid <TitleBar x:Name="titleBar">
x:Name="titleBar" <!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
Height="32" <TitleBar.LeftHeader>
ColumnSpacing="16"> <ImageIcon
<Grid.ColumnDefinitions> Height="16"
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" /> Margin="16,0,0,0"
<ColumnDefinition x:Name="IconColumn" Width="Auto" /> Source="/Assets/Hosts/Hosts.ico" />
<ColumnDefinition x:Name="TitleColumn" Width="Auto" /> </TitleBar.LeftHeader>
<ColumnDefinition x:Name="RightPaddingColumn" Width="0" /> </TitleBar>
</Grid.ColumnDefinitions>
<Image
Grid.Column="1"
Width="16"
Height="16"
VerticalAlignment="Center"
Source="../Assets/Hosts/Hosts.ico" />
<TextBlock
x:Name="AppTitleTextBlock"
Grid.Column="2"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" />
</Grid>
</Grid> </Grid>
</winuiex:WindowEx> </winuiex:WindowEx>

View File

@@ -9,19 +9,15 @@ using HostsUILib.Helpers;
using HostsUILib.Views; using HostsUILib.Views;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Telemetry; using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Microsoft.Windows.ApplicationModel.Resources; using Microsoft.Windows.ApplicationModel.Resources;
using WinUIEx; using WinUIEx;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Hosts namespace Hosts
{ {
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : WindowEx public sealed partial class MainWindow : WindowEx
{ {
private HostsMainPage MainPage { get; } private HostsMainPage MainPage { get; }
@@ -38,31 +34,18 @@ namespace Hosts
var title = Host.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle"); var title = Host.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
Title = title; Title = title;
AppTitleTextBlock.Text = title; titleBar.Title = title;
var handle = this.GetWindowHandle(); var handle = this.GetWindowHandle();
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(handle); WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(handle);
WindowHelpers.BringToForeground(handle); WindowHelpers.BringToForeground(handle);
Activated += MainWindow_Activated;
MainPage = Host.GetService<HostsMainPage>(); MainPage = Host.GetService<HostsMainPage>();
PowerToysTelemetry.Log.WriteEvent(new HostEditorStartFinishEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }); PowerToysTelemetry.Log.WriteEvent(new HostEditorStartFinishEvent() { TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
} }
private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
AppTitleTextBlock.Foreground = (SolidColorBrush)App.Current.Resources["WindowCaptionForegroundDisabled"];
}
else
{
AppTitleTextBlock.Foreground = (SolidColorBrush)App.Current.Resources["WindowCaptionForeground"];
}
}
private void Grid_Loaded(object sender, RoutedEventArgs e) private void Grid_Loaded(object sender, RoutedEventArgs e)
{ {
MainGrid.Children.Add(MainPage); MainGrid.Children.Add(MainPage);

View File

@@ -31,7 +31,11 @@ struct CommonState
Measurement::Unit units = Measurement::Unit::Pixel; Measurement::Unit units = Measurement::Unit::Pixel;
POINT cursorPosSystemSpace = {}; // updated atomically #pragma warning(push)
#pragma warning(disable : 4324)
alignas(8) POINT cursorPosSystemSpace = {}; // updated atomically
#pragma warning(pop)
std::atomic_bool closeOnOtherMonitors = false; std::atomic_bool closeOnOtherMonitors = false;
float GetPhysicalPx2MmRatio(HWND window) const float GetPhysicalPx2MmRatio(HWND window) const

View File

@@ -21,6 +21,26 @@
// Note: Settings are managed via Settings and UI Settings // Note: Settings are managed via Settings and UI Settings
class NewModule : public PowertoyModuleIface class NewModule : public PowertoyModuleIface
{ {
private:
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
Logger::info(L"New+ context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::Unregister();
Logger::info(L"New+ context menu unregistered");
#endif
}
}
public: public:
NewModule() NewModule()
{ {
@@ -98,14 +118,9 @@ public:
{ {
newplus::utilities::register_msix_package(); newplus::utilities::register_msix_package();
} }
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
#endif
}
powertoy_new_enabled = true; powertoy_new_enabled = true;
UpdateRegistration(powertoy_new_enabled);
} }
virtual void disable() override virtual void disable() override
@@ -150,19 +165,14 @@ private:
{ {
Trace::EventToggleOnOff(false); Trace::EventToggleOnOff(false);
} }
if (!package::IsWin11OrGreater())
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::Unregister();
Logger::info(L"New+ context menu unregistered (Win10)");
#endif
}
powertoy_new_enabled = false; powertoy_new_enabled = false;
UpdateRegistration(powertoy_new_enabled);
} }
void init_settings() void init_settings()
{ {
powertoy_new_enabled = NewSettingsInstance().GetEnabled(); powertoy_new_enabled = NewSettingsInstance().GetEnabled();
UpdateRegistration(powertoy_new_enabled);
} }
}; };

View File

@@ -49,7 +49,9 @@ namespace Awake.Core
private static DateTimeOffset ExpireAt { get; set; } private static DateTimeOffset ExpireAt { get; set; }
private static readonly CompositeFormat AwakeMinute = CompositeFormat.Parse(Resources.AWAKE_MINUTE);
private static readonly CompositeFormat AwakeMinutes = CompositeFormat.Parse(Resources.AWAKE_MINUTES); private static readonly CompositeFormat AwakeMinutes = CompositeFormat.Parse(Resources.AWAKE_MINUTES);
private static readonly CompositeFormat AwakeHour = CompositeFormat.Parse(Resources.AWAKE_HOUR);
private static readonly CompositeFormat AwakeHours = CompositeFormat.Parse(Resources.AWAKE_HOURS); private static readonly CompositeFormat AwakeHours = CompositeFormat.Parse(Resources.AWAKE_HOURS);
private static readonly BlockingCollection<ExecutionState> _stateQueue; private static readonly BlockingCollection<ExecutionState> _stateQueue;
private static CancellationTokenSource _tokenSource; private static CancellationTokenSource _tokenSource;
@@ -451,7 +453,7 @@ namespace Awake.Core
Dictionary<string, uint> optionsList = new() Dictionary<string, uint> optionsList = new()
{ {
{ string.Format(CultureInfo.InvariantCulture, AwakeMinutes, 30), 1800 }, { string.Format(CultureInfo.InvariantCulture, AwakeMinutes, 30), 1800 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 1), 3600 }, { string.Format(CultureInfo.InvariantCulture, AwakeHour, 1), 3600 },
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 2), 7200 }, { string.Format(CultureInfo.InvariantCulture, AwakeHours, 2), 7200 },
}; };
return optionsList; return optionsList;

View File

@@ -159,6 +159,15 @@ namespace Awake.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to {0} hour.
/// </summary>
internal static string AWAKE_HOUR {
get {
return ResourceManager.GetString("AWAKE_HOUR", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to {0} hours. /// Looks up a localized string similar to {0} hours.
/// </summary> /// </summary>
@@ -240,6 +249,15 @@ namespace Awake.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to {0} minute.
/// </summary>
internal static string AWAKE_MINUTE {
get {
return ResourceManager.GetString("AWAKE_MINUTE", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to {0} minutes. /// Looks up a localized string similar to {0} minutes.
/// </summary> /// </summary>

View File

@@ -123,6 +123,10 @@
<data name="AWAKE_EXIT" xml:space="preserve"> <data name="AWAKE_EXIT" xml:space="preserve">
<value>Exit</value> <value>Exit</value>
</data> </data>
<data name="AWAKE_HOUR" xml:space="preserve">
<value>{0} hour</value>
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
</data>
<data name="AWAKE_HOURS" xml:space="preserve"> <data name="AWAKE_HOURS" xml:space="preserve">
<value>{0} hours</value> <value>{0} hours</value>
<comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment> <comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
@@ -142,6 +146,10 @@
<value>Keep awake until expiration date and time</value> <value>Keep awake until expiration date and time</value>
<comment>Keep the system awake until expiration date and time</comment> <comment>Keep the system awake until expiration date and time</comment>
</data> </data>
<data name="AWAKE_MINUTE" xml:space="preserve">
<value>{0} minute</value>
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
</data>
<data name="AWAKE_MINUTES" xml:space="preserve"> <data name="AWAKE_MINUTES" xml:space="preserve">
<value>{0} minutes</value> <value>{0} minutes</value>
<comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment> <comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>

View File

@@ -0,0 +1,155 @@
// 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.CodeAnalysis;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Common.Commands;
public sealed partial class ConfirmableCommand : InvokableCommand
{
private readonly IInvokableCommand? _command;
public Func<bool>? IsConfirmationRequired { get; init; }
public required string ConfirmationTitle { get; init; }
public required string ConfirmationMessage { get; init; }
public required IInvokableCommand Command
{
get => _command!;
init
{
if (_command is INotifyPropChanged oldNotifier)
{
oldNotifier.PropChanged -= InnerCommand_PropChanged;
}
_command = value;
if (_command is INotifyPropChanged notifier)
{
notifier.PropChanged += InnerCommand_PropChanged;
}
OnPropertyChanged(nameof(Name));
OnPropertyChanged(nameof(Id));
OnPropertyChanged(nameof(Icon));
}
}
public override string Name
{
get => (_command as Command)?.Name ?? base.Name;
set
{
if (_command is Command cmd)
{
cmd.Name = value;
}
else
{
base.Name = value;
}
}
}
public override string Id
{
get => (_command as Command)?.Id ?? base.Id;
set
{
var previous = Id;
if (_command is Command cmd)
{
cmd.Id = value;
}
else
{
base.Id = value;
}
if (previous != Id)
{
OnPropertyChanged(nameof(Id));
}
}
}
public override IconInfo Icon
{
get => (_command as Command)?.Icon ?? base.Icon;
set
{
if (_command is Command cmd)
{
cmd.Icon = value;
}
else
{
base.Icon = value;
}
}
}
public ConfirmableCommand()
{
// Allow init-only construction
}
[SetsRequiredMembers]
public ConfirmableCommand(IInvokableCommand command, string confirmationTitle, string confirmationMessage, Func<bool>? isConfirmationRequired = null)
{
ArgumentNullException.ThrowIfNull(command);
ArgumentException.ThrowIfNullOrWhiteSpace(confirmationMessage);
ArgumentNullException.ThrowIfNull(confirmationMessage);
IsConfirmationRequired = isConfirmationRequired;
ConfirmationTitle = confirmationTitle;
ConfirmationMessage = confirmationMessage;
Command = command;
}
private void InnerCommand_PropChanged(object sender, IPropChangedEventArgs args)
{
var property = args.PropertyName;
if (string.IsNullOrEmpty(property) || property == nameof(Name))
{
OnPropertyChanged(nameof(Name));
}
if (string.IsNullOrEmpty(property) || property == nameof(Id))
{
OnPropertyChanged(nameof(Id));
}
if (string.IsNullOrEmpty(property) || property == nameof(Icon))
{
OnPropertyChanged(nameof(Icon));
}
}
public override ICommandResult Invoke()
{
var showConfirmationDialog = IsConfirmationRequired?.Invoke() ?? true;
if (showConfirmationDialog)
{
return CommandResult.Confirm(new ConfirmationArgs
{
Title = ConfirmationTitle,
Description = ConfirmationMessage,
PrimaryCommand = Command,
IsPrimaryCommandCritical = true,
});
}
else
{
return Command.Invoke(this) ?? CommandResult.Dismiss();
}
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.Common.Helpers;
/// <summary>
/// Well-known key chords used in the Command Palette and extensions.
/// </summary>
/// <remarks>
/// Assigned key chords should not conflict with system or application shortcuts.
/// However, the key chords in this class are not guaranteed to be unique and may conflict
/// with each other, especially when commands appear together in the same menu.
/// </remarks>
public static class WellKnownKeyChords
{
/// <summary>
/// Gets the well-known key chord for opening the file location. Shortcut: Ctrl+Shift+E.
/// </summary>
public static KeyChord OpenFileLocation { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.E);
/// <summary>
/// Gets the well-known key chord for copying the file path. Shortcut: Ctrl+Shift+C.
/// </summary>
public static KeyChord CopyFilePath { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.C);
/// <summary>
/// Gets the well-known key chord for opening the current location in a console. Shortcut: Ctrl+Shift+R.
/// </summary>
public static KeyChord OpenInConsole { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.R);
/// <summary>
/// Gets the well-known key chord for running the selected item as administrator. Shortcut: Ctrl+Shift+Enter.
/// </summary>
public static KeyChord RunAsAdministrator { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.Enter);
/// <summary>
/// Gets the well-known key chord for running the selected item as a different user. Shortcut: Ctrl+Shift+U.
/// </summary>
public static KeyChord RunAsDifferentUser { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.U);
/// <summary>
/// Gets the well-known key chord for toggling the pin state. Shortcut: Ctrl+P.
/// </summary>
public static KeyChord TogglePin { get; } = KeyChordHelpers.FromModifiers(ctrl: true, vkey: (int)VirtualKey.P);
}

View File

@@ -160,7 +160,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
Initialized |= InitializedState.Initialized; Initialized |= InitializedState.Initialized;
} }
public void SlowInitializeProperties() public virtual void SlowInitializeProperties()
{ {
if (IsSelectedInitialized) if (IsSelectedInitialized)
{ {

View File

@@ -47,9 +47,21 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
UpdateTags(li.Tags); UpdateTags(li.Tags);
TextToSuggest = li.TextToSuggest;
Section = li.Section ?? string.Empty; Section = li.Section ?? string.Empty;
var extensionDetails = li.Details;
UpdateProperty(nameof(Section));
}
public override void SlowInitializeProperties()
{
base.SlowInitializeProperties();
var model = Model.Unsafe;
if (model is null)
{
return;
}
var extensionDetails = model.Details;
if (extensionDetails is not null) if (extensionDetails is not null)
{ {
Details = new(extensionDetails, PageContext); Details = new(extensionDetails, PageContext);
@@ -58,8 +70,8 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
UpdateProperty(nameof(HasDetails)); UpdateProperty(nameof(HasDetails));
} }
TextToSuggest = model.TextToSuggest;
UpdateProperty(nameof(TextToSuggest)); UpdateProperty(nameof(TextToSuggest));
UpdateProperty(nameof(Section));
} }
protected override void FetchProperty(string propertyName) protected override void FetchProperty(string propertyName)

View File

@@ -3,10 +3,12 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation; using Windows.Foundation;
@@ -39,7 +41,7 @@ public partial class AppStateModel : ObservableObject
{ {
if (string.IsNullOrEmpty(FilePath)) if (string.IsNullOrEmpty(FilePath))
{ {
throw new InvalidOperationException($"You must set a valid {nameof(SettingsModel.FilePath)} before calling {nameof(LoadState)}"); throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(LoadState)}");
} }
if (!File.Exists(FilePath)) if (!File.Exists(FilePath))
@@ -77,43 +79,84 @@ public partial class AppStateModel : ObservableObject
try try
{ {
// Serialize the main dictionary to JSON and save it to the file // Serialize the main dictionary to JSON and save it to the file
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel); var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel!);
// Is it valid JSON? // validate JSON
if (JsonNode.Parse(settingsJson) is JsonObject newSettings) if (JsonNode.Parse(settingsJson) is not JsonObject newSettings)
{ {
// Now, read the existing content from the file Logger.LogError("Failed to parse app state as a JsonObject.");
var oldContent = File.Exists(FilePath) ? File.ReadAllText(FilePath) : "{}"; return;
}
// Is it valid JSON? // read previous settings
if (JsonNode.Parse(oldContent) is JsonObject savedSettings) if (!TryReadSavedState(out var savedSettings))
{ {
foreach (var item in newSettings) savedSettings = new JsonObject();
{ }
savedSettings[item.Key] = item.Value?.DeepClone();
}
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options); // merge new settings into old ones
File.WriteAllText(FilePath, serialized); foreach (var item in newSettings)
{
savedSettings[item.Key] = item.Value?.DeepClone();
}
// TODO: Instead of just raising the event here, we should var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel!.Options);
// have a file change watcher on the settings file, and File.WriteAllText(FilePath, serialized);
// reload the settings then
model.StateChanged?.Invoke(model, null); // TODO: Instead of just raising the event here, we should
} // have a file change watcher on the settings file, and
else // reload the settings then
{ model.StateChanged?.Invoke(model, null);
Debug.WriteLine("Failed to parse settings file as JsonObject."); }
} catch (Exception ex)
{
Logger.LogError($"Failed to save application state to {FilePath}:", ex);
}
}
private static bool TryReadSavedState([NotNullWhen(true)] out JsonObject? savedSettings)
{
savedSettings = null;
// read existing content from the file
string oldContent;
try
{
if (File.Exists(FilePath))
{
oldContent = File.ReadAllText(FilePath);
} }
else else
{ {
Debug.WriteLine("Failed to parse settings file as JsonObject."); // file doesn't exist (might not have been created yet), so consider this a success
// and return empty settings
savedSettings = new JsonObject();
return true;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.WriteLine(ex.ToString()); Logger.LogWarning($"Failed to read app state file {FilePath}:\n{ex}");
return false;
}
// detect empty file, just for sake of logging
if (string.IsNullOrWhiteSpace(oldContent))
{
Logger.LogInfo($"App state file is empty: {FilePath}");
return false;
}
// is it valid JSON?
try
{
savedSettings = JsonNode.Parse(oldContent) as JsonObject;
return savedSettings != null;
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to parse app state from {FilePath}:\n{ex}");
return false;
} }
} }

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Commands; using Microsoft.CmdPal.UI.ViewModels.Commands;
using Microsoft.CmdPal.UI.ViewModels.Properties; using Microsoft.CmdPal.UI.ViewModels.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -19,6 +20,10 @@ internal sealed partial class FallbackLogItem : FallbackCommandItem
Title = string.Empty; Title = string.Empty;
_logMessagesPage.Name = string.Empty; _logMessagesPage.Name = string.Empty;
Subtitle = Properties.Resources.builtin_log_subtitle; Subtitle = Properties.Resources.builtin_log_subtitle;
var logPath = Logger.LogDirectoryPath("\\CmdPal\\Logs\\");
var openLogCommand = new OpenFileCommand(logPath) { Name = Resources.builtin_log_folder_command_name };
MoreCommands = [new CommandContextItem(openLogCommand)];
} }
public override void UpdateQuery(string query) public override void UpdateQuery(string query)

View File

@@ -27,7 +27,9 @@ public partial class MainListPage : DynamicListPage,
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly TopLevelCommandManager _tlcManager; private readonly TopLevelCommandManager _tlcManager;
private IEnumerable<IListItem>? _filteredItems; private IEnumerable<Scored<IListItem>>? _filteredItems;
private IEnumerable<Scored<IListItem>>? _filteredApps;
private IEnumerable<IListItem>? _allApps;
private bool _includeApps; private bool _includeApps;
private bool _filteredItemsIncludesApps; private bool _filteredItemsIncludesApps;
@@ -83,7 +85,7 @@ public partial class MainListPage : DynamicListPage,
} }
else else
{ {
RaiseItemsChanged(_tlcManager.TopLevelCommands.Count); RaiseItemsChanged();
} }
} }
@@ -148,7 +150,13 @@ public partial class MainListPage : DynamicListPage,
{ {
lock (_tlcManager.TopLevelCommands) lock (_tlcManager.TopLevelCommands)
{ {
return _filteredItems?.ToArray() ?? []; var items = Enumerable.Empty<Scored<IListItem>>()
.Concat(_filteredItems is not null ? _filteredItems : [])
.Concat(_filteredApps is not null ? _filteredApps : [])
.OrderByDescending(o => o.Score)
.Select(s => s.Item)
.ToArray();
return items;
} }
} }
} }
@@ -167,6 +175,8 @@ public partial class MainListPage : DynamicListPage,
{ {
_filteredItemsIncludesApps = _includeApps; _filteredItemsIncludesApps = _includeApps;
_filteredItems = null; _filteredItems = null;
_filteredApps = null;
_allApps = null;
} }
} }
@@ -184,6 +194,8 @@ public partial class MainListPage : DynamicListPage,
{ {
_filteredItemsIncludesApps = _includeApps; _filteredItemsIncludesApps = _includeApps;
_filteredItems = null; _filteredItems = null;
_filteredApps = null;
_allApps = null;
RaiseItemsChanged(commands.Count); RaiseItemsChanged(commands.Count);
return; return;
} }
@@ -193,35 +205,49 @@ public partial class MainListPage : DynamicListPage,
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase)) if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
{ {
_filteredItems = null; _filteredItems = null;
_filteredApps = null;
_allApps = null;
} }
// If the internal state has changed, reset _filteredItems to reset the list. // If the internal state has changed, reset _filteredItems to reset the list.
if (_filteredItemsIncludesApps != _includeApps) if (_filteredItemsIncludesApps != _includeApps)
{ {
_filteredItems = null; _filteredItems = null;
_filteredApps = null;
_allApps = null;
} }
var newFilteredItems = _filteredItems?.Select(s => s.Item);
// If we don't have any previous filter results to work with, start // If we don't have any previous filter results to work with, start
// with a list of all our commands & apps. // with a list of all our commands & apps.
if (_filteredItems is null) if (newFilteredItems is null && _filteredApps is null)
{ {
_filteredItems = commands; newFilteredItems = commands;
_filteredItemsIncludesApps = _includeApps; _filteredItemsIncludesApps = _includeApps;
if (_includeApps) if (_includeApps)
{ {
IEnumerable<IListItem> apps = AllAppsCommandProvider.Page.GetItems(); _allApps = AllAppsCommandProvider.Page.GetItems();
var appIds = apps.Select(app => app.Command.Id).ToArray();
// Remove any top level pinned apps and use the apps from AllAppsCommandProvider.Page.GetItems()
// since they contain details.
_filteredItems = _filteredItems.Where(item => item.Command is not AppCommand);
_filteredItems = _filteredItems.Concat(apps);
} }
} }
// Produce a list of everything that matches the current filter. // Produce a list of everything that matches the current filter.
_filteredItems = ListHelpers.FilterList<IListItem>(_filteredItems, SearchText, ScoreTopLevelItem); _filteredItems = ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, ScoreTopLevelItem);
RaiseItemsChanged(_filteredItems.Count());
// Produce a list of filtered apps with the appropriate limit
if (_allApps is not null)
{
_filteredApps = ListHelpers.FilterListWithScores<IListItem>(_allApps, SearchText, ScoreTopLevelItem);
var appResultLimit = AllAppsCommandProvider.TopLevelResultLimit;
if (appResultLimit >= 0)
{
_filteredApps = _filteredApps.Take(appResultLimit);
}
}
RaiseItemsChanged();
} }
} }

View File

@@ -126,6 +126,10 @@ public class ExtensionWrapper : IExtensionWrapper
// We'll just return out nothing. // We'll just return out nothing.
return; return;
} }
else if (hr.Value != 0)
{
Logger.LogError($"Failed to find {ExtensionDisplayName}: {hr.Value}");
}
// Marshal.ThrowExceptionForHR(hr); // Marshal.ThrowExceptionForHR(hr);
_extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr); _extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr);

View File

@@ -285,6 +285,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to View log folder.
/// </summary>
public static string builtin_log_folder_command_name {
get {
return ResourceManager.GetString("builtin_log_folder_command_name", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to View log. /// Looks up a localized string similar to View log.
/// </summary> /// </summary>

View File

@@ -135,6 +135,9 @@
<data name="builtin_log_title" xml:space="preserve"> <data name="builtin_log_title" xml:space="preserve">
<value>View log</value> <value>View log</value>
</data> </data>
<data name="builtin_log_folder_command_name" xml:space="preserve">
<value>View log folder</value>
</data>
<data name="builtin_reload_subtitle" xml:space="preserve"> <data name="builtin_reload_subtitle" xml:space="preserve">
<value>Reload Command Palette extensions</value> <value>Reload Command Palette extensions</value>
</data> </data>

View File

@@ -32,6 +32,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
private string _generatedId = string.Empty; private string _generatedId = string.Empty;
private HotkeySettings? _hotkey; private HotkeySettings? _hotkey;
private IIconInfo? _initialIcon;
private CommandAlias? Alias { get; set; } private CommandAlias? Alias { get; set; }
@@ -57,6 +58,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
public IIconInfo Icon => _commandItemViewModel.Icon; public IIconInfo Icon => _commandItemViewModel.Icon;
public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon;
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe; ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
@@ -205,6 +208,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
{ {
DisplayTitle = fallback.DisplayTitle; DisplayTitle = fallback.DisplayTitle;
} }
UpdateInitialIcon(false);
} }
} }
@@ -221,7 +226,31 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
FetchAliasFromAliasManager(); FetchAliasFromAliasManager();
UpdateHotkey(); UpdateHotkey();
UpdateTags(); UpdateTags();
UpdateInitialIcon();
} }
else if (e.PropertyName == nameof(CommandItem.Icon))
{
UpdateInitialIcon();
}
}
}
private void UpdateInitialIcon(bool raiseNotification = true)
{
if (_initialIcon != null || !_commandItemViewModel.Icon.IsSet)
{
return;
}
_initialIcon = _commandItemViewModel.Icon;
if (raiseNotification)
{
DoOnUiThread(
() =>
{
PropChanged?.Invoke(this, new PropChangedEventArgs(nameof(InitialIcon)));
});
} }
} }

View File

@@ -85,7 +85,7 @@ public partial class App : Application
AppWindow = new MainWindow(); AppWindow = new MainWindow();
var activatedEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs(); var activatedEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs();
((MainWindow)AppWindow).HandleLaunch(activatedEventArgs); ((MainWindow)AppWindow).HandleLaunchNonUI(activatedEventArgs);
} }
/// <summary> /// <summary>

View File

@@ -134,6 +134,15 @@ public sealed partial class CommandBar : UserControl,
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom)); WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
} }
/// <summary>
/// Sets focus to the "More" button after closing the context menu,
/// keeping keyboard navigation intuitive.
/// </summary>
public void FocusMoreCommandsButton()
{
MoreCommandsButton?.Focus(FocusState.Programmatic);
}
private void ContextMenuFlyout_Opened(object sender, object e) private void ContextMenuFlyout_Opened(object sender, object e)
{ {
// We need to wait until our flyout is opened to try and toss focus // We need to wait until our flyout is opened to try and toss focus

View File

@@ -15,6 +15,7 @@
xmlns:ui="using:CommunityToolkit.WinUI" xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels" xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
Background="Transparent" Background="Transparent"
PreviewKeyDown="UserControl_PreviewKeyDown"
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Messages; using Microsoft.CmdPal.UI.Messages;
@@ -115,6 +116,24 @@ public sealed partial class ContextMenu : UserControl,
} }
} }
/// <summary>
/// Handles Escape to close the context menu and return focus to the "More" button.
/// </summary>
private void UserControl_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Escape)
{
// Close the context menu (if not already handled)
WeakReferenceMessenger.Default.Send(new CloseContextMenuMessage());
// Find the parent CommandBar and set focus to MoreCommandsButton
var parent = this.FindParent<CommandBar>();
parent?.FocusMoreCommandsButton();
e.Handled = true;
}
}
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{ {
var prop = e.PropertyName; var prop = e.PropertyName;

View File

@@ -2,8 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.UI.Deferred; using Microsoft.CmdPal.UI.Deferred;
using Microsoft.Terminal.UI;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
@@ -55,6 +57,8 @@ public partial class IconBox : ContentControl
{ {
TabFocusNavigation = KeyboardNavigationMode.Once; TabFocusNavigation = KeyboardNavigationMode.Once;
IsTabStop = false; IsTabStop = false;
HorizontalContentAlignment = HorizontalAlignment.Center;
VerticalContentAlignment = VerticalAlignment.Center;
} }
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -75,6 +79,8 @@ public partial class IconBox : ContentControl
IconSourceElement elem = new() IconSourceElement elem = new()
{ {
IconSource = fontIco, IconSource = fontIco,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
}; };
@this.Content = elem; @this.Content = elem;
break; break;
@@ -98,14 +104,20 @@ public partial class IconBox : ContentControl
else else
{ {
// TODO GH #239 switch back when using the new MD text block // TODO GH #239 switch back when using the new MD text block
// Switching back to EnqueueAsync has broken icons in tags (they don't show)
// _ = @this._queue.EnqueueAsync(() => // _ = @this._queue.EnqueueAsync(() =>
@this._queue.TryEnqueue(new(async () => @this._queue.TryEnqueue(async void () =>
{ {
var requestedTheme = @this.ActualTheme; try
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
if (@this.SourceRequested is not null)
{ {
if (@this.SourceRequested is null)
{
return;
}
var requestedTheme = @this.ActualTheme;
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
await @this.SourceRequested.InvokeAsync(@this, eventArgs); await @this.SourceRequested.InvokeAsync(@this, eventArgs);
// After the await: // After the await:
@@ -130,37 +142,35 @@ public partial class IconBox : ContentControl
// So, if the icon we get back was a font icon, // So, if the icon we get back was a font icon,
// and the glyph for that icon is NOT in the range of // and the glyph for that icon is NOT in the range of
// Segoe icons, then let's give the icon some extra space // Segoe icons, then let's give the icon some extra space
@this.Padding = new Thickness(0); var iconData = eventArgs.Key switch
IconDataViewModel? iconData = null;
if (eventArgs.Key is IconDataViewModel)
{ {
iconData = eventArgs.Key as IconDataViewModel; IconDataViewModel key => key,
IconInfoViewModel info => requestedTheme == ElementTheme.Light ? info.Light : info.Dark,
_ => null,
};
if (iconData?.Icon is not null && @this.Source is FontIconSource)
{
var iconSize =
!double.IsNaN(@this.Width) ? @this.Width :
!double.IsNaN(@this.Height) ? @this.Height :
@this.ActualWidth > 0 ? @this.ActualWidth :
@this.ActualHeight;
@this.Padding = new Thickness(Math.Round(iconSize * -0.2));
} }
else if (eventArgs.Key is IconInfoViewModel info) else
{ {
iconData = requestedTheme == ElementTheme.Light ? info.Light : info.Dark; @this.Padding = default;
}
if (iconData is not null &&
@this.Source is FontIconSource)
{
if (!string.IsNullOrEmpty(iconData.Icon) && iconData.Icon.Length <= 2)
{
var ch = iconData.Icon[0];
// The range of MDL2 Icons isn't explicitly defined, but
// we're using this based off the table on:
// https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font
var isMDL2Icon = ch is >= '\uE700' and <= '\uF8FF';
if (!isMDL2Icon)
{
@this.Padding = new Thickness(-4);
}
}
} }
} }
})); catch (Exception ex)
{
// Exception from TryEnqueue bypasses the global error handler,
// and crashes the app.
Logger.LogError("Failed to set icon", ex);
}
});
} }
} }
} }

View File

@@ -123,6 +123,9 @@ public sealed partial class MainWindow : WindowEx,
_localKeyboardListener = new LocalKeyboardListener(); _localKeyboardListener = new LocalKeyboardListener();
_localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed; _localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
_localKeyboardListener.Start(); _localKeyboardListener.Start();
// Force window to be created, and then cloaked. This will offset initial animation when the window is shown.
HideWindow();
} }
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e) private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
@@ -233,9 +236,6 @@ public sealed partial class MainWindow : WindowEx,
{ {
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd); var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
// Make sure our HWND is cloaked before any possible window manipulations
Cloak();
// Remember, IsIconic == "minimized", which is entirely different state // Remember, IsIconic == "minimized", which is entirely different state
// from "show/hide" // from "show/hide"
// If we're currently minimized, restore us first, before we reveal // If we're currently minimized, restore us first, before we reveal
@@ -243,6 +243,9 @@ public sealed partial class MainWindow : WindowEx,
// which would remain not visible to the user. // which would remain not visible to the user.
if (PInvoke.IsIconic(hwnd)) if (PInvoke.IsIconic(hwnd))
{ {
// Make sure our HWND is cloaked before any possible window manipulations
Cloak();
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE); PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
} }
@@ -481,8 +484,13 @@ public sealed partial class MainWindow : WindowEx,
} }
} }
public void HandleLaunch(AppActivationArguments? activatedEventArgs) public void HandleLaunchNonUI(AppActivationArguments? activatedEventArgs)
{ {
// LOAD BEARING
// Any reading and processing of the activation arguments must be done
// synchronously in this method, before it returns. The sending instance
// remains blocked until this returns; afterward it may quit, causing
// the activation arguments to be lost.
if (activatedEventArgs is null) if (activatedEventArgs is null)
{ {
Summon(string.Empty); Summon(string.Empty);
@@ -519,9 +527,26 @@ public sealed partial class MainWindow : WindowEx,
} }
catch (COMException ex) catch (COMException ex)
{ {
// https://learn.microsoft.com/en-us/windows/win32/rpc/rpc-return-values
const int RPC_S_SERVER_UNAVAILABLE = -2147023174;
const int RPC_S_CALL_FAILED = 2147023170;
// Accessing properties activatedEventArgs.Kind and activatedEventArgs.Data might cause COMException // Accessing properties activatedEventArgs.Kind and activatedEventArgs.Data might cause COMException
// if the args are not valid or not passed correctly. // if the args are not valid or not passed correctly.
Logger.LogError("COM exception when activating the application", ex); if (ex.HResult is RPC_S_SERVER_UNAVAILABLE or RPC_S_CALL_FAILED)
{
Logger.LogWarning(
$"COM exception (HRESULT {ex.HResult}) when accessing activation arguments. " +
$"This might be due to the calling application not passing them correctly or exiting before we could read them. " +
$"The application will continue running and fall back to showing the Command Palette window.");
}
else
{
Logger.LogError(
$"COM exception (HRESULT {ex.HResult}) when activating the application. " +
$"The application will continue running and fall back to showing the Command Palette window.",
ex);
}
} }
Summon(string.Empty); Summon(string.Empty);
@@ -610,6 +635,20 @@ public sealed partial class MainWindow : WindowEx,
} }
private void HandleSummon(string commandId) private void HandleSummon(string commandId)
{
if (_ignoreHotKeyWhenFullScreen)
{
// If we're in full screen mode, ignore the hotkey
if (WindowHelper.IsWindowFullscreen())
{
return;
}
}
HandleSummonCore(commandId);
}
private void HandleSummonCore(string commandId)
{ {
var isRootHotkey = string.IsNullOrEmpty(commandId); var isRootHotkey = string.IsNullOrEmpty(commandId);
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey)); PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
@@ -634,8 +673,6 @@ public sealed partial class MainWindow : WindowEx,
// so that we can bind hotkeys to individual commands // so that we can bind hotkeys to individual commands
if (!isVisible || !isRootHotkey) if (!isVisible || !isRootHotkey)
{ {
Activate();
Summon(commandId); Summon(commandId);
} }
else if (isRootHotkey) else if (isRootHotkey)
@@ -671,15 +708,6 @@ public sealed partial class MainWindow : WindowEx,
var hotkeyIndex = (int)wParam.Value; var hotkeyIndex = (int)wParam.Value;
if (hotkeyIndex < _hotkeys.Count) if (hotkeyIndex < _hotkeys.Count)
{ {
if (_ignoreHotKeyWhenFullScreen)
{
// If we're in full screen mode, ignore the hotkey
if (WindowHelper.IsWindowFullscreen())
{
return (LRESULT)IntPtr.Zero;
}
}
var hotkey = _hotkeys[hotkeyIndex]; var hotkey = _hotkeys[hotkeyIndex];
HandleSummon(hotkey.CommandId); HandleSummon(hotkey.CommandId);
} }

View File

@@ -107,12 +107,33 @@ internal sealed class Program
{ {
// Do the redirection on another thread, and use a non-blocking // Do the redirection on another thread, and use a non-blocking
// wait method to wait for the redirection to complete. // wait method to wait for the redirection to complete.
var redirectSemaphore = new Semaphore(0, 1); using var redirectSemaphore = new Semaphore(0, 1);
Task.Run(() => var redirectTimeout = TimeSpan.FromSeconds(32);
_ = Task.Run(() =>
{ {
keyInstance.RedirectActivationToAsync(args).AsTask().Wait(); using var cts = new CancellationTokenSource(redirectTimeout);
redirectSemaphore.Release(); try
{
keyInstance.RedirectActivationToAsync(args)
.AsTask(cts.Token)
.GetAwaiter()
.GetResult();
}
catch (OperationCanceledException)
{
Logger.LogError($"Failed to activate existing instance; timed out after {redirectTimeout}.");
}
catch (Exception ex)
{
Logger.LogError("Failed to activate existing instance", ex);
}
finally
{
redirectSemaphore.Release();
}
}); });
_ = PInvoke.CoWaitForMultipleObjects( _ = PInvoke.CoWaitForMultipleObjects(
(uint)CWMO_FLAGS.CWMO_DEFAULT, (uint)CWMO_FLAGS.CWMO_DEFAULT,
PInvoke.INFINITE, PInvoke.INFINITE,
@@ -124,13 +145,14 @@ internal sealed class Program
{ {
// If we already have a form, display the message now. // If we already have a form, display the message now.
// Otherwise, add it to the collection for displaying later. // Otherwise, add it to the collection for displaying later.
if (App.Current is App thisApp) if (App.Current?.AppWindow is MainWindow mainWindow)
{ {
if (thisApp.AppWindow is not null and // LOAD BEARING
MainWindow mainWindow) // This must be synchronous to ensure the method does not return
{ // before the activation is fully handled and the parameters are processed.
uiContext?.Post(_ => mainWindow.HandleLaunch(args), null); // The sending instance remains blocked until this returns; afterward it may quit,
} // causing the activation arguments to be lost.
mainWindow.HandleLaunchNonUI(args);
} }
} }
} }

View File

@@ -125,7 +125,7 @@
Width="20" Width="20"
Height="20" Height="20"
AutomationProperties.AccessibilityView="Raw" AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind Icon, Mode=OneWay}" SourceKey="{x:Bind InitialIcon, Mode=OneWay}"
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" /> SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
</cpcontrols:ContentIcon.Content> </cpcontrols:ContentIcon.Content>
</cpcontrols:ContentIcon> </cpcontrols:ContentIcon>

View File

@@ -24,23 +24,15 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- TO DO: Replace this with WinUI TitleBar once that ships. --> <TitleBar x:Name="TitleBar">
<StackPanel <!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
x:Name="AppTitleBar" <TitleBar.LeftHeader>
Grid.Row="0" <ImageIcon
Height="48" Height="16"
Margin="16,0,0,0" Margin="16,0,0,0"
Orientation="Horizontal"> Source="ms-appx:///Assets/icon.svg" />
<Image </TitleBar.LeftHeader>
Width="16" </TitleBar>
Height="16"
Source="ms-appx:///Assets/icon.svg" />
<TextBlock
x:Uid="CmdPalSettingsHeader"
Margin="12,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" />
</StackPanel>
<NavigationView <NavigationView
x:Name="NavView" x:Name="NavView"
Grid.Row="1" Grid.Row="1"
@@ -77,7 +69,6 @@
x:Name="NavigationBreadcrumbBar" x:Name="NavigationBreadcrumbBar"
Grid.Row="0" Grid.Row="0"
MaxWidth="1000" MaxWidth="1000"
Margin="16,0,0,0"
ItemClicked="NavigationBreadcrumbBar_ItemClicked" ItemClicked="NavigationBreadcrumbBar_ItemClicked"
ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}"> ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}">
<BreadcrumbBar.ItemTemplate> <BreadcrumbBar.ItemTemplate>

View File

@@ -31,8 +31,10 @@ public sealed partial class SettingsWindow : WindowEx,
this.InitializeComponent(); this.InitializeComponent();
this.ExtendsContentIntoTitleBar = true; this.ExtendsContentIntoTitleBar = true;
this.SetIcon(); this.SetIcon();
this.AppWindow.Title = RS_.GetString("SettingsWindowTitle"); var title = RS_.GetString("SettingsWindowTitle");
this.AppWindow.Title = title;
this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall; this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
this.TitleBar.Title = title;
PositionCentered(); PositionCentered();
WeakReferenceMessenger.Default.Register<NavigateToExtensionSettingsMessage>(this); WeakReferenceMessenger.Default.Register<NavigateToExtensionSettingsMessage>(this);

View File

@@ -438,7 +438,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="NavigationPaneClosed" xml:space="preserve"> <data name="NavigationPaneClosed" xml:space="preserve">
<value>Navigation pane closed</value> <value>Navigation pane closed</value>
</data> </data>
<data name="NavigationPageOpened" xml:space="preserve"> <data name="NavigationPaneOpened" xml:space="preserve">
<value>Navigation page opened</value> <value>Navigation page opened</value>
</data> </data>
</root> </root>

View File

@@ -0,0 +1,177 @@
#include "pch.h"
#include "FontIconGlyphClassifier.h"
#include "FontIconGlyphClassifier.g.cpp"
#include <icu.h>
#include <utility>
namespace winrt::Microsoft::Terminal::UI::implementation
{
namespace
{
// Check if the code point is in the Private Use Area range used by Fluent UI icons.
[[nodiscard]] constexpr bool _isFluentIconPua(const UChar32 cp) noexcept
{
static constexpr UChar32 _fluentIconsPrivateUseAreaStart = 0xE700;
static constexpr UChar32 _fluentIconsPrivateUseAreaEnd = 0xF8FF;
return cp >= _fluentIconsPrivateUseAreaStart && cp <= _fluentIconsPrivateUseAreaEnd;
}
// Determine if the given text (as a sequence of UChar code units) is emoji
[[nodiscard]] bool _isEmoji(const UChar* p, const int32_t length) noexcept
{
if (!p || length < 1)
{
return false;
}
// https://www.unicode.org/reports/tr51/#Emoji_Variation_Selector_Notes
constexpr UChar32 vs15CodePoint = 0xFE0E; // Variation Selectors 15: text variation selector
constexpr UChar32 vs16CodePoint = 0xFE0F; // Variation Selectors: 16 emoji variation selector
// Decode the first code point correctly (surrogate-safe)
int32_t i0{ 0 };
UChar32 first{ 0 };
U16_NEXT(p, i0, length, first);
for (int32_t i = 0; i < length;)
{
UChar32 cp{ 0 };
U16_NEXT(p, i, length, cp);
if (cp == vs16CodePoint) { return true; }
if (cp == vs15CodePoint) { return false; }
}
return !U_IS_SURROGATE(first) && u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION);
}
}
bool FontIconGlyphClassifier::IsLikelyToBeEmojiOrSymbolIcon(const hstring& text)
{
if (text.empty())
{
return false;
}
if (text.size() == 1 && !IS_HIGH_SURROGATE(text[0]))
{
// If it's a single code unit, it's definitely either zero or one grapheme clusters.
// If it turns out to be illegal Unicode, we don't really care.
return true;
}
if (text.size() >= 2 && text[0] <= 0x7F && text[1] <= 0x7F)
{
// Two adjacent ASCII characters (as seen in most file paths) aren't a single
// grapheme cluster.
return false;
}
// Use ICU to determine whether text is composed of a single grapheme cluster.
int32_t off{ 0 };
UErrorCode status{ U_ZERO_ERROR };
UBreakIterator* const bi{ ubrk_open(UBRK_CHARACTER,
nullptr,
reinterpret_cast<const UChar*>(text.data()),
static_cast<int>(text.size()),
&status) };
if (bi)
{
if (U_SUCCESS(status))
{
off = ubrk_next(bi);
}
ubrk_close(bi);
}
return std::cmp_equal(off, text.size());
}
FontIconGlyphKind FontIconGlyphClassifier::Classify(hstring const& text) noexcept
{
if (text.empty())
{
return FontIconGlyphKind::None;
}
const size_t textSize{ text.size() };
const auto* buffer{ reinterpret_cast<const UChar*>(text.c_str()) };
// Fast path 1: Single UTF-16 code unit (most common case)
if (textSize == 1)
{
const UChar ch{ buffer[0] };
if (IS_HIGH_SURROGATE(ch))
{
return FontIconGlyphKind::Invalid;
}
if (_isFluentIconPua(ch))
{
return FontIconGlyphKind::FluentSymbol;
}
if (_isEmoji(&ch, 1))
{
return FontIconGlyphKind::Emoji;
}
return FontIconGlyphKind::Other;
}
// Fast path 2: Common file path pattern - two ASCII printable characters
if (textSize >= 2 && buffer[0] <= 0x7F && buffer[1] <= 0x7F)
{
// Definitely multiple graphemes
return FontIconGlyphKind::Invalid;
}
// Expensive path: Use ICU to determine grapheme boundaries
UErrorCode status{ U_ZERO_ERROR };
UBreakIterator* bi{ ubrk_open(UBRK_CHARACTER,
nullptr,
buffer,
static_cast<int32_t>(textSize),
&status) };
if (U_FAILURE(status) || !bi)
{
return FontIconGlyphKind::Invalid;
}
const int32_t start{ ubrk_first(bi) };
const int32_t end{ ubrk_next(bi) }; // end of first grapheme
ubrk_close(bi);
// No graphemes found
if (end == UBRK_DONE || end <= start)
{
return FontIconGlyphKind::None;
}
// If there's more than one grapheme, it's not a valid icon glyph
if (std::cmp_not_equal(end, textSize))
{
return FontIconGlyphKind::Invalid;
}
// Exactly one grapheme: classify
const UChar* grapheme = buffer + start;
const int32_t graphemeLength = end - start;
if (graphemeLength == 1 && _isFluentIconPua(grapheme[0]))
{
return FontIconGlyphKind::FluentSymbol;
}
if (_isEmoji(grapheme, graphemeLength))
{
return FontIconGlyphKind::Emoji;
}
return FontIconGlyphKind::Other;
}
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include "FontIconGlyphClassifier.g.h"
namespace winrt::Microsoft::Terminal::UI::implementation
{
struct FontIconGlyphClassifier
{
[[nodiscard]] static bool IsLikelyToBeEmojiOrSymbolIcon(const winrt::hstring& text);
[[nodiscard]] static FontIconGlyphKind Classify(winrt::hstring const& text) noexcept;
};
}
namespace winrt::Microsoft::Terminal::UI::factory_implementation
{
BASIC_FACTORY(FontIconGlyphClassifier);
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace Microsoft.Terminal.UI
{
/// <summary>
/// Categorizes the type of a single grapheme cluster or input text.
/// Used to determine how the input should be handled or rendered (for example,
/// whether it should be treated as an emoji, an icon from a symbol font, plain text, etc.).
/// </summary>
enum FontIconGlyphKind
{
/// <summary>
/// Input is invalid or contains more than one grapheme cluster and therefore cannot be
/// treated as a single symbol. Typical for multi-character text like file paths
/// or composed strings that include separators.
/// </summary>
Invalid = -1,
/// <summary>
/// No grapheme present (empty string). Indicates absence of a symbol.
/// </summary>
None = 0,
/// <summary>
/// A single emoji grapheme cluster. This may consist of multiple Unicode code
/// points combined into one visible glyph (e.g., emoji with modifiers or ZWJ sequences).
/// </summary>
Emoji = 1,
/// <summary>
/// A single glyph from the Segoe Fluent Icons / MDL2 Assets Private Use Area (PUA),
/// typically in the Unicode range U+E700U+F8FF. These are font-based icons (Fluent/MDL2).
/// </summary>
FluentSymbol = 2,
/// <summary>
/// A single non-emoji grapheme that is not a Fluent/MDL2 PUA symbol.
/// Covers ordinary characters, letters, numbers, or other single glyph symbols.
/// </summary>
Other = 3,
};
/// <summary>
/// Static utility class for text and icon analysis
/// </summary>
static runtimeclass FontIconGlyphClassifier
{
/// <summary>
/// Determines if text represents a single grapheme cluster (emoji/symbol icon).
/// Uses ICU for Unicode boundary detection to distinguish icons from file paths.
/// </summary>
/// <param name="text">Text to analyze</param>
/// <returns>True if single grapheme cluster, false for multi-character text or paths</returns>
static Boolean IsLikelyToBeEmojiOrSymbolIcon(String text);
/// <summary>
/// Classifies the input into a glyph kind suitable for icon or text rendering.
/// </summary>
static FontIconGlyphKind Classify(String text);
};
}

View File

@@ -2,7 +2,7 @@
#include "IconPathConverter.h" #include "IconPathConverter.h"
#include "IconPathConverter.g.cpp" #include "IconPathConverter.g.cpp"
// #include "Utils.h" #include "FontIconGlyphClassifier.h"
#include <Shlobj.h> #include <Shlobj.h>
#include <Shlobj_core.h> #include <Shlobj_core.h>
@@ -110,7 +110,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg")) if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg"))
{ {
typename ImageIconSource<TIconSource>::type iconSource; typename ImageIconSource<TIconSource>::type iconSource;
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri }; winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
iconSource.ImageSource(source); iconSource.ImageSource(source);
return iconSource; return iconSource;
} }
@@ -169,41 +169,46 @@ namespace winrt::Microsoft::Terminal::UI::implementation
// If we fail to set the icon source using the "icon" as a path, // If we fail to set the icon source using the "icon" as a path,
// let's try it as a symbol/emoji. // let's try it as a symbol/emoji.
// if (!iconSource)
// Anything longer than 2 wchar_t's _isn't_ an emoji or symbol, so
// don't do this if it's just an invalid path.
if (!iconSource && iconPath.size() <= 2)
{ {
try try
{ {
typename FontIconSource<TIconSource>::type icon; const auto glyph_kind = FontIconGlyphClassifier::Classify(iconPath);
const auto ch = til::at(iconPath, 0);
// The range of MDL2 Icons isn't explicitly defined, but winrt::hstring family;
// we're using this based off the table on: if (glyph_kind == FontIconGlyphKind::Invalid)
// https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font
const auto isMDL2Icon = ch >= L'\uE700' && ch <= L'\uF8FF';
if (isMDL2Icon)
{ {
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); family = L"Segoe UI";
} }
else if (!fontFamily.empty()) else if (!fontFamily.empty())
{ {
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ fontFamily }); family = fontFamily;
}
else if (glyph_kind == FontIconGlyphKind::FluentSymbol)
{
family = L"Segoe Fluent Icons, Segoe MDL2 Assets";
}
else if (glyph_kind == FontIconGlyphKind::Emoji)
{
// Emoji and other symbols go in the Segoe UI Emoji font.
// Some emojis (e.g. 2⃣) would be rendered as emoji glyphs otherwise.
family = L"Segoe UI Emoji, Segoe UI";
} }
else else
{ {
// Note: you _do_ need to manually set the font here. family = L"Segoe UI";
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe UI" });
} }
typename FontIconSource<TIconSource>::type icon;
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ family });
icon.FontSize(targetSize); icon.FontSize(targetSize);
icon.Glyph(iconPath); icon.Glyph(glyph_kind == FontIconGlyphKind::Invalid ? L"\u25CC" : iconPath);
iconSource = icon; iconSource = icon;
} }
CATCH_LOG(); CATCH_LOG();
} }
} }
if (!iconSource) if (!iconSource)
{ {
// Set the default IconSource to a BitmapIconSource with a null source // Set the default IconSource to a BitmapIconSource with a null source
@@ -326,7 +331,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
} }
static winrt::Microsoft::UI::Xaml::Media::Imaging::SoftwareBitmapSource _getImageIconSourceForBinary(std::wstring_view iconPathWithoutIndex, static winrt::Microsoft::UI::Xaml::Media::Imaging::SoftwareBitmapSource _getImageIconSourceForBinary(std::wstring_view iconPathWithoutIndex,
int index, int index,
int targetSize) int targetSize)
{ {
// Try: // Try:

View File

@@ -159,6 +159,9 @@
<ClInclude Include="ResourceString.h"> <ClInclude Include="ResourceString.h">
<DependentUpon>ResourceString.idl</DependentUpon> <DependentUpon>ResourceString.idl</DependentUpon>
</ClInclude> </ClInclude>
<ClInclude Include="FontIconGlyphClassifier.h">
<DependentUpon>FontIconGlyphClassifier.idl</DependentUpon>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="init.cpp" /> <ClCompile Include="init.cpp" />
@@ -178,6 +181,9 @@
<DependentUpon>ResourceString.idl</DependentUpon> <DependentUpon>ResourceString.idl</DependentUpon>
</ClCompile> </ClCompile>
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" /> <ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
<ClCompile Include="FontIconGlyphClassifier.cpp">
<DependentUpon>FontIconGlyphClassifier.idl</DependentUpon>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Midl Include="Converters.idl" /> <Midl Include="Converters.idl" />
@@ -185,6 +191,7 @@
<Midl Include="RunHistory.idl" /> <Midl Include="RunHistory.idl" />
<Midl Include="IDirectKeyListener.idl" /> <Midl Include="IDirectKeyListener.idl" />
<Midl Include="ResourceString.idl" /> <Midl Include="ResourceString.idl" />
<Midl Include="FontIconGlyphClassifier.idl" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />

View File

@@ -2,10 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests; namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
@@ -13,34 +12,22 @@ public class MockSettingsInterface : ISettingsInterface
{ {
private readonly List<HistoryItem> _historyItems; private readonly List<HistoryItem> _historyItems;
public event EventHandler HistoryChanged;
public bool GlobalIfURI { get; set; } public bool GlobalIfURI { get; set; }
public string ShowHistory { get; set; } public int HistoryItemCount { get; set; }
public MockSettingsInterface(string showHistory = "none", bool globalIfUri = true, List<HistoryItem> mockHistory = null) public IReadOnlyList<HistoryItem> HistoryItems => _historyItems;
public MockSettingsInterface(int historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)
{ {
_historyItems = mockHistory ?? new List<HistoryItem>(); _historyItems = mockHistory ?? new List<HistoryItem>();
GlobalIfURI = globalIfUri; GlobalIfURI = globalIfUri;
ShowHistory = showHistory; HistoryItemCount = historyItemCount;
} }
public List<ListItem> LoadHistory() public void AddHistoryItem(HistoryItem historyItem)
{
var listItems = new List<ListItem>();
foreach (var historyItem in _historyItems)
{
listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this))
{
Title = historyItem.SearchString,
Subtitle = historyItem.Timestamp.ToString("g", System.Globalization.CultureInfo.InvariantCulture),
});
}
listItems.Reverse();
return listItems;
}
public void SaveHistory(HistoryItem historyItem)
{ {
if (historyItem is null) if (historyItem is null)
{ {
@@ -50,19 +37,22 @@ public class MockSettingsInterface : ISettingsInterface
_historyItems.Add(historyItem); _historyItems.Add(historyItem);
// Simulate the same logic as SettingsManager // Simulate the same logic as SettingsManager
if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0) if (HistoryItemCount > 0)
{ {
while (_historyItems.Count > maxHistoryItems) while (_historyItems.Count > HistoryItemCount)
{ {
_historyItems.RemoveAt(0); // Remove the oldest item _historyItems.RemoveAt(0);
} }
} }
HistoryChanged?.Invoke(this, EventArgs.Empty);
} }
// Helper method for testing // Helper method for testing
public void ClearHistory() public void ClearHistory()
{ {
_historyItems.Clear(); _historyItems.Clear();
HistoryChanged?.Invoke(this, EventArgs.Empty);
} }
// Helper method for testing // Helper method for testing

View File

@@ -45,7 +45,7 @@ public class QueryTests : CommandPaletteUnitTestBase
} }
[TestMethod] [TestMethod]
public async Task LoadHistoryReturnsExpectedItems() public async Task HistoryReturnsExpectedItems()
{ {
// Setup // Setup
var mockHistoryItems = new List<HistoryItem> var mockHistoryItems = new List<HistoryItem>
@@ -54,7 +54,7 @@ public class QueryTests : CommandPaletteUnitTestBase
new HistoryItem("another search", DateTime.Parse("2024-01-02 13:00:00", CultureInfo.CurrentCulture)), new HistoryItem("another search", DateTime.Parse("2024-01-02 13:00:00", CultureInfo.CurrentCulture)),
}; };
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "5"); var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var page = new WebSearchListPage(settings); var page = new WebSearchListPage(settings);
@@ -77,7 +77,7 @@ public class QueryTests : CommandPaletteUnitTestBase
} }
[TestMethod] [TestMethod]
public async Task LoadHistoryMoreThanLimitation() public async Task HistoryExceedingLimitReturnsMaxItems()
{ {
// Setup // Setup
var mockHistoryItems = new List<HistoryItem> var mockHistoryItems = new List<HistoryItem>
@@ -89,7 +89,7 @@ public class QueryTests : CommandPaletteUnitTestBase
new HistoryItem("another search4", DateTime.Parse("2024-01-05 13:00:00", CultureInfo.CurrentCulture)), new HistoryItem("another search4", DateTime.Parse("2024-01-05 13:00:00", CultureInfo.CurrentCulture)),
}; };
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "5"); var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var page = new WebSearchListPage(settings); var page = new WebSearchListPage(settings);
@@ -109,7 +109,7 @@ public class QueryTests : CommandPaletteUnitTestBase
} }
[TestMethod] [TestMethod]
public async Task LoadHistoryWithDisableSetting() public async Task HistoryWhenSetToNoneReturnEmptyList()
{ {
// Setup // Setup
var mockHistoryItems = new List<HistoryItem> var mockHistoryItems = new List<HistoryItem>
@@ -122,7 +122,7 @@ public class QueryTests : CommandPaletteUnitTestBase
new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)), new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)),
}; };
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "None"); var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0);
var page = new WebSearchListPage(settings); var page = new WebSearchListPage(settings);

View File

@@ -0,0 +1,48 @@
// 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.Threading.Tasks;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Pages;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
[TestClass]
public class SettingsManagerTests : CommandPaletteUnitTestBase
{
[TestMethod]
public async Task HistoryChangedEventIsRaisedWhenItemIsAdded()
{
// Setup
var settings = new MockSettingsInterface(historyItemCount: 5);
var page = new WebSearchListPage(settings);
var eventRaised = false;
try
{
settings.HistoryChanged += Handler;
// Act
settings.AddHistoryItem(new HistoryItem("test event", DateTime.UtcNow));
await Task.Delay(50);
// Assert
Assert.IsTrue(eventRaised, "Expected HistoryChanged to be raised when saving history.");
}
finally
{
settings.HistoryChanged -= Handler;
page.Dispose();
}
return;
void Handler(object s, EventArgs e) => eventRaised = true;
}
}

View File

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

View File

@@ -3,9 +3,7 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State; using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
@@ -45,6 +43,28 @@ public partial class AllAppsCommandProvider : CommandProvider
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged; PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
} }
public static int TopLevelResultLimit
{
get
{
var limitSetting = AllAppsSettings.Instance.SearchResultLimit;
if (limitSetting is null)
{
return -1;
}
var quantity = -1;
if (int.TryParse(limitSetting, out var result))
{
quantity = result;
}
return quantity;
}
}
public override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()]; public override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()];
public ICommandItem? LookupApp(string displayName) public ICommandItem? LookupApp(string displayName)

View File

@@ -20,6 +20,16 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
private static string Experimental(string propertyName) => $"{_namespace}.experimental.{propertyName}"; private static string Experimental(string propertyName) => $"{_namespace}.experimental.{propertyName}";
private static readonly List<ChoiceSetSetting.Choice> _searchResultLimitChoices =
[
new ChoiceSetSetting.Choice(Resources.limit_none, "-1"),
new ChoiceSetSetting.Choice(Resources.limit_0, "0"),
new ChoiceSetSetting.Choice(Resources.limit_1, "1"),
new ChoiceSetSetting.Choice(Resources.limit_5, "5"),
new ChoiceSetSetting.Choice(Resources.limit_10, "10"),
new ChoiceSetSetting.Choice(Resources.limit_20, "20"),
];
#pragma warning disable SA1401 // Fields should be private #pragma warning disable SA1401 // Fields should be private
internal static AllAppsSettings Instance = new(); internal static AllAppsSettings Instance = new();
#pragma warning restore SA1401 // Fields should be private #pragma warning restore SA1401 // Fields should be private
@@ -42,6 +52,14 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
public bool EnablePathEnvironmentVariableSource => _enablePathEnvironmentVariableSource.Value; public bool EnablePathEnvironmentVariableSource => _enablePathEnvironmentVariableSource.Value;
private readonly ChoiceSetSetting _searchResultLimitSource = new(
Namespaced(nameof(SearchResultLimit)),
Resources.limit_fallback_results_source,
Resources.limit_fallback_results_source_description,
_searchResultLimitChoices);
public string SearchResultLimit => _searchResultLimitSource.Value ?? string.Empty;
private readonly ToggleSetting _enableStartMenuSource = new( private readonly ToggleSetting _enableStartMenuSource = new(
Namespaced(nameof(EnableStartMenuSource)), Namespaced(nameof(EnableStartMenuSource)),
Resources.enable_start_menu_source, Resources.enable_start_menu_source,
@@ -87,6 +105,7 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
Settings.Add(_enableDesktopSource); Settings.Add(_enableDesktopSource);
Settings.Add(_enableRegistrySource); Settings.Add(_enableRegistrySource);
Settings.Add(_enablePathEnvironmentVariableSource); Settings.Add(_enablePathEnvironmentVariableSource);
Settings.Add(_searchResultLimitSource);
// Load settings from file upon initialization // Load settings from file upon initialization
LoadSettings(); LoadSettings();

View File

@@ -133,17 +133,13 @@ internal sealed partial class AppListItem : ListItem
newCommands.Add(new Separator()); newCommands.Add(new Separator());
// 0x50 = P
// Full key chord would be Ctrl+P
var pinKeyChord = KeyChordHelpers.FromModifiers(true, false, false, false, 0x50, 0);
if (isPinned) if (isPinned)
{ {
newCommands.Add( newCommands.Add(
new CommandContextItem( new CommandContextItem(
new UnpinAppCommand(this.AppIdentifier)) new UnpinAppCommand(this.AppIdentifier))
{ {
RequestedShortcut = pinKeyChord, RequestedShortcut = KeyChords.TogglePin,
}); });
} }
else else
@@ -152,7 +148,7 @@ internal sealed partial class AppListItem : ListItem
new CommandContextItem( new CommandContextItem(
new PinAppCommand(this.AppIdentifier)) new PinAppCommand(this.AppIdentifier))
{ {
RequestedShortcut = pinKeyChord, RequestedShortcut = KeyChords.TogglePin,
}); });
} }

View File

@@ -1,47 +0,0 @@
// 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.Text;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class CopyPathCommand : InvokableCommand
{
private readonly string _target;
public CopyPathCommand(string target)
{
Name = Resources.copy_path;
Icon = Icons.CopyIcon;
_target = target;
}
private static readonly CompositeFormat CopyFailedFormat = CompositeFormat.Parse(Resources.copy_failed);
public override CommandResult Invoke()
{
try
{
ClipboardHelper.SetText(_target);
}
catch (Exception ex)
{
Logger.LogError("Copy failed: " + ex.Message);
return CommandResult.ShowToast(
new ToastArgs
{
Message = string.Format(CultureInfo.CurrentCulture, CopyFailedFormat, ex.Message),
Result = CommandResult.KeepOpen(),
});
}
return CommandResult.ShowToast(Resources.copied_to_clipboard);
}
}

View File

@@ -0,0 +1,130 @@
// 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.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Management.Deployment;
namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class UninstallApplicationCommand : InvokableCommand
{
// This is a ms-settings URI that opens the Apps & Features page in Windows Settings.
// It's correct and follows the Microsoft documentation:
// https://learn.microsoft.com/en-us/windows/apps/develop/launch/launch-settings-app#apps
private const string AppsFeaturesUri = "ms-settings:appsfeatures";
private readonly UWPApplication? _uwpTarget;
private readonly Win32Program? _win32Target;
public UninstallApplicationCommand(UWPApplication target)
{
Name = Resources.uninstall_application;
Icon = Icons.UninstallApplicationIcon;
_uwpTarget = target ?? throw new ArgumentNullException(nameof(target));
}
public UninstallApplicationCommand(Win32Program target)
{
Name = Resources.uninstall_application;
Icon = Icons.UninstallApplicationIcon;
_win32Target = target ?? throw new ArgumentNullException(nameof(target));
}
private async Task<CommandResult> UninstallUwpAppAsync(UWPApplication app)
{
if (string.IsNullOrWhiteSpace(app.Package.FullName))
{
Logger.LogError($"Critical error while uninstalling: packageFullName cannot be null or empty.");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
try
{
// Which timeout to use for the uninstallation operation?
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)))
{
var packageManager = new PackageManager();
var result = await packageManager.RemovePackageAsync(app.Package.FullName).AsTask(cts.Token);
if (result.ErrorText is not null && result.ErrorText.Length > 0)
{
Logger.LogError($"Failed to uninstall {app.Package.FullName}: {result.ErrorText}");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
}
// TODO: Update the Search results after uninstalling the app - unsure how to do this yet.
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_successful), app.DisplayName),
Result = CommandResult.GoHome(),
});
}
catch (OperationCanceledException)
{
Logger.LogError($"Timeout exceeded while uninstalling {app.Package.FullName}");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
catch (UnauthorizedAccessException ex)
{
Logger.LogError($"Permission denied to uninstall {app.Package.FullName}. Elevated privileges may be required. Error: {ex.Message}");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
catch (Exception ex)
{
Logger.LogError($"An unexpected error occurred during uninstallation of {app.Package.FullName}: {ex.Message}");
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_failed), app.DisplayName),
Result = CommandResult.KeepOpen(),
});
}
}
public override CommandResult Invoke()
{
if (_uwpTarget is not null)
{
return UninstallUwpAppAsync(_uwpTarget).ConfigureAwait(false).GetAwaiter().GetResult();
}
if (_win32Target is not null)
{
Process.Start(new ProcessStartInfo
{
FileName = AppsFeaturesUri,
UseShellExecute = true,
});
return CommandResult.Dismiss();
}
Logger.LogError("UninstallApplicationCommand invoked with no target.");
return CommandResult.Dismiss();
}
}

View File

@@ -0,0 +1,66 @@
// 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.Text;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class UninstallApplicationConfirmation : InvokableCommand
{
private readonly UWPApplication? _uwpTarget;
private readonly Win32Program? _win32Target;
public UninstallApplicationConfirmation(UWPApplication target)
{
Name = Resources.uninstall_application;
Icon = Icons.UninstallApplicationIcon;
_uwpTarget = target ?? throw new ArgumentNullException(nameof(target));
}
public UninstallApplicationConfirmation(Win32Program target)
{
Name = Resources.uninstall_application;
Icon = Icons.UninstallApplicationIcon;
_win32Target = target ?? throw new ArgumentNullException(nameof(target));
}
public override CommandResult Invoke()
{
UninstallApplicationCommand uninstallCommand;
var applicationTitle = Resources.uninstall_application;
if (_uwpTarget is not null)
{
uninstallCommand = new UninstallApplicationCommand(_uwpTarget);
applicationTitle = _uwpTarget.DisplayName;
}
else if (_win32Target is not null)
{
uninstallCommand = new UninstallApplicationCommand(_win32Target);
applicationTitle = _win32Target.Name;
}
else
{
Logger.LogError("UninstallApplicationCommand invoked with no target.");
return CommandResult.Dismiss();
}
var confirmArgs = new ConfirmationArgs()
{
Title = string.Format(CultureInfo.CurrentCulture, CompositeFormat.Parse(Resources.uninstall_application_confirm_title), applicationTitle),
Description = Resources.uninstall_application_confirm_description,
PrimaryCommand = uninstallCommand,
IsPrimaryCommandCritical = true,
};
return CommandResult.Confirm(confirmArgs);
}
}

View File

@@ -21,4 +21,6 @@ internal sealed class Icons
public static IconInfo UnpinIcon { get; } = new("\uE77A"); // Unpin icon public static IconInfo UnpinIcon { get; } = new("\uE77A"); // Unpin icon
public static IconInfo PinIcon { get; } = new("\uE840"); // Pin icon public static IconInfo PinIcon { get; } = new("\uE840"); // Pin icon
public static IconInfo UninstallApplicationIcon { get; } = new("\uE74D"); // Uninstall icon
} }

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 Microsoft.CmdPal.Common.Helpers;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Ext.Apps;
internal static class KeyChords
{
internal static KeyChord OpenFileLocation { get; } = WellKnownKeyChords.OpenFileLocation;
internal static KeyChord CopyFilePath { get; } = WellKnownKeyChords.CopyFilePath;
internal static KeyChord OpenInConsole { get; } = WellKnownKeyChords.OpenInConsole;
internal static KeyChord RunAsAdministrator { get; } = WellKnownKeyChords.RunAsAdministrator;
internal static KeyChord RunAsDifferentUser { get; } = WellKnownKeyChords.RunAsDifferentUser;
internal static KeyChord TogglePin { get; } = WellKnownKeyChords.TogglePin;
}

View File

@@ -25,6 +25,7 @@
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" /> <ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" /> <ProjectReference Include="..\..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props --> <!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
</ItemGroup> </ItemGroup>

View File

@@ -87,7 +87,7 @@ public class UWPApplication : IUWPApplication
new CommandContextItem( new CommandContextItem(
new RunAsAdminCommand(UniqueIdentifier, string.Empty, true)) new RunAsAdminCommand(UniqueIdentifier, string.Empty, true))
{ {
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Enter), RequestedShortcut = KeyChords.RunAsAdministrator,
}); });
// We don't add context menu to 'run as different user', because UWP applications normally installed per user and not for all users. // We don't add context menu to 'run as different user', because UWP applications normally installed per user and not for all users.
@@ -95,9 +95,9 @@ public class UWPApplication : IUWPApplication
commands.Add( commands.Add(
new CommandContextItem( new CommandContextItem(
new Commands.CopyPathCommand(Location)) new CopyTextCommand(Location) { Name = Resources.copy_path })
{ {
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C), RequestedShortcut = KeyChords.CopyFilePath,
}); });
commands.Add( commands.Add(
@@ -107,16 +107,24 @@ public class UWPApplication : IUWPApplication
Name = Resources.open_containing_folder, Name = Resources.open_containing_folder,
}) })
{ {
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.E), RequestedShortcut = KeyChords.OpenFileLocation,
}); });
commands.Add( commands.Add(
new CommandContextItem( new CommandContextItem(
new OpenInConsoleCommand(Package.Location)) new OpenInConsoleCommand(Package.Location))
{ {
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R), RequestedShortcut = KeyChords.OpenInConsole,
}); });
commands.Add(
new CommandContextItem(
new UninstallApplicationConfirmation(this))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete),
IsCritical = true,
});
return commands; return commands;
} }

View File

@@ -191,34 +191,44 @@ public partial class Win32Program : IProgram
commands.Add(new CommandContextItem( commands.Add(new CommandContextItem(
new RunAsAdminCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory, false)) new RunAsAdminCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory, false))
{ {
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Enter), RequestedShortcut = KeyChords.RunAsAdministrator,
}); });
commands.Add(new CommandContextItem( commands.Add(new CommandContextItem(
new RunAsUserCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory)) new RunAsUserCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory))
{ {
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.U), RequestedShortcut = KeyChords.RunAsDifferentUser,
}); });
} }
commands.Add(new CommandContextItem( commands.Add(new CommandContextItem(
new Commands.CopyPathCommand(FullPath)) new CopyTextCommand(FullPath) { Name = Resources.copy_path })
{ {
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C), RequestedShortcut = KeyChords.CopyFilePath,
}); });
commands.Add(new CommandContextItem( commands.Add(new CommandContextItem(
new OpenPathCommand(ParentDirectory)) new OpenPathCommand(ParentDirectory))
{ {
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.E), RequestedShortcut = KeyChords.OpenFileLocation,
}); });
commands.Add(new CommandContextItem( commands.Add(new CommandContextItem(
new OpenInConsoleCommand(ParentDirectory)) new OpenInConsoleCommand(ParentDirectory))
{ {
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R), RequestedShortcut = KeyChords.OpenInConsole,
}); });
if (AppType == ApplicationType.ShortcutApplication || AppType == ApplicationType.ApprefApplication || AppType == ApplicationType.Win32Application)
{
commands.Add(new CommandContextItem(
new UninstallApplicationConfirmation(this))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete),
IsCritical = true,
});
}
return commands; return commands;
} }

View File

@@ -78,24 +78,6 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to Copied to clipboard!.
/// </summary>
internal static string copied_to_clipboard {
get {
return ResourceManager.GetString("copied_to_clipboard", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy failed ({0}). Please try again..
/// </summary>
internal static string copy_failed {
get {
return ResourceManager.GetString("copy_failed", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Copy path. /// Looks up a localized string similar to Copy path.
/// </summary> /// </summary>
@@ -177,6 +159,78 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to 0.
/// </summary>
internal static string limit_0 {
get {
return ResourceManager.GetString("limit_0", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 1.
/// </summary>
internal static string limit_1 {
get {
return ResourceManager.GetString("limit_1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 10.
/// </summary>
internal static string limit_10 {
get {
return ResourceManager.GetString("limit_10", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 20.
/// </summary>
internal static string limit_20 {
get {
return ResourceManager.GetString("limit_20", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 5.
/// </summary>
internal static string limit_5 {
get {
return ResourceManager.GetString("limit_5", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Limit the number of applications returned from the top level.
/// </summary>
internal static string limit_fallback_results_source {
get {
return ResourceManager.GetString("limit_fallback_results_source", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Limit fallback results to n apps.
/// </summary>
internal static string limit_fallback_results_source_description {
get {
return ResourceManager.GetString("limit_fallback_results_source_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unlimited.
/// </summary>
internal static string limit_none {
get {
return ResourceManager.GetString("limit_none", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Open containing folder. /// Looks up a localized string similar to Open containing folder.
/// </summary> /// </summary>
@@ -276,6 +330,51 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to Uninstall.
/// </summary>
internal static string uninstall_application {
get {
return ResourceManager.GetString("uninstall_application", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This app and its related information will be removed..
/// </summary>
internal static string uninstall_application_confirm_description {
get {
return ResourceManager.GetString("uninstall_application_confirm_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Uninstall &quot;{0}&quot;?.
/// </summary>
internal static string uninstall_application_confirm_title {
get {
return ResourceManager.GetString("uninstall_application_confirm_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Error while uninstalling &apos;{0}&apos;.
/// </summary>
internal static string uninstall_application_failed {
get {
return ResourceManager.GetString("uninstall_application_failed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &apos;{0}&apos; has been successfully uninstalled..
/// </summary>
internal static string uninstall_application_successful {
get {
return ResourceManager.GetString("uninstall_application_successful", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Unpin. /// Looks up a localized string similar to Unpin.
/// </summary> /// </summary>

View File

@@ -172,13 +172,6 @@
<data name="run_as_different_user" xml:space="preserve"> <data name="run_as_different_user" xml:space="preserve">
<value>Run as different user</value> <value>Run as different user</value>
</data> </data>
<data name="copy_failed" xml:space="preserve">
<value>Copy failed ({0}). Please try again.</value>
<comment>{0} is the error message</comment>
</data>
<data name="copied_to_clipboard" xml:space="preserve">
<value>Copied to clipboard!</value>
</data>
<data name="enable_start_menu_source" xml:space="preserve"> <data name="enable_start_menu_source" xml:space="preserve">
<value>Include apps found in the Start Menu</value> <value>Include apps found in the Start Menu</value>
</data> </data>
@@ -205,4 +198,43 @@
<data name="unpin_app" xml:space="preserve"> <data name="unpin_app" xml:space="preserve">
<value>Unpin</value> <value>Unpin</value>
</data> </data>
</root> <data name="uninstall_application" xml:space="preserve">
<value>Uninstall</value>
</data>
<data name="uninstall_application_successful" xml:space="preserve">
<value>'{0}' has been successfully uninstalled.</value>
</data>
<data name="uninstall_application_failed" xml:space="preserve">
<value>Error while uninstalling '{0}'</value>
</data>
<data name="uninstall_application_confirm_description" xml:space="preserve">
<value>This app and its related information will be removed.</value>
</data>
<data name="uninstall_application_confirm_title" xml:space="preserve">
<value>Uninstall "{0}"?</value>
</data>
<data name="limit_1" xml:space="preserve">
<value>1</value>
</data>
<data name="limit_5" xml:space="preserve">
<value>5</value>
</data>
<data name="limit_10" xml:space="preserve">
<value>10</value>
</data>
<data name="limit_20" xml:space="preserve">
<value>20</value>
</data>
<data name="limit_fallback_results_source" xml:space="preserve">
<value>Limit the number of applications returned from the top level</value>
</data>
<data name="limit_fallback_results_source_description" xml:space="preserve">
<value>Limit fallback results to n apps</value>
</data>
<data name="limit_0" xml:space="preserve">
<value>0</value>
</data>
<data name="limit_none" xml:space="preserve">
<value>Unlimited</value>
</data>
</root>

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CmdPal.Ext.ClipboardHistory.Pages; using Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -11,19 +12,25 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory;
public partial class ClipboardHistoryCommandsProvider : CommandProvider public partial class ClipboardHistoryCommandsProvider : CommandProvider
{ {
private readonly ListItem _clipboardHistoryListItem; private readonly ListItem _clipboardHistoryListItem;
private readonly SettingsManager _settingsManager = new();
public ClipboardHistoryCommandsProvider() public ClipboardHistoryCommandsProvider()
{ {
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage()) _clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
{ {
Title = Properties.Resources.list_item_title, Title = Properties.Resources.list_item_title,
Subtitle = Properties.Resources.list_item_subtitle, Subtitle = Properties.Resources.list_item_subtitle,
Icon = Icons.ClipboardListIcon, Icon = Icons.ClipboardListIcon,
MoreCommands = [
new CommandContextItem(_settingsManager.Settings.SettingsPage),
],
}; };
DisplayName = Properties.Resources.provider_display_name; DisplayName = Properties.Resources.provider_display_name;
Icon = Icons.ClipboardListIcon; Icon = Icons.ClipboardListIcon;
Id = "Windows.ClipboardHistory"; Id = "Windows.ClipboardHistory";
Settings = _settingsManager.Settings;
} }
public override IListItem[] TopLevelCommands() public override IListItem[] TopLevelCommands()

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.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
internal sealed partial class DeleteItemCommand : InvokableCommand
{
private readonly ClipboardItem _clipboardItem;
internal DeleteItemCommand(ClipboardItem clipboardItem)
{
_clipboardItem = clipboardItem;
Name = Properties.Resources.delete_command_name;
Icon = Icons.DeleteIcon;
}
public override CommandResult Invoke()
{
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
return CommandResult.ShowToast(new ToastArgs
{
Message = Properties.Resources.delete_toast_text,
Result = CommandResult.KeepOpen(),
});
}
}

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Common.Messages; using Microsoft.CmdPal.Common.Messages;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models; using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer; using Windows.ApplicationModel.DataTransfer;
@@ -14,11 +15,13 @@ internal sealed partial class PasteCommand : InvokableCommand
{ {
private readonly ClipboardItem _clipboardItem; private readonly ClipboardItem _clipboardItem;
private readonly ClipboardFormat _clipboardFormat; private readonly ClipboardFormat _clipboardFormat;
private readonly ISettingOptions _settings;
internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat) internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat, ISettingOptions settings)
{ {
_clipboardItem = clipboardItem; _clipboardItem = clipboardItem;
_clipboardFormat = clipboardFormat; _clipboardFormat = clipboardFormat;
_settings = settings;
Name = Properties.Resources.paste_command_name; Name = Properties.Resources.paste_command_name;
Icon = Icons.PasteIcon; Icon = Icons.PasteIcon;
} }
@@ -39,7 +42,11 @@ internal sealed partial class PasteCommand : InvokableCommand
ClipboardHelper.SendPasteKeyCombination(); ClipboardHelper.SendPasteKeyCombination();
Clipboard.DeleteItemFromHistory(_clipboardItem.Item); if (!_settings.KeepAfterPaste)
{
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
}
return CommandResult.ShowToast(Properties.Resources.paste_toast_text); return CommandResult.ShowToast(Properties.Resources.paste_toast_text);
} }
} }

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;
public interface ISettingOptions
{
bool KeepAfterPaste { get; }
bool DeleteFromHistoryRequiresConfirmation { get; }
}

View File

@@ -0,0 +1,54 @@
// 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.IO;
using Microsoft.CmdPal.Ext.ClipboardHistory.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
internal sealed class SettingsManager : JsonSettingsManager, ISettingOptions
{
private const string Namespace = "clipboardHistory";
private static string Namespaced(string propertyName) => $"{Namespace}.{propertyName}";
private readonly ToggleSetting _keepAfterPaste = new(
Namespaced(nameof(KeepAfterPaste)),
Resources.settings_keep_after_paste_title!,
Resources.settings_keep_after_paste_description!,
false);
private readonly ToggleSetting _confirmDelete = new(
Namespaced(nameof(DeleteFromHistoryRequiresConfirmation)),
Resources.settings_confirm_delete_title!,
Resources.settings_confirm_delete_description!,
true);
public bool KeepAfterPaste => _keepAfterPaste.Value;
public bool DeleteFromHistoryRequiresConfirmation => _confirmDelete.Value;
private static string SettingsJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
}
public SettingsManager()
{
FilePath = SettingsJsonPath();
Settings.Add(_keepAfterPaste);
Settings.Add(_confirmDelete);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (_, _) => SaveSettings();
}
}

View File

@@ -14,5 +14,7 @@ internal sealed class Icons
internal static IconInfo PasteIcon { get; } = new("\uE77F"); internal static IconInfo PasteIcon { get; } = new("\uE77F");
internal static IconInfo DeleteIcon { get; } = new("\uE74D");
internal static IconInfo ClipboardListIcon { get; } = IconHelpers.FromRelativePath("Assets\\ClipboardHistory.svg"); internal static IconInfo ClipboardListIcon { get; } = IconHelpers.FromRelativePath("Assets\\ClipboardHistory.svg");
} }

View File

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

View File

@@ -7,7 +7,9 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Microsoft.CmdPal.Common.Commands;
using Microsoft.CmdPal.Ext.ClipboardHistory.Commands; using Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer; using Windows.ApplicationModel.DataTransfer;
using Windows.Storage.Streams; using Windows.Storage.Streams;
@@ -16,9 +18,11 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
public class ClipboardItem public class ClipboardItem
{ {
public string? Content { get; set; } public string? Content { get; init; }
public required ClipboardHistoryItem Item { get; set; } public required ClipboardHistoryItem Item { get; init; }
public required ISettingOptions Settings { get; init; }
public DateTimeOffset Timestamp => Item?.Timestamp ?? DateTimeOffset.MinValue; public DateTimeOffset Timestamp => Item?.Timestamp ?? DateTimeOffset.MinValue;
@@ -87,6 +91,19 @@ public class ClipboardItem
Data = new DetailsLink(Item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)), 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) if (IsImage)
{ {
var iconData = new IconData(ImageData); var iconData = new IconData(ImageData);
@@ -103,7 +120,9 @@ public class ClipboardItem
Metadata = metadata.ToArray(), Metadata = metadata.ToArray(),
}, },
MoreCommands = [ MoreCommands = [
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image)) new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image, Settings)),
new Separator(),
deleteContextMenuItem,
], ],
}; };
} }
@@ -126,8 +145,10 @@ public class ClipboardItem
Metadata = metadata.ToArray(), Metadata = metadata.ToArray(),
}, },
MoreCommands = [ MoreCommands = [
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text)), new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text, Settings)),
], new Separator(),
deleteContextMenuItem,
],
}; };
} }
else else

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
using Microsoft.CmdPal.Ext.ClipboardHistory.Models; using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -17,11 +18,15 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
internal sealed partial class ClipboardHistoryListPage : ListPage internal sealed partial class ClipboardHistoryListPage : ListPage
{ {
private readonly SettingsManager _settingsManager;
private readonly ObservableCollection<ClipboardItem> clipboardHistory; private readonly ObservableCollection<ClipboardItem> clipboardHistory;
private readonly string _defaultIconPath; private readonly string _defaultIconPath;
public ClipboardHistoryListPage() public ClipboardHistoryListPage(SettingsManager settingsManager)
{ {
ArgumentNullException.ThrowIfNull(settingsManager);
_settingsManager = settingsManager;
clipboardHistory = []; clipboardHistory = [];
_defaultIconPath = string.Empty; _defaultIconPath = string.Empty;
Icon = Icons.ClipboardListIcon; Icon = Icons.ClipboardListIcon;
@@ -84,11 +89,11 @@ internal sealed partial class ClipboardHistoryListPage : ListPage
if (item.Content.Contains(StandardDataFormats.Text)) if (item.Content.Contains(StandardDataFormats.Text))
{ {
var text = await item.Content.GetTextAsync(); var text = await item.Content.GetTextAsync();
items.Add(new ClipboardItem { Content = text, Item = item }); items.Add(new ClipboardItem { Settings = _settingsManager, Content = text, Item = item });
} }
else if (item.Content.Contains(StandardDataFormats.Bitmap)) else if (item.Content.Contains(StandardDataFormats.Bitmap))
{ {
items.Add(new ClipboardItem { Item = item }); items.Add(new ClipboardItem { Settings = _settingsManager, Item = item });
} }
} }

View File

@@ -96,6 +96,42 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to Delete.
/// </summary>
public static string delete_command_name {
get {
return ResourceManager.GetString("delete_command_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Are you sure you want to delete this item from clipboard history? This action cannot be undone..
/// </summary>
public static string delete_confirmation_message {
get {
return ResourceManager.GetString("delete_confirmation_message", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete item?.
/// </summary>
public static string delete_confirmation_title {
get {
return ResourceManager.GetString("delete_confirmation_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Deleted from clipboard history.
/// </summary>
public static string delete_toast_text {
get {
return ResourceManager.GetString("delete_toast_text", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Copy, paste, and search items on the clipboard. /// Looks up a localized string similar to Copy, paste, and search items on the clipboard.
/// </summary> /// </summary>
@@ -140,5 +176,41 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
return ResourceManager.GetString("provider_display_name", resourceCulture); return ResourceManager.GetString("provider_display_name", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
public static string settings_confirm_delete_description {
get {
return ResourceManager.GetString("settings_confirm_delete_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show a confirmation dialog when manually deleting an item.
/// </summary>
public static string settings_confirm_delete_title {
get {
return ResourceManager.GetString("settings_confirm_delete_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to .
/// </summary>
public static string settings_keep_after_paste_description {
get {
return ResourceManager.GetString("settings_keep_after_paste_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Keep items in clipboard history after pasting.
/// </summary>
public static string settings_keep_after_paste_title {
get {
return ResourceManager.GetString("settings_keep_after_paste_title", resourceCulture);
}
}
} }
} }

View File

@@ -144,4 +144,28 @@
<data name="clipboard_failed_to_load" xml:space="preserve"> <data name="clipboard_failed_to_load" xml:space="preserve">
<value>Loading clipboard history failed</value> <value>Loading clipboard history failed</value>
</data> </data>
<data name="delete_command_name" xml:space="preserve">
<value>Delete</value>
</data>
<data name="delete_toast_text" xml:space="preserve">
<value>Deleted from clipboard history</value>
</data>
<data name="settings_keep_after_paste_description" xml:space="preserve">
<value />
</data>
<data name="settings_keep_after_paste_title" xml:space="preserve">
<value>Keep items in clipboard history after pasting</value>
</data>
<data name="settings_confirm_delete_title" xml:space="preserve">
<value>Show a confirmation dialog when manually deleting an item</value>
</data>
<data name="settings_confirm_delete_description" xml:space="preserve">
<value />
</data>
<data name="delete_confirmation_title" xml:space="preserve">
<value>Delete item?</value>
</data>
<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>
</root> </root>

View File

@@ -91,9 +91,9 @@ internal sealed partial class IndexerListItem : ListItem
} }
commands.Add(new CommandContextItem(new OpenWithCommand(fullPath))); commands.Add(new CommandContextItem(new OpenWithCommand(fullPath)));
commands.Add(new CommandContextItem(new ShowFileInFolderCommand(fullPath) { Name = Resources.Indexer_Command_ShowInFolder })); commands.Add(new CommandContextItem(new ShowFileInFolderCommand(fullPath) { Name = Resources.Indexer_Command_ShowInFolder }) { RequestedShortcut = KeyChords.OpenFileLocation });
commands.Add(new CommandContextItem(new CopyPathCommand(fullPath) { Name = Resources.Indexer_Command_CopyPath })); commands.Add(new CommandContextItem(new CopyPathCommand(fullPath) { Name = Resources.Indexer_Command_CopyPath }) { RequestedShortcut = KeyChords.CopyFilePath });
commands.Add(new CommandContextItem(new OpenInConsoleCommand(fullPath))); commands.Add(new CommandContextItem(new OpenInConsoleCommand(fullPath)) { RequestedShortcut = KeyChords.OpenInConsole });
commands.Add(new CommandContextItem(new OpenPropertiesCommand(fullPath))); commands.Add(new CommandContextItem(new OpenPropertiesCommand(fullPath)));
if (IsActionsFeatureEnabled && ApiInformation.IsApiContractPresent("Windows.AI.Actions.ActionsContract", 4)) if (IsActionsFeatureEnabled && ApiInformation.IsApiContractPresent("Windows.AI.Actions.ActionsContract", 4))

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.
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Ext.Indexer;
internal static class KeyChords
{
internal static KeyChord OpenFileLocation { get; } = WellKnownKeyChords.OpenFileLocation;
internal static KeyChord CopyFilePath { get; } = WellKnownKeyChords.CopyFilePath;
internal static KeyChord OpenInConsole { get; } = WellKnownKeyChords.OpenInConsole;
}

View File

@@ -17,6 +17,7 @@ internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem
{ {
Title = string.Empty; Title = string.Empty;
Subtitle = string.Empty; Subtitle = string.Empty;
Icon = Icons.LockIcon;
var isBootedInUefiMode = settings.GetSystemFirmwareType() == FirmwareType.Uefi; var isBootedInUefiMode = settings.GetSystemFirmwareType() == FirmwareType.Uefi;
var hideEmptyRB = settings.HideEmptyRecycleBin(); var hideEmptyRB = settings.HideEmptyRecycleBin();

View File

@@ -22,6 +22,7 @@ internal sealed partial class FallbackTimeDateItem : FallbackCommandItem
{ {
Title = string.Empty; Title = string.Empty;
Subtitle = string.Empty; Subtitle = string.Empty;
Icon = Icons.TimeDateIcon;
_settingsManager = settings; _settingsManager = settings;
_timestamp = timestamp; _timestamp = timestamp;

View File

@@ -79,16 +79,22 @@ public sealed partial class TimeDateCalculator
} }
else else
{ {
List<(int Score, AvailableResult Item)> itemScores = [];
// Generate filtered list of results // Generate filtered list of results
foreach (var f in availableFormats) foreach (var f in availableFormats)
{ {
var score = f.Score(query, f.Label, f.AlternativeSearchTag); var score = f.Score(query, f.Label, f.AlternativeSearchTag);
if (score > 0) if (score > 0)
{ {
results.Add(f.ToListItem()); itemScores.Add((score, f));
} }
} }
results = itemScores
.OrderByDescending(s => s.Score)
.Select(s => s.Item.ToListItem())
.ToList();
} }
if (results.Count == 0) if (results.Count == 0)

View File

@@ -34,9 +34,9 @@ internal sealed partial class SearchWebCommand : InvokableCommand
return CommandResult.KeepOpen(); return CommandResult.KeepOpen();
} }
if (_settingsManager.ShowHistory != Resources.history_none) if (_settingsManager.HistoryItemCount != 0)
{ {
_settingsManager.SaveHistory(new HistoryItem(Arguments, DateTime.Now)); _settingsManager.AddHistoryItem(new HistoryItem(Arguments, DateTime.Now));
} }
return CommandResult.Dismiss(); return CommandResult.Dismiss();

View File

@@ -0,0 +1,119 @@
// 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.IO;
using System.Text.Json;
using System.Threading;
using ManagedCommon;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
internal sealed class HistoryStore
{
private readonly string _filePath;
private readonly List<HistoryItem> _items = [];
private readonly Lock _lock = new();
private int _capacity;
public event EventHandler? Changed;
public HistoryStore(string filePath, int capacity)
{
ArgumentNullException.ThrowIfNull(filePath);
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
_filePath = filePath;
_capacity = capacity;
_items.AddRange(LoadFromDiskSafe());
TrimNoLock();
}
public IReadOnlyList<HistoryItem> HistoryItems
{
get
{
lock (_lock)
{
return [.. _items];
}
}
}
public void Add(HistoryItem item)
{
ArgumentNullException.ThrowIfNull(item);
lock (_lock)
{
_items.Add(item);
_ = TrimNoLock();
SaveNoLock();
}
Changed?.Invoke(this, EventArgs.Empty);
}
public void SetCapacity(int capacity)
{
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
bool trimmed;
lock (_lock)
{
_capacity = capacity;
trimmed = TrimNoLock();
if (trimmed)
{
SaveNoLock();
}
}
if (trimmed)
{
Changed?.Invoke(this, EventArgs.Empty);
}
}
private bool TrimNoLock()
{
var max = _capacity;
if (_items.Count > max)
{
_items.RemoveRange(0, _items.Count - max);
return true;
}
return false;
}
private List<HistoryItem> LoadFromDiskSafe()
{
try
{
if (!File.Exists(_filePath))
{
return [];
}
var fileContent = File.ReadAllText(_filePath);
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
return historyItems;
}
catch (Exception ex)
{
Logger.LogError("Unable to load history", ex);
return [];
}
}
private void SaveNoLock()
{
var json = JsonSerializer.Serialize(_items, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_filePath, json);
}
}

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -9,11 +10,13 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
public interface ISettingsInterface public interface ISettingsInterface
{ {
event EventHandler? HistoryChanged;
public bool GlobalIfURI { get; } public bool GlobalIfURI { get; }
public string ShowHistory { get; } public int HistoryItemCount { get; }
public List<ListItem> LoadHistory(); public IReadOnlyList<HistoryItem> HistoryItems { get; }
public void SaveHistory(HistoryItem historyItem); public void AddHistoryItem(HistoryItem historyItem);
} }

View File

@@ -4,11 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using ManagedCommon;
using System.Text.Json;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -16,19 +13,26 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
public class SettingsManager : JsonSettingsManager, ISettingsInterface public class SettingsManager : JsonSettingsManager, ISettingsInterface
{ {
private readonly string _historyPath; private const string HistoryItemCountLegacySettingsKey = "ShowHistory";
private static readonly string _namespace = "websearch"; private static readonly string _namespace = "websearch";
public event EventHandler? HistoryChanged
{
add => _history.Changed += value;
remove => _history.Changed -= value;
}
private readonly HistoryStore _history;
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
private static readonly List<ChoiceSetSetting.Choice> _choices = private static readonly List<ChoiceSetSetting.Choice> _choices =
[ [
new ChoiceSetSetting.Choice(Resources.history_none, Resources.history_none), new ChoiceSetSetting.Choice(Resources.history_none, "None"),
new ChoiceSetSetting.Choice(Resources.history_1, Resources.history_1), new ChoiceSetSetting.Choice(Resources.history_1, "1"),
new ChoiceSetSetting.Choice(Resources.history_5, Resources.history_5), new ChoiceSetSetting.Choice(Resources.history_5, "5"),
new ChoiceSetSetting.Choice(Resources.history_10, Resources.history_10), new ChoiceSetSetting.Choice(Resources.history_10, "10"),
new ChoiceSetSetting.Choice(Resources.history_20, Resources.history_20), new ChoiceSetSetting.Choice(Resources.history_20, "20"),
]; ];
private readonly ToggleSetting _globalIfURI = new( private readonly ToggleSetting _globalIfURI = new(
@@ -37,17 +41,34 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Resources.plugin_global_if_uri, Resources.plugin_global_if_uri,
false); false);
private readonly ChoiceSetSetting _showHistory = new( private readonly ChoiceSetSetting _historyItemCount = new(
Namespaced(nameof(ShowHistory)), Namespaced(HistoryItemCountLegacySettingsKey),
Resources.plugin_show_history, Resources.plugin_history_item_count,
Resources.plugin_show_history, Resources.plugin_history_item_count,
_choices); _choices);
public bool GlobalIfURI => _globalIfURI.Value; public bool GlobalIfURI => _globalIfURI.Value;
public string ShowHistory => _showHistory.Value ?? string.Empty; public int HistoryItemCount => int.TryParse(_historyItemCount.Value, out var value) && value >= 0 ? value : 0;
internal static string SettingsJsonPath() public IReadOnlyList<HistoryItem> HistoryItems => _history.HistoryItems;
public SettingsManager()
{
FilePath = SettingsJsonPath();
Settings.Add(_globalIfURI);
Settings.Add(_historyItemCount);
LoadSettings();
// Initialize history store after loading settings to get the correct capacity
_history = new HistoryStore(HistoryStateJsonPath(), HistoryItemCount);
Settings.SettingsChanged += (_, _) => SaveSettings();
}
private static string SettingsJsonPath()
{ {
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
@@ -56,7 +77,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
return Path.Combine(directory, "settings.json"); return Path.Combine(directory, "settings.json");
} }
internal static string HistoryStateJsonPath() private static string HistoryStateJsonPath()
{ {
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
@@ -65,156 +86,30 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
return Path.Combine(directory, "websearch_history.json"); return Path.Combine(directory, "websearch_history.json");
} }
public void SaveHistory(HistoryItem historyItem) public void AddHistoryItem(HistoryItem historyItem)
{ {
if (historyItem is null)
{
return;
}
try try
{ {
List<HistoryItem> historyItems; _history.Add(historyItem);
// Check if the file exists and load existing history
if (File.Exists(_historyPath))
{
var existingContent = File.ReadAllText(_historyPath);
historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
}
else
{
historyItems = [];
}
// Add the new history item
historyItems.Add(historyItem);
// Determine the maximum number of items to keep based on ShowHistory
if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0)
{
// Keep only the most recent `maxHistoryItems` items
while (historyItems.Count > maxHistoryItems)
{
historyItems.RemoveAt(0); // Remove the oldest item
}
}
// Serialize the updated list back to JSON and save it
var historyJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_historyPath, historyJson);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError("Failed to add item to the search history", ex);
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
} }
} }
public List<ListItem> LoadHistory()
{
try
{
if (!File.Exists(_historyPath))
{
return [];
}
// Read and deserialize JSON into a list of HistoryItem objects
var fileContent = File.ReadAllText(_historyPath);
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Convert each HistoryItem to a ListItem
var listItems = new List<ListItem>();
foreach (var historyItem in historyItems)
{
listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this))
{
Title = historyItem.SearchString,
Subtitle = historyItem.Timestamp.ToString("g", CultureInfo.InvariantCulture), // Ensures consistent formatting
});
}
listItems.Reverse();
return listItems;
}
catch (Exception ex)
{
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
return [];
}
}
public SettingsManager()
{
FilePath = SettingsJsonPath();
_historyPath = HistoryStateJsonPath();
Settings.Add(_globalIfURI);
Settings.Add(_showHistory);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();
}
private void ClearHistory()
{
try
{
if (File.Exists(_historyPath))
{
// Delete the history file
File.Delete(_historyPath);
// Log that the history was successfully cleared
ExtensionHost.LogMessage(new LogMessage() { Message = "History cleared successfully." });
}
else
{
// Log that there was no history file to delete
ExtensionHost.LogMessage(new LogMessage() { Message = "No history file found to clear." });
}
}
catch (Exception ex)
{
// Log any exception that occurs
ExtensionHost.LogMessage(new LogMessage() { Message = $"Failed to clear history: {ex}" });
}
}
public override void SaveSettings() public override void SaveSettings()
{ {
base.SaveSettings(); base.SaveSettings();
try try
{ {
if (ShowHistory == Resources.history_none) _history.SetCapacity(HistoryItemCount);
{
ClearHistory();
}
else if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0)
{
// Trim the history file if there are more items than the new limit
if (File.Exists(_historyPath))
{
var existingContent = File.ReadAllText(_historyPath);
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Check if trimming is needed
if (historyItems.Count > maxHistoryItems)
{
// Trim the list to keep only the most recent `maxHistoryItems` items
historyItems = historyItems.Skip(historyItems.Count - maxHistoryItems).ToList();
// Save the trimmed history back to the file
var trimmedHistoryJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_historyPath, trimmedHistoryJson);
}
}
}
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError("Failed to save the search history", ex);
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
} }
} }

View File

@@ -5,8 +5,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Text; using System.Text;
using System.Threading;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
@@ -16,31 +16,30 @@ using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Pages; namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
internal sealed partial class WebSearchListPage : DynamicListPage internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
{ {
private readonly string _iconPath = string.Empty; private readonly IconInfo _newSearchIcon = new(string.Empty);
private readonly List<ListItem>? _historyItems;
private readonly ISettingsInterface _settingsManager; private readonly ISettingsInterface _settingsManager;
private readonly Lock _sync = new();
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name); private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open); private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
private List<ListItem> _allItems; private IListItem[] _allItems = [];
private List<ListItem> _historyItems = [];
public WebSearchListPage(ISettingsInterface settingsManager) public WebSearchListPage(ISettingsInterface settingsManager)
{ {
ArgumentNullException.ThrowIfNull(settingsManager);
Name = Resources.command_item_title; Name = Resources.command_item_title;
Title = Resources.command_item_title; Title = Resources.command_item_title;
Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png"); Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png");
_allItems = [];
Id = "com.microsoft.cmdpal.websearch"; Id = "com.microsoft.cmdpal.websearch";
_settingsManager = settingsManager; _settingsManager = settingsManager;
_historyItems = _settingsManager.ShowHistory != Resources.history_none ? _settingsManager.LoadHistory() : null; _settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged;
if (_historyItems is not null)
{
_allItems.AddRange(_historyItems);
}
// It just looks viewer to have string twice on the page, and default placeholder is good enough // It just looks viewer to have string twice on the page, and default placeholder is good enough
PlaceholderText = _allItems.Count > 0 ? Resources.plugin_description : string.Empty; PlaceholderText = _allItems.Length > 0 ? Resources.plugin_description : string.Empty;
EmptyContent = new CommandItem(new NoOpCommand()) EmptyContent = new CommandItem(new NoOpCommand())
{ {
@@ -48,45 +47,102 @@ internal sealed partial class WebSearchListPage : DynamicListPage
Title = Properties.Resources.plugin_description, Title = Properties.Resources.plugin_description,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
}; };
UpdateHistory();
RequeryAndUpdateItems(SearchText);
} }
public List<ListItem> Query(string query) private void SettingsManagerOnHistoryChanged(object? sender, EventArgs e)
{ {
ArgumentNullException.ThrowIfNull(query); UpdateHistory();
IEnumerable<ListItem>? filteredHistoryItems = null; RequeryAndUpdateItems(SearchText);
}
if (_historyItems is not null) private void UpdateHistory()
{
List<ListItem> history = [];
if (_settingsManager.HistoryItemCount > 0)
{ {
filteredHistoryItems = _settingsManager.ShowHistory != Resources.history_none ? ListHelpers.FilterList(_historyItems, query).OfType<ListItem>() : null; var items = _settingsManager.HistoryItems;
for (var index = items.Count - 1; index >= 0; index--)
{
var historyItem = items[index];
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager))
{
Title = historyItem.SearchString,
Subtitle = historyItem.Timestamp.ToString("g", CultureInfo.InvariantCulture),
});
}
} }
var results = new List<ListItem>(); lock (_sync)
{
_historyItems = history;
}
}
private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager, IconInfo newSearchIcon)
{
ArgumentNullException.ThrowIfNull(query);
var filteredHistoryItems = settingsManager.HistoryItemCount > 0
? ListHelpers.FilterList(historySnapshot, query)
: [];
var results = new List<IListItem>();
if (!string.IsNullOrEmpty(query)) if (!string.IsNullOrEmpty(query))
{ {
var searchTerm = query; var searchTerm = query;
var result = new ListItem(new SearchWebCommand(searchTerm, _settingsManager)) var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager))
{ {
Title = searchTerm, Title = searchTerm,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
Icon = new IconInfo(_iconPath), Icon = newSearchIcon,
}; };
results.Add(result); results.Add(result);
} }
if (filteredHistoryItems is not null) results.AddRange(filteredHistoryItems);
return [.. results];
}
private void RequeryAndUpdateItems(string search)
{
List<ListItem> historySnapshot;
lock (_sync)
{ {
results.AddRange(filteredHistoryItems); historySnapshot = _historyItems;
} }
return results; var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _newSearchIcon);
lock (_sync)
{
_allItems = items;
}
RaiseItemsChanged();
} }
public override void UpdateSearchText(string oldSearch, string newSearch) public override void UpdateSearchText(string oldSearch, string newSearch)
{ {
_allItems = [.. Query(newSearch)]; RequeryAndUpdateItems(newSearch);
RaiseItemsChanged(0);
} }
public override IListItem[] GetItems() => [.. _allItems]; public override IListItem[] GetItems()
{
lock (_sync)
{
return _allItems;
}
}
public void Dispose()
{
_settingsManager.HistoryChanged -= SettingsManagerOnHistoryChanged;
GC.SuppressFinalize(this);
}
} }

View File

@@ -168,6 +168,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to Determines the number of history items to show from previous searches.
/// </summary>
public static string plugin_history_item_count {
get {
return ResourceManager.GetString("plugin_history_item_count", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to In the default browser. /// Looks up a localized string similar to In the default browser.
/// </summary> /// </summary>
@@ -231,15 +240,6 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to Determines the number of history items to show from previous searches.
/// </summary>
public static string plugin_show_history {
get {
return ResourceManager.GetString("plugin_show_history", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Settings. /// Looks up a localized string similar to Settings.
/// </summary> /// </summary>

View File

@@ -172,7 +172,7 @@
<data name="plugin_search_failed" xml:space="preserve"> <data name="plugin_search_failed" xml:space="preserve">
<value>Failed to open {0}.</value> <value>Failed to open {0}.</value>
</data> </data>
<data name="plugin_show_history" xml:space="preserve"> <data name="plugin_history_item_count" xml:space="preserve">
<value>Determines the number of history items to show from previous searches</value> <value>Determines the number of history items to show from previous searches</value>
</data> </data>
<data name="settings_page_name" xml:space="preserve"> <data name="settings_page_name" xml:space="preserve">

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
@@ -15,6 +16,9 @@ public partial class WebSearchCommandsProvider : CommandProvider
private readonly SettingsManager _settingsManager = new(); private readonly SettingsManager _settingsManager = new();
private readonly FallbackExecuteSearchItem _fallbackItem; private readonly FallbackExecuteSearchItem _fallbackItem;
private readonly FallbackOpenURLItem _openUrlFallbackItem; private readonly FallbackOpenURLItem _openUrlFallbackItem;
private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem;
private readonly ICommandItem[] _topLevelItems;
private readonly IFallbackCommandItem[] _fallbackCommands;
public WebSearchCommandsProvider() public WebSearchCommandsProvider()
{ {
@@ -25,18 +29,27 @@ public partial class WebSearchCommandsProvider : CommandProvider
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager); _fallbackItem = new FallbackExecuteSearchItem(_settingsManager);
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager); _openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager);
}
public override ICommandItem[] TopLevelCommands() _webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager)
{
return [new WebSearchTopLevelCommandItem(_settingsManager)
{ {
MoreCommands = [ MoreCommands =
[
new CommandContextItem(Settings!.SettingsPage), new CommandContextItem(Settings!.SettingsPage),
], ],
} };
]; _topLevelItems = [_webSearchTopLevelItem];
_fallbackCommands = [_openUrlFallbackItem, _fallbackItem];
} }
public override IFallbackCommandItem[]? FallbackCommands() => [_openUrlFallbackItem, _fallbackItem]; public override ICommandItem[] TopLevelCommands() => _topLevelItems;
public override IFallbackCommandItem[]? FallbackCommands() => _fallbackCommands;
public override void Dispose()
{
_webSearchTopLevelItem?.Dispose();
base.Dispose();
GC.SuppressFinalize(this);
}
} }

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.IO;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Pages; using Microsoft.CmdPal.Ext.WebSearch.Pages;
@@ -13,7 +12,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch; namespace Microsoft.CmdPal.Ext.WebSearch;
public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable
{ {
private readonly SettingsManager _settingsManager; private readonly SettingsManager _settingsManager;
@@ -27,17 +26,29 @@ public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandle
private void SetDefaultTitle() => Title = Resources.command_item_title; private void SetDefaultTitle() => Title = Resources.command_item_title;
private void ReplaceCommand(ICommand newCommand)
{
(Command as IDisposable)?.Dispose();
Command = newCommand;
}
public void UpdateQuery(string query) public void UpdateQuery(string query)
{ {
if (string.IsNullOrEmpty(query)) if (string.IsNullOrEmpty(query))
{ {
SetDefaultTitle(); SetDefaultTitle();
Command = new WebSearchListPage(_settingsManager); ReplaceCommand(new WebSearchListPage(_settingsManager));
} }
else else
{ {
Title = query; Title = query;
Command = new SearchWebCommand(query, _settingsManager); ReplaceCommand(new SearchWebCommand(query, _settingsManager));
} }
} }
public void Dispose()
{
(Command as IDisposable)?.Dispose();
GC.SuppressFinalize(this);
}
} }

View File

@@ -65,6 +65,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
if (_results is not null && _results.Count != 0) if (_results is not null && _results.Count != 0)
{ {
var stopwatch = Stopwatch.StartNew();
var count = _results.Count; var count = _results.Count;
var results = new ListItem[count]; var results = new ListItem[count];
var next = 0; var next = 0;
@@ -82,6 +83,8 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
} }
} }
stopwatch.Stop();
Logger.LogDebug($"Building ListItems took {stopwatch.ElapsedMilliseconds}ms", memberName: nameof(GetItems));
IsLoading = false; IsLoading = false;
return results; return results;
} }
@@ -244,15 +247,22 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
// foreach (var catalog in connections) // foreach (var catalog in connections)
{ {
Stopwatch findPackages_stopwatch = new();
findPackages_stopwatch.Start();
Logger.LogDebug($" Searching {catalog.Info.Name} ({query})", memberName: nameof(DoSearchAsync)); Logger.LogDebug($" Searching {catalog.Info.Name} ({query})", memberName: nameof(DoSearchAsync));
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
Logger.LogDebug($"Preface for \"{searchDebugText}\" took {stopwatch.ElapsedMilliseconds}ms", memberName: nameof(DoSearchAsync));
// BODGY, re: microsoft/winget-cli#5151 // BODGY, re: microsoft/winget-cli#5151
// FindPackagesAsync isn't actually async. // FindPackagesAsync isn't actually async.
var internalSearchTask = Task.Run(() => catalog.FindPackages(opts), ct); var internalSearchTask = Task.Run(() => catalog.FindPackages(opts), ct);
var searchResults = await internalSearchTask; var searchResults = await internalSearchTask;
findPackages_stopwatch.Stop();
Logger.LogDebug($"FindPackages for \"{searchDebugText}\" took {findPackages_stopwatch.ElapsedMilliseconds}ms", memberName: nameof(DoSearchAsync));
// TODO more error handling like this: // TODO more error handling like this:
if (searchResults.Status != FindPackagesResultStatus.Ok) if (searchResults.Status != FindPackagesResultStatus.Ok)
{ {
@@ -261,6 +271,8 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
return []; return [];
} }
ct.ThrowIfCancellationRequested();
Logger.LogDebug($" got results for ({query})", memberName: nameof(DoSearchAsync)); Logger.LogDebug($" got results for ({query})", memberName: nameof(DoSearchAsync));
// FYI Using .ToArray or any other kind of enumerable loop // FYI Using .ToArray or any other kind of enumerable loop

View File

@@ -1,39 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Resources;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.WindowsSettings.Classes;
using Microsoft.CmdPal.Ext.WindowsSettings.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
using Windows.Networking.NetworkOperators;
using Windows.UI;
namespace Microsoft.CmdPal.Ext.WindowsSettings.Commands;
internal sealed partial class CopySettingCommand : InvokableCommand
{
private readonly WindowsSetting _entry;
internal CopySettingCommand(WindowsSetting entry)
{
Name = Resources.CopyCommand;
Icon = Icons.CopyIcon;
_entry = entry;
}
public override CommandResult Invoke()
{
ClipboardHelper.SetText(_entry.Command);
return CommandResult.Dismiss();
}
}

View File

@@ -23,7 +23,7 @@ internal static class ContextMenuHelper
{ {
var list = new List<CommandContextItem>(1) var list = new List<CommandContextItem>(1)
{ {
new(new CopySettingCommand(entry)), new(new CopyTextCommand(entry.Command) { Name = Resources.CopyCommand }),
}; };
return list; return list;

View File

@@ -0,0 +1,189 @@
// 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.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension.Pages;
internal sealed partial class SampleIconPage : ListPage
{
private readonly IListItem[] _items =
[
/*
* Quick intro to Unicode in source code:
* - Every character has a code point (e.g., U+0041 = 'A').
* - Code points up to U+FFFF use \u1234 (4 hex digits and lowercase u).
* - Code points above that (up to U+10FFFF) use \U12345678 (8 hex digits and capital letter U).
* - If your source file is UTF-8, you can type the character directly, but it may not display properly in editors,
* and it's harder to see the actual code point.
* - Some symbols (like many emojis) are built from multiple code points
* joined together (e.g., 👋🏻 = U+1F44B + U+1F3FB).
*
* Examples:
* 😍 = "😍" or "\U0001F60D"
* 👋🏻 = "👋🏻" or "\U0001F44B\U0001F3FB"
* 🧙‍♂️ = "🧙‍♂️" or "\U0001F9D9\u200D\u2642\U0000FE0F" (male mage)
* 🧙🏿‍♀️ = "🧙🏿‍♀️" or "\U0001F9D9\U0001F3FF\u200D\u2640\U0000FE0F" (dark-skinned woman mage)
*
*/
// Emoji Smiling Face with Heart-Eyes
// Unicode: \U0001F60D
BuildIconItem("😍", "Standard emoji icon", "Basic emoji character rendered as an icon"),
// Emoji Smiling Face with Heart-Eyes
// Unicode: \U0001F60D\U0001F643\U0001F622
BuildIconItem("😍🙃😢", "Multiple emojis", "Use of multiple emojis for icon is not allowed"),
// Emoji Smiling Face with Sunglasses
// Unicode: \U0001F60E
BuildIconItem("\U0001F60E", "Unicode escape sequence emoji", "Emoji defined using Unicode escape sequence notation"),
// Segoe Fluent Icons font icon
// Unicode: \uE8D4
BuildIconItem("\uE8D4", "Segoe Fluent icon demonstration", "Segoe Fluent/MDL2 icon from system font\nWorks as an icon but won't display properly in button text"),
// Extended pictographic symbol for keyboard
BuildIconItem("\u2328", "Extended pictographic symbol", "Pictographic symbol representing a keyboard"),
// Capital letter A
BuildIconItem("A", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
// Letter 1
// Unicode: \U00000031
BuildIconItem("1", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
// Emoji Keycap Digit Two ... 2
// Unicode: \U00000032\U000020E3
// This is a sequence of three code points: the digit '2' (U+0032), and a combining enclosing keycap (U+20E3). No variation selector is used here.
BuildIconItem("\U00000032\U000020E3", "Emoji without variation selector", "Emoji character doesn't have VS16 variation selector to render as text"),
// Emoji Keycap Digit Three ... 3
// Unicode: \U00000033\U0000FE0F\U000020E3
// This is a sequence of three code points: the digit '3' (U+0033), a variation selector (U+FE0F) to specify emoji presentation, and a combining enclosing keycap (U+20E3).
BuildIconItem("3⃣", "Emoji with variation selector", "Emoji character using a variation selector to specify emoji presentation"),
// Symbol #
// Unicode: \u0023
BuildIconItem("#", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
// Symbol # keycap
// Unicode: \u0023\ufe0f\u20e3
// Sequence of 3 code points: symbol #, a variation selector (U+FE0F) to specify emoji presentation, and a combining enclosing keycap (U+20E3).
BuildIconItem("\u0023\ufe0f\u20e3", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
// Capital letter WM
// This is two characters, which is not a valid icon representation. It will be replaced by a placeholder signalizing an invalid icon.
BuildIconItem("WM", "Invalid icon representation", "String with multiple characters that does not correspond to a valid single icon"),
// Emoji Mage
// Unicode: \U0001F9D9
BuildIconItem("🧙", "Single code-point emoji example", "Simple emoji character using a single Unicode code point"),
// Emoji Male Mage (Mage with gender modifier)
// Unicode: \U0001F9D9\u200D\u2642\uFE0F
BuildIconItem("🧙‍♂️", "Complex emoji with gender modifier", "Composite emoji using Zero-Width Joiner (ZWJ) sequence for male variant"),
// Emoji Woman Mage (Mage with gender modifier)
// Unicode: \U0001F9D9\u200D\u2640\uFE0F
BuildIconItem("\U0001F9D9\u200D\u2640\uFE0F", "Complex emoji with gender modifier", "Composite emoji using Zero-Width Joiner (ZWJ) sequence for female variant"),
// Emoji Waving Hand
// Unicode: \U0001F44B
BuildIconItem("👋", "Basic hand gesture emoji", "Standard emoji character representing a waving hand"),
// Emoji Waving Hand + Light Skin Tone
// Unicode: \U0001F44B\U0001F3FB
BuildIconItem("👋🏻", "Emoji with light skin tone modifier", "Emoji enhanced with Unicode skin tone modifier (light)"),
// Emoji Waving Hand + Dark Skin Tone
// Unicode: \U0001F44B\U0001F3FF
BuildIconItem("\U0001F44B\U0001F3FF", "Emoji with dark skin tone modifier", "Emoji enhanced with Unicode skin tone modifier (dark)"),
// Flag of Czechia (Czech Republic)
// Unicode: \U0001F1E8\U0001F1FF
BuildIconItem("\U0001F1E8\U0001F1FF", "Flag emoji using regional indicators", "Emoji flag constructed from regional indicator symbols for Czechia"),
// Use of ZWJ without emojis
// KA (\u0995) + VIRAMA (\u09CD) + ZWJ (\u200D) - shows the half-form KA
// Unicode: \u0995\u09CD\u200D
BuildIconItem("\u0995\u09CD\u200D", "Use of ZWJ in non-emoji context", "Shows the half-form KA"),
// Use of ZWJ without emojis
// KA (\u0995) + VIRAMA (\u09CD) + Shows full KA with an explicit virama mark (not half-form).
// Unicode: \u0995\u09CD
BuildIconItem("\u0995\u09CD", "Use of ZWJ in non-emoji context", "Shows full KA with an explicit virama mark"),
// mahjong tile red dragon (using Unicode escape sequence)
// https://en.wikipedia.org/wiki/Mahjong_Tiles_(Unicode_block)
// Unicode: \U0001F004
BuildIconItem("\U0001F004", "Mahjong tile emoji (red dragon)", "Mahjong tile red dragon emoji character using Unicode escape sequence"),
// mahjong tile green dragon (non-emoji)
// https://en.wikipedia.org/wiki/Mahjong_Tiles_(Unicode_block)
// Unicode: \U0001F005
BuildIconItem("\U0001F005", "Mahjong tile non-emoji (green dragon)", "Mahjong tile character that is not classified as an emoji"),
// Play, PlayPause, Stop
BuildIconItem("\u25B6", "Play symbol (standalone)", "Play symbol"),
BuildIconItem("\u25B6\uFE0E", "Play symbol + VS15 (request text)", "Play symbol with variation specifier requesting rendering as text"),
BuildIconItem("\u25B6\uFE0F", "Play symbol + VS16 (request emoji)", "Play symbol with variation specifier requesting rendering as emoji "),
BuildIconItem("⏯️", "Play/Pause keycap emoji", "Play/Pause keycap emoji doesn't have plain text variant"),
BuildIconItem("⏸️", "Pause keycap emoji", "Pause keycap emoji doesn't have plain text variant"),
// Copyright and emoji copyright:
BuildIconItem("\u00a9", "Copyright symbol (standalone)", "Copyright symbol that is not classified as an emoji"),
BuildIconItem("\u00a9\uFE0E", "Copyright symbol + VS15 (request text)", "Copyright symbol that is not classified as an emoji"),
BuildIconItem("\u00a9\uFE0F", "Copyright symbol + VS16 (request emoji)", "Copyright symbol that is not classified as an emoji"),
// Tag flags
BuildIconItem("🏳️", "White Flag", "White Flag"),
BuildIconItem("\U0001F3F4\u200D\u2620\uFE0F", "Pirate Flag", "Pirate Flag"),
];
public SampleIconPage()
{
Icon = new IconInfo("\uE8BA");
Name = "Sample Icon Page";
ShowDetails = true;
}
public override IListItem[] GetItems() => _items;
private static ListItem BuildIconItem(string icon, string title, string description)
{
var iconInfo = new IconInfo(icon);
return new ListItem(new CopyTextCommand(icon) { Name = "Action with " + icon })
{
Title = title,
Subtitle = description,
Icon = iconInfo,
Tags = [
new Tag("Tag") { Icon = iconInfo },
],
Details = new Details
{
HeroImage = iconInfo,
Title = title,
Body = description,
Metadata = [
new DetailsElement
{
Key = "Unicode Code Points",
Data = new DetailsTags
{
Tags = icon.EnumerateRunes()
.Select(rune => rune.Value <= 0xFFFF ? $"\\u{rune.Value:X4}" : $"\\U{rune.Value:X8}")
.Select(t => new Tag(t))
.ToArray<ITag>(),
},
}
],
},
};
}
}

View File

@@ -4,6 +4,7 @@
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using SamplePagesExtension.Pages;
namespace SamplePagesExtension; namespace SamplePagesExtension;
@@ -37,6 +38,11 @@ public partial class SamplesListPage : ListPage
Title = "Demo of OnLoad/OnUnload", Title = "Demo of OnLoad/OnUnload",
Subtitle = "Changes the list of items every time the page is opened / closed", Subtitle = "Changes the list of items every time the page is opened / closed",
}, },
new ListItem(new SampleIconPage())
{
Title = "Sample Icon Page",
Subtitle = "A demo of using icons in various ways",
},
// Content pages // Content pages
new ListItem(new SampleContentPage()) new ListItem(new SampleContentPage())

View File

@@ -43,13 +43,18 @@ public partial class ListHelpers
} }
public static IEnumerable<T> FilterList<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction) public static IEnumerable<T> FilterList<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction)
{
return FilterListWithScores<T>(items, query, scoreFunction)
.Select(score => score.Item);
}
public static IEnumerable<Scored<T>> FilterListWithScores<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction)
{ {
var scores = items var scores = items
.Select(li => new Scored<T>() { Item = li, Score = scoreFunction(query, li) }) .Select(li => new Scored<T>() { Item = li, Score = scoreFunction(query, li) })
.Where(score => score.Score > 0) .Where(score => score.Score > 0)
.OrderByDescending(score => score.Score); .OrderByDescending(score => score.Score);
return scores return scores;
.Select(score => score.Item);
} }
/// <summary> /// <summary>

View File

@@ -372,20 +372,17 @@ namespace UITests_FancyZones
// launch FancyZones settings page // launch FancyZones settings page
private void LaunchFancyZones() private void LaunchFancyZones()
{ {
if (this.FindAll<NavigationViewItem>("FancyZones").Count == 0) this.Find<NavigationViewItem>(By.AccessibilityId("WindowingAndLayoutsNavItem")).Click();
{
this.Find<NavigationViewItem>("Windowing & Layouts").Click();
}
this.Find<NavigationViewItem>("FancyZones").Click(); this.Find<NavigationViewItem>(By.AccessibilityId("FancyZonesNavItem")).Click();
this.Find<ToggleSwitch>("Enable FancyZones").Toggle(true); this.Find<ToggleSwitch>(By.AccessibilityId("EnableFancyZonesToggleSwitch")).Toggle(true);
this.Session.SetMainWindowSize(WindowSize.Large); this.Session.SetMainWindowSize(WindowSize.Large);
Find<Element>(By.AccessibilityId("HeaderPresenter")).Click(); Find<Element>(By.AccessibilityId("HeaderPresenter")).Click();
this.Scroll(6, "Down"); // Pull the settings page up to make sure the settings are visible this.Scroll(6, "Down"); // Pull the settings page up to make sure the settings are visible
ZoneBehaviourSettings(TestContext.TestName); ZoneBehaviourSettings(TestContext.TestName);
this.Find<Microsoft.PowerToys.UITest.Button>("Launch layout editor").Click(false, 500, 10000); this.Find<Microsoft.PowerToys.UITest.Button>(By.AccessibilityId("LaunchLayoutEditorButton")).Click(false, 500, 10000);
this.Session.Attach(PowerToysModule.FancyZone); this.Session.Attach(PowerToysModule.FancyZone);
// pipeline machine may have an unstable delays, causing the custom layout to be unavailable as we set. then A retry is required. // pipeline machine may have an unstable delays, causing the custom layout to be unavailable as we set. then A retry is required.
@@ -403,7 +400,7 @@ namespace UITests_FancyZones
this.Find<Microsoft.PowerToys.UITest.Button>("Close").Click(); this.Find<Microsoft.PowerToys.UITest.Button>("Close").Click();
this.Session.Attach(PowerToysModule.PowerToysSettings); this.Session.Attach(PowerToysModule.PowerToysSettings);
SetupCustomLayouts(); SetupCustomLayouts();
this.Find<Microsoft.PowerToys.UITest.Button>("Launch layout editor").Click(false, 5000, 5000); this.Find<Microsoft.PowerToys.UITest.Button>(By.AccessibilityId("LaunchLayoutEditorButton")).Click(false, 5000, 5000);
this.Session.Attach(PowerToysModule.FancyZone); this.Session.Attach(PowerToysModule.FancyZone);
this.Find<Microsoft.PowerToys.UITest.Button>("Maximize").Click(); this.Find<Microsoft.PowerToys.UITest.Button>("Maximize").Click();

View File

@@ -43,11 +43,32 @@ private:
//contains the non localized key of the powertoy //contains the non localized key of the powertoy
std::wstring app_key; std::wstring app_key;
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::EnsureRegistered();
Logger::info(L"ImageResizer context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::Unregister();
Logger::info(L"ImageResizer context menu unregistered");
#endif
}
}
public: public:
// Constructor // Constructor
ImageResizerModule() ImageResizerModule()
{ {
m_enabled = CSettingsInstance().GetEnabled(); m_enabled = CSettingsInstance().GetEnabled();
UpdateRegistration(m_enabled);
app_name = GET_RESOURCE_STRING(IDS_IMAGERESIZER); app_name = GET_RESOURCE_STRING(IDS_IMAGERESIZER);
app_key = ImageResizerConstants::ModuleKey; app_key = ImageResizerConstants::ModuleKey;
LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::imageResizerLoggerName); LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::imageResizerLoggerName);
@@ -112,10 +133,7 @@ public:
package::RegisterSparsePackage(path, packageUri); package::RegisterSparsePackage(path, packageUri);
} }
} }
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG) UpdateRegistration(m_enabled);
ImageResizerRuntimeRegistration::EnsureRegistered();
#endif
Trace::EnableImageResizer(m_enabled); Trace::EnableImageResizer(m_enabled);
} }
@@ -123,11 +141,8 @@ public:
virtual void disable() virtual void disable()
{ {
m_enabled = false; m_enabled = false;
UpdateRegistration(m_enabled);
Trace::EnableImageResizer(m_enabled); Trace::EnableImageResizer(m_enabled);
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::Unregister();
Logger::info(L"ImageResizer context menu unregistered (Win10)");
#endif
} }
// Returns if the powertoys is enabled // Returns if the powertoys is enabled

View File

@@ -168,6 +168,25 @@ private:
//contains the non localized key of the powertoy //contains the non localized key of the powertoy
std::wstring app_key; std::wstring app_key;
// Update registration based on enabled state
void UpdateRegistration(bool enabled)
{
if (enabled)
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::EnsureRegistered();
Logger::info(L"PowerRename context menu registered");
#endif
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::Unregister();
Logger::info(L"PowerRename context menu unregistered");
#endif
}
}
public: public:
// Return the localized display name of the powertoy // Return the localized display name of the powertoy
virtual PCWSTR get_name() override virtual PCWSTR get_name() override
@@ -202,9 +221,7 @@ public:
package::RegisterSparsePackage(path, packageUri); package::RegisterSparsePackage(path, packageUri);
} }
} }
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG) UpdateRegistration(m_enabled);
PowerRenameRuntimeRegistration::EnsureRegistered();
#endif
} }
// Disable the powertoy // Disable the powertoy
@@ -212,10 +229,7 @@ public:
{ {
m_enabled = false; m_enabled = false;
Logger::info(L"PowerRename disabled"); Logger::info(L"PowerRename disabled");
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG) UpdateRegistration(m_enabled);
PowerRenameRuntimeRegistration::Unregister();
Logger::info(L"PowerRename context menu unregistered (Win10)");
#endif
} }
// Returns if the powertoy is enabled // Returns if the powertoy is enabled
@@ -315,6 +329,7 @@ public:
void init_settings() void init_settings()
{ {
m_enabled = CSettingsInstance().GetEnabled(); m_enabled = CSettingsInstance().GetEnabled();
UpdateRegistration(m_enabled);
Trace::EnablePowerRename(m_enabled); Trace::EnablePowerRename(m_enabled);
} }

View File

@@ -15,10 +15,10 @@ namespace RegistryPreview
/// </summary> /// </summary>
private void AppWindow_Closing(Microsoft.UI.Windowing.AppWindow sender, Microsoft.UI.Windowing.AppWindowClosingEventArgs args) private void AppWindow_Closing(Microsoft.UI.Windowing.AppWindow sender, Microsoft.UI.Windowing.AppWindowClosingEventArgs args)
{ {
jsonWindowPlacement.SetNamedValue("appWindow.Position.X", JsonValue.CreateNumberValue(appWindow.Position.X)); jsonWindowPlacement.SetNamedValue("appWindow.Position.X", JsonValue.CreateNumberValue(AppWindow.Position.X));
jsonWindowPlacement.SetNamedValue("appWindow.Position.Y", JsonValue.CreateNumberValue(appWindow.Position.Y)); jsonWindowPlacement.SetNamedValue("appWindow.Position.Y", JsonValue.CreateNumberValue(AppWindow.Position.Y));
jsonWindowPlacement.SetNamedValue("appWindow.Size.Width", JsonValue.CreateNumberValue(appWindow.Size.Width)); jsonWindowPlacement.SetNamedValue("appWindow.Size.Width", JsonValue.CreateNumberValue(AppWindow.Size.Width));
jsonWindowPlacement.SetNamedValue("appWindow.Size.Height", JsonValue.CreateNumberValue(appWindow.Size.Height)); jsonWindowPlacement.SetNamedValue("appWindow.Size.Height", JsonValue.CreateNumberValue(AppWindow.Size.Height));
} }
/// <summary> /// <summary>

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