Compare commits

..

68 Commits

Author SHA1 Message Date
Zachary Teutsch
6e9a4be1a5 update output path for kbm 2025-12-10 14:12:33 -05:00
Hao Liu
161240e9c0 [Keyboard Manager] New Keyboard Manager - Text Remapping Page and Code-behind functionalities (#39888)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

> [Target feature branch ``feature/KeyboardManagerWinUI3``]

Refactor the New Keyboard Manager code to generalize functions (like
Keyboard Hook) for use across pages. Implement Code-behind and the Text
Remapping Page.

- Implement the Text Remapping page
- Restructure the Key Remappings code
- Implement unified Keyboard Hook for all Pages
- Implement Save and Delete for Text Page
- Adjust the Text Page list UI
- Create Input Control for Text Page
- Validate the user input in Text Page and show teaching tip for the
validation error
- Move Remapping Saving into Helper
- Move Remapping Delete into Helper
- Don't check for duplicates if the original keys are the same as the
remapping being edited
- Use InputMode to indicate the active toggle for Original Keys/Remapped
Keys
- Add Rectangle for all pages


![image](https://github.com/user-attachments/assets/e109d82f-01c5-4da7-9efb-facea4070808)

![image](https://github.com/user-attachments/assets/09cc4462-43eb-4823-9b01-ab630a0bf9ed)

![image](https://github.com/user-attachments/assets/9b85c0f6-cbce-40cc-8e65-b755cb852eae)

![image](https://github.com/user-attachments/assets/ff7945cc-6535-4697-812e-93c2effab14e)

![image](https://github.com/user-attachments/assets/7cf887c8-24f3-45bd-956f-b3d655844974)

![image](https://github.com/user-attachments/assets/d958bb16-66b9-44e0-8918-32ddcc780f02)
2025-06-09 15:10:16 +08:00
Hao Liu
3e69b2a411 Merge branch 'main' into feature/KeyboardManagerWinUI3 2025-04-30 10:48:15 +08:00
Kai Tao
9cb99be4e9 [Tool] Delete export pfx function to remove use of hard coded password (#39144)
don't need export pfx functionality
2025-04-29 18:31:21 +08:00
Hao Liu
d349d81bd5 [Keyboard Manager] New Keyboard Manager - Keys Remapping Window, Keyboard Hook and Input Validator (#39081)
* Add delete remapping function

* Fix the problem that Keyboard Hook is not cleaned properly when the toggle button lost focus (user clicked other checkbox or switched to another app)

* Make sure original toggle button is checked for InputControl

* Disable app setting for single key remapping

* Update KBM Editor UI Signing

* Ensuring Modifier Keys Don't Repeat

* Make sure the shortcut takes at most 4 modifier keys and show notification to the user

* Show teaching tip for reserved illegal shortcuts - Win+L and Ctrl+Alt+Delete

* Add validation to check if the user is trying to remap a key or shortcut that's already mapped for the same app context

* Add validation to make sure key/shortcut cannot be mapped to itself

* Add validation to check the original/new keys are not empty. Application name must be entered if the specific app checkbox is checked.

* Add validation to check shortcuts must contain at least one action key in addition to modifier keys

* Add validation to warn orphaned key to user

* Unify and consolidate the validation code

* Refactor the validation mechanism

* Delete the existing remapping when user modified the content
2025-04-29 17:01:31 +08:00
Mike Griese
ad974bd679 Wait to update SearchText until we've actually updated SearchText (#39093)
Closes #38829

If we always UpdateProperty here, then there's a possible
race condition, where we raise the PropertyChanged(SearchText)
before the subclass actually retrieves the new SearchText from the
model. In that race situation, if the UI thread handles the
PropertyChanged before ListViewModel fetches the SearchText, it'll
think that the old search text is the _new_ value.
2025-04-27 15:29:15 -05:00
Yu Leng
49e5bbb5f0 [cmdpal][aot] Remove some unused file in CmdPal.Common and mark it as AOT compatible. (#39110)
* Remove unused com interface

* Remove unused file

* Remove unused file

* Remove unused file

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-27 17:18:01 +08:00
Kai Tao
7efbd2f013 [CmdPal] Launch cmd pal with retry (#39039)
* Add Retry when enable

* Add correct for the checking logic

* Retry in another thread (#39042)

* launch thread

* dev

* fix a thread safety

* improve

* improve

* make code clear

* Fix comment

* fix comment

* improve

* self review

* fix & log

* silent fail if not reach 10 times

* fix a ci build flag error

* fix a macro

* some simple improve

---------

Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
2025-04-27 13:47:56 +08:00
Clint Rutkas
ba230eca07 Start progress on AoT. (#39051)
* starting AoT flag push

* Few more

* bookmarks

* Really? The VM project compiles?

* Disable publish AOT before we really testing it.

---------

Co-authored-by: Mike Griese <migrie@microsoft.com>
Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-27 13:44:47 +08:00
Gordon Lam
30df5e0df2 Update the ADO path for tsa.json (#39079)
The previous ADO area path for Powertoys was gone, we need to update to new one.
2025-04-27 08:59:44 +08:00
Yu Leng
9a6c64f9c0 [cmdpal] [AOT] make Clipboard/System/WebSearch/WindowsSettings ext become AOT compatible. (#39080)
Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-25 16:22:50 +08:00
Corey Hayward
7dc2a05c45 [PTRun] Allow preventing usage based ordering results (#37491)
* Allow preventing selected result data retrieval

* Updated implementation to calculate sort order on result and update property name to better reflect purpose

* Update Result.cs sort order method name

Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>

* Align with the name GetSortOrderScore

---------

Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>
Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
2025-04-25 12:27:54 +08:00
Lemonyte
26fe36ab8d Color Picker: add Oklab and Oklch color formats (#38052)
* Resolve Resources.resw conflict

* Update CIE LCh chroma practical upper bound according to CSS spec

* Add review suggestions

* Add WIP tests (lch and oklch do not pass yet)

* Deduplicate Lab to LCh converter method

* Update expect.txt

* Fix liberty test color

* Reimplement oklab with better precision

* Remove CIE LCh

* Add tooltip for color param descriptions

* Update spell-check expect.txt with new words

* Remove 'cielch' and 'lch' from expect.txt

---------

Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
Co-authored-by: Clint Rutkas <clint@rutkas.com>
Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
2025-04-25 10:48:19 +08:00
cryolithic
06b56a10bd 37405 Advanced Paste: Image To Text doesn't work with English (Canada) (#37806)
* [AdvancedPaste] [Fix Bug] Create ocrEngine from user profile language

GetOCRLanguage may fail based on language tag not matching (en-CA does not match en-GB or en-US), however user profile language may be valid.

* Update exception message.

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* update

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

---------

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
2025-04-25 10:45:11 +08:00
Kai Tao
fc804a8156 [Tool] Script to build an installer locally (#39017)
* add script to build a installer

* minor fix

* fix search path for msix file

* fix sign

* fix sign

* fix spelling

* Fix powershell5 can't recognize emoji

* ensure-wix

* bring cmdpal available during local build

* remove early quit

* fix marco

* add logger

* doc

* add a note

* self review

* fix macro def

* add functionality to export cert so that other machine can install it.

* spelling
2025-04-25 09:57:42 +08:00
Mike Griese
f63fcfd91c Add support for filterable, nested context menus (#38776)
_targets #38573_

At first I just wanted to add support for nested context menus.

But then I also had to add a search box, so the focus wouldn't get weird.

End result:

![nested-menus-001](https://github.com/user-attachments/assets/4e8f1ec8-4b09-4095-9b81-caf7abde8aea)

This gets rid of the need to have the search box and the command bar both track item keybindings - now it's just in the command bar.

Closes #38299
Closes #38442
2025-04-24 13:32:07 -05:00
Davide Giacometti
195ff24a85 [Settings] Fix null CmdPal HotKey crash (#39052)
fixed null cmdpal hotkey crash when settings.json not exists or not define hotkey
2025-04-24 18:18:38 +08:00
Yu Leng
5691c5754b [cmdpal] Ref to AotCompatibility in some cmdpal project. (#39061)
* Ref to AotCompatibility

* Typo issue

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-24 16:07:10 +08:00
Shawn Yuan
100d560f9e [CmdPal] Added fallback for time and date (#38918)
* Added fallback for time and date

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* only support week/now/time/year query

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* Add week option

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* Changed setting for time date fallback.

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* update globalization string

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* use week of year.

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* update

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

---------

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
2025-04-24 14:58:01 +08:00
leileizhang
25c29ade8e upgrade the boost dependencies for Fuzzing Project (#39057) 2025-04-24 12:00:03 +08:00
Gordon Lam
8dfa55fe28 Update to WinAppSDK 1.7 latest version (#39016)
* Update to WinAppSDK 1.7 latest version
* Update UpdateVersions.ps1

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
Co-authored-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Clint Rutkas <clint@rutkas.com>
2025-04-24 09:25:47 +08:00
Gordon Lam
6c317c4ee1 Pre-creation of generated folder for any csproj (#39018)
* Test for Precreation of generated folder for any csproj to prevent random build break
2025-04-24 07:09:51 +08:00
Mike Griese
54e058e82d Fully initialize context menus when they change (#38998)
If we don't slow-initialize the whole menu when it changes, then we won't see that there's secondary (& more) commands.  

Tested this with the extension from [waaverecords/CmdPal.Ext.Spotify#4](https://github.com/waaverecords/CmdPal.Ext.Spotify/pull/4)

Closes #38959 


Also seemingly closes #38347 - seems that needed additional bumping of the `EmptyContent`'s
2025-04-23 10:54:01 -05:00
Mike Griese
b0e7473760 CmdPal: Tidy up some winget experiences (#38174)
I'm filing this so that I don't lose it on this machine I use less often. We can probably hold it out of 0.90


Fixes:
* If a package is installed, we always display the version as "Unknown"
  * also deals with a case where getting the package metadata could fail, and we'd hide the list item. That's only possible in the "installed, no updates available" case
* Allow package updates, add an icon for updates
* moves off the preview winget API onto a higher stable version
2025-04-23 04:45:34 -07:00
Niels Laute
f085ba0cd2 [CmdPal] Better support for long labels in empty content and commandbar (#38974)
Closes: #38970

Before:
<img width="605" alt="image" src="https://github.com/user-attachments/assets/8310e08f-c471-4663-9000-bfd1eb8c99f3" />

After:
<img width="514" alt="image" src="https://github.com/user-attachments/assets/b1e4c5f6-cd6b-42b2-9a23-3e1e3642202a" />
2025-04-23 04:40:33 -07:00
Niels Laute
884bfc71d3 [CmdPal] Tweaks to the detailspane UX (#38972)
Details pane UX tweaks. Closes: https://github.com/microsoft/PowerToys/issues/38973

Before:
![image](https://github.com/user-attachments/assets/ba7aca91-31ef-4571-b4ca-0951abe73c21)


After:
![image](https://github.com/user-attachments/assets/c17179b0-4319-4176-bac7-b4ca140bc624)
2025-04-23 04:35:19 -07:00
Davide Giacometti
252cf2670f [CmdPal] Hide commands that shouldn't be visible (#39011)
hide commands from more commands
2025-04-23 17:30:36 +08:00
leileizhang
4be6129835 [Fuzzing] Add PowerRename Fuzzing and C++ Project Setup Guidance (#38922)
* add fuzz

* update solution

* update pipeline

* update solution

* remove arm64

* use reference

* revert the code

* add fuzzing readme

* update solution

* fix spell-check

* Parent reference don't need update

* remove fuzzing config

* add debug config

* update the config
2025-04-23 17:05:29 +08:00
Kai Tao
583614449d [Workspaces]Fix for steam games capture&launch: capture and correctly launch steam games. (#38380)
* Workspaces fix: capture steam games.

* minor fix

* Launch steam apps by url appmodeluserid instead of directly exe call.

* fix copilot comment

* fix

* remove unnecessary string

* expect words

* white list words

* Order of alphabet

* exclude thin frame if it's not a steam game.

* fix build

* fix regression

* adjust comment
2025-04-23 15:39:54 +08:00
Yu Leng
d4e577bb81 [cmdpal] Add setting "ignore hotkey when full screen" (#38923)
* init

* merge main

* merge main

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-23 11:40:20 +08:00
Ionuț Manța
e1ad7e39c6 [PowerAccent] Cancel previous ShowToolbar task if a new one is triggered (#37757)
* Cancel previous ShowToolbar task if a new one is triggered

* more changes

* Fix space trigger starting at center

* Removed unneded code

* More changes

* Addressed feedback

* Fix another edge case
2025-04-23 11:03:05 +08:00
leileizhang
e8b02cd797 [ImageResizer]Fix: Deleting an Image Resizer preset deletes the wrong preset (#38476)
* [ImageResizer]Fix: Deleting an Image Resizer preset deletes the wrong preset

* update the helper

* sort items
2025-04-23 09:35:40 +08:00
Dustin L. Howett
232e1b79bd release: don't publish private symbols for 100 years (#39015) 2025-04-21 23:16:42 -05:00
Gordon Lam
5ec2728dea Fix build break because of miss Signing for new files (#39014)
* Add CmdPalKeyboardService.dll as part of sign
* Move 3rd party signing to another section
2025-04-22 09:18:55 +08:00
Heiko
d314fa075e [RegPreview] Init with header and add NEW button (#37626)
* default value

* add new button

* fix tool tip

* update button symbol

* feedback changes

* spellcheck
2025-04-21 16:11:07 +08:00
Aung Khaing Khant
90723d5b12 [CmdPal] Fixed #38961 (#38988)
* [CmdPal] Fixed #38961

* [CmdPal] Fixed #38961

* [CmdPal] Fix #38961 Wrong Message for "Open Recycle Bin" Command

* [CmdPal] Fix #38961 Wrong Message for "Open Recycle Bin" Command

---------

Co-authored-by: Aung Khaing Khant <aungkhaingkhant@advent-soft.com>
2025-04-21 15:35:47 +08:00
Yu Leng
f2a5505601 [cmdpal] Add fallback item for system command. (#38865)
* init

* Remove unused cache

* merge main

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-21 14:07:43 +08:00
Heiko
311ab88ec3 [CmdPalette > Time and Date] Custom formats (Port #37743) and other plugin improvements - 2 (#38952)
* port changes from broken PR

* fixes

* fix formatting
2025-04-21 13:47:45 +08:00
Niels Laute
c04400e7df [CmdPal] A11y improvements (#38840)
This PR introduces the following changes:

- Adding the right a11y labels - as a result, Accesibility Insights is no longer flagging any errors: https://github.com/microsoft/PowerToys/issues/38395
- Removing and tweaking a few animations, addressing: https://github.com/microsoft/PowerToys/issues/38438
- Localization improvements
2025-04-17 08:51:40 -05:00
Mike Griese
05218e8af6 Add the list item requested shortcuts back (#38573)
* [x] Re-adds the context menu shortcut text
* [x] Hooks up the keybindings to the search box so that you can just press the keys while you have an item selected, and do a context command
* [x] Hook these keybindings up to the context flyout itself
* [x] Adds a sample for testing

Solves #38271
2025-04-17 06:13:11 -05:00
Yu Leng
6cf73ce839 [cmdpal] Port v1 calculator extension (#38629)
* init

* update

* Remove duplicated cp command

* Change the long desc

* Update notice.md

* Use the same icon for fallback item

* Add Rappl to expect list

* update notice.md

* Move the original order back.

* Make Radians become default choice

* Fix empty result

* Remove unused settings.
Move history back.
Refactory the query logic

* fix typo

* merge main

* CmdPal: minor calc updates (#38914)

A bunch of calc updates

* maintain the visibility of the history
* add other formats to the context menu #38708
* some other icon tidying

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
2025-04-17 14:59:10 +08:00
Kai Tao
4f9e829155 [cmdpal] Run cmdpalette from runner locally (#38725)
* empowering users to maximize OOBE to their heart desire (#37823)

empowering users to maximize to their heart desire

* resume main

* Trust selfsign cert in localmachine\root to make msix available

* minor fix

* retry signing

---------

Co-authored-by: Clint Rutkas <clint@rutkas.com>
2025-04-17 11:53:22 +08:00
Ved Nig
397d5cf655 Powertoys for Linear plugin added (#38883)
* Update thirdPartyRunPlugins.md

* Update names.txt

---------

Co-authored-by: Clint Rutkas <clint@rutkas.com>
2025-04-16 15:26:36 -07:00
Mike Griese
01584f33e1 Update the settings form when ext settings are saved (#38851)
This one was subtle - the Settings class in the toolkit didn't ever change the items for a SettingsContentPage. For the main settings window, this was problematic. It would only ever hang onto one instance of that CommandSettings.SettingsContentPage, and never re-retrieve the value from it.

This fixes that issue, by making sure to raise an ItemsChanged in the settings changed handler, so that we automatically pull down the new settings forms.

For settings that were added to commands, as a context item, this wasn't an issue. They were always returning new forms to the host, with the current settings values in it.

Closes #38191
2025-04-16 14:50:56 -05:00
Mike Griese
67463abf98 Immediately select search text on opening page (#38842)
closes #38712
closes #38315
related to #38379 (and might sufficiently close that too)

This immediately selects the search text when a page is loaded.
![38712-select-search-text-000](https://github.com/user-attachments/assets/9db8b455-9afb-4b11-9a30-8ddaa23cdb64)
2025-04-16 14:47:55 -05:00
Mike Griese
e95a570d48 Don't auto-hide when an MSAL dialog is opened on us (#38850)
Apps that want to show MSAL dialogs on the Command Palette would explode if they passed CmdPal's HWND to WithParentActivityOrWindow. It's not entirely clear why, but MSAL would explode if the parent HWND is hidden.

When the MSAL dialog opened, we'd hide ourselves, and badda bing, badda boom, the extension would crash.

MSAL dialogs will set us to WS_DISABLED right before the dialog is opened. Easy solution. Don't hide ourselves, if we're disabled.

Helps some friends not depend on the existence of Teams :P
2025-04-16 14:47:33 -05:00
Heiko
cb27874805 [PowerToysRun] Add version info to plugin error messages (#38491)
* changes

* fix resx
2025-04-16 18:51:20 +00:00
Mike Griese
f65a3fc06f Adds support for JUMBO thumbnails in the helper (#38539)
Adds a parameter to `Toolkit.ThumbnailHelper.GetThumbnail` to retrieve the largest possible icon from the file. For most use cases, the normal icon size will be good for list items and page icons. 

But for details, you'll want to use the JUMBO icons, and to retrieve them, we need to get the icon from a different API. 

As a drive-by, I also have us fetching the highest-res app icon for UWP's rather than the lowest-res icon.

Solves #38238 

Screenshots:
| before | after | 
| ------ | ----- |
| ![image](https://github.com/user-attachments/assets/8aebf163-2f71-45c5-9bee-052ef5528c58) | ![image](https://github.com/user-attachments/assets/7d7b417a-d8d0-4234-ad2b-446a4ca804ba) |
| ![image](https://github.com/user-attachments/assets/3aa21305-2d5f-40a5-a091-fbe5ca5f332c) | ![image](https://github.com/user-attachments/assets/beb5e62f-c649-4cbc-8f6e-8d2c1655cac0) |
2025-04-16 12:04:46 -05:00
Stefan Markovic
c7789abf04 CmdPal: Add low-level keyboard hook for global hotkey (#38250)
Adds the ability to make the global CmdPal hotkey a low-level hook. This is needed for `win+space`, `win+r`, et al. 

I've only added this to the root hotkey. Other hotkeys will still use the normal `RegisterHotkey` ones. We can re-evaluate this for 0.2+.



Originally: https://github.com/zadjii-msft/PowerToys/issues/349

Solves: https://github.com/microsoft/PowerToys/issues/38297

---------

Co-authored-by: Mike Griese <migrie@microsoft.com>
2025-04-16 05:36:01 -05:00
Davide Giacometti
e700f86ace [QuickAccent] Fix on-screen keyboard activation (#37581)
* fix on-screen keyboard activation

* cleanup
2025-04-16 11:08:43 +08:00
Yu Leng
a16f784011 [cmdpal] Fix empty file name issue when create new ext in "Create New Extension" command. (#38864)
Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-16 10:17:43 +08:00
Abhyudit
4e0db267dc [PowerRename] Add 12-hour time format patterns with AM/PM support (#30703) (#38723)
* [PowerRename][Feature] Add new date/time formatting patterns to GetDatedFileName

* [PowerRename][UI] Add date/time shortcut patterns to cheat sheet

* [PowerRename][Tests] Add tests for new date/time formatting patterns

* [PowerRename] [Refactor] Simplify AM/PM string handling in time patterns
2025-04-16 10:15:03 +08:00
Carlos Zamora
0e98cbd57e [CNF] Only enable experimental features if they exist (#37690) 2025-04-16 10:14:23 +08:00
Clint Rutkas
f15bed5323 upgrading toolkit + suppressing warnings (#38746)
* upgrade toolkit to latest

* supressing warnings

* Update expect.txt

making generic

* Update suppression comments for AOT compatibility

* Fix case for 'MVVMTK' in spell-check file

* Update NOTICE.md
2025-04-15 11:58:33 -07:00
Clint Rutkas
d3c4e808d0 Upgrading Boost dependencies (#38782)
Upgrading Boost
2025-04-15 11:57:58 -07:00
Mike Griese
2b5181b4c9 Rename the [Ee]xts dir to ext (#38852)
**WARNING:** This PR will probably blow up all in-flight PRs

at some point in the early days of CmdPal, two of us created seperate
`Exts` and `exts` dirs. Depending on what the casing was on the branch
that you checked one of those out from, it'd get stuck like that on your
PC forever.

Windows didn't care, so we never noticed.

But GitHub does care, and now browsing the source on GitHub is basically
impossible.

Closes #38081
2025-04-15 06:07:22 -05:00
Typpi
60f50d853b Apply security best practices for GitHub Actions / Dependency (#38552)
This update aligns with Microsoft's security guidelines by pinning all GitHub Action tags and Docker tags to their full-length commits. This practice ensures immutability and reduces the risk of supply chain attacks. Note that 1st and 2nd party actions do not require hash pinning.
2025-04-15 16:33:05 +08:00
Hao Liu
ed8b9c6ade Merge branch 'main' into feature/KeyboardManagerWinUI3 2025-04-10 10:56:41 +08:00
Hao Liu
c5635c1e3e [Keyboard Manager] Functional Improvements on new KBM UX (#38652)
* Make Disable toggle button functional in Shortcuts page

* Clean up main window and aggregate Constants

* Update key remapping naming

* Fix the key visual stretch out issue when the shortcut contains too many keys

* Refactor code structure

* fix wrapping in dialog

* update naming

* Implement Keyboard hook to get the user input of remappings

* Implement Reset for InputControl to make sure InputControl can have correct status for various dialog
2025-04-09 10:30:25 +08:00
Hao Liu
c641fd17d2 Merge branch 'main' into feature/KeyboardManagerWinUI3 2025-03-12 12:25:10 +08:00
Hao Liu
4f4bcbfb53 Merge the new WinUI3 Keyboard Manager FHL change into feature branch 2025-03-11 17:04:54 +08:00
Niels Laute
85dea93a50 Design tweaks and XAML refactoring 2025-03-10 11:11:28 +01:00
chenmy77
e184808068 [FHL] Add Select Key UI in Inputcontrol (#37788)
* add press button

* add right press button

* add right press button by original button

* change toggle button position

* fix type error

* merge

* add link

* brush blue in new keys

* remove unuse annotations

* remove unuse keyvisual
2025-03-06 20:48:55 +08:00
Hao Liu
e52ac85a1b [FHL] Save Remapping Config and Data (#37787)
* load URL remapping

* Save Remapping Config and Data

* update mock data for testing

* fix app mapping

* update xaml
2025-03-06 17:56:13 +08:00
Hao Liu
1e3108efbc [FHL] Load Remapping Config and Data for Keyboard Manager new UX (#37762)
* Load remapping config and data

* display single key to shortcuts mapping properly

* get remapping with operation type so we can display them in different tabs

* clean up the xaml
2025-03-05 18:48:16 +08:00
Hao Liu
f7ed043446 [FHL] New static UX for Keyboard Manager Editor (#37747)
* Set up static new UX for Keyboard Manager Editor

* Improve the formatting and performance

* fix spelling
2025-03-04 19:09:28 +08:00
Hao Liu
d90215ee8b [FHL] Set up dev UX for Keyboard Manager Editor (#37730)
* Implement a dev version ui which is copy of current WinUI2 UX

* Implement WinUI3 dropdown and P/Invoke the key list from existing C++ Library

* Update Dllimport path
2025-03-03 10:55:44 +08:00
Hao Liu
b2dae5b48e Initial commit: Have the WinUI3 Editor up and run 2025-02-27 15:42:30 +08:00
614 changed files with 13040 additions and 1926 deletions

View File

@@ -273,4 +273,4 @@ mengyuanchen
testhost
#Tools
OIP
OIP

View File

@@ -189,6 +189,7 @@ Zoltan
Zykova
Sameerjs
ruslanlap
vednig
# OTHERS

View File

@@ -8,6 +8,7 @@ Acceleratorkeys
ACCEPTFILES
ACCESSDENIED
ACCESSTOKEN
acfs
AClient
AColumn
acrt
@@ -173,6 +174,7 @@ CContext
CDeclaration
CElems
CENTERALIGN
cer
certlm
certmgr
cfp
@@ -196,6 +198,7 @@ CLIPBOARDUPDATE
CLIPCHILDREN
CLIPSIBLINGS
closesocket
clp
CLSCTX
clsids
Clusion
@@ -272,6 +275,7 @@ CURSORINFO
cursorpos
customaction
CUSTOMACTIONTEST
CUSTOMFORMATPLACEHOLDER
CVal
cvd
CVirtual
@@ -343,6 +347,7 @@ DEVMODE
DEVMODEW
devpal
DIALOGEX
digicert
dimm
DISABLEASACTIONKEY
DISABLENOSCROLL
@@ -515,11 +520,13 @@ FRAMECHANGED
frm
Froml
FROMTOUCH
fsanitize
fsmgmt
FZE
gacutil
Gaeilge
Gaidhlig
gameid
GC'ed
GCLP
gdi
@@ -626,6 +633,7 @@ HOTKEYF
hotkeys
hotlight
hotspot
Hostx
HPAINTBUFFER
HRAWINPUT
HREDRAW
@@ -678,6 +686,7 @@ IGo
iid
Iindex
Ijwhost
ILD
IMAGEHLP
IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT
@@ -708,6 +717,7 @@ INPUTSINK
INPUTTYPE
INSTALLDESKTOPSHORTCUT
INSTALLDIR
installdir
INSTALLFOLDER
INSTALLFOLDERTOBOOTSTRAPPERINSTALLFOLDER
INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
@@ -938,6 +948,7 @@ mpmc
MRM
MRT
mru
MSAL
msc
mscorlib
msctls
@@ -1001,6 +1012,7 @@ netsh
newcolor
NEWDIALOGSTYLE
NEWFILE
NEWFILEHEADER
newitem
newpath
newplus
@@ -1034,6 +1046,7 @@ NOINHERITLAYOUT
NOINTERFACE
NOINVERT
NOLINKINFO
nologo
NOMCX
NOMINMAX
NOMIRRORBITMAP
@@ -1071,6 +1084,7 @@ NOTRACK
NOTSRCCOPY
NOTSRCERASE
NOTXORPEN
notwindows
NOZORDER
NPH
npmjs
@@ -1265,6 +1279,7 @@ pstm
PStr
pstream
pstrm
pswd
PSYSTEM
psz
ptb
@@ -1295,6 +1310,7 @@ QUNS
QXZ
RAII
RAlt
Rappl
randi
Rasterization
Rasterize
@@ -1391,6 +1407,7 @@ sacl
safeprojectname
SAMEKEYPREVIOUSLYMAPPED
SAMESHORTCUTPREVIOUSLYMAPPED
sancov
SAVEFAILED
scanled
schedtasks
@@ -1409,6 +1426,7 @@ searchterm
SEARCHUI
SECONDARYDISPLAY
secpol
securestring
SEEMASKINVOKEIDLIST
SELCHANGE
SENDCHANGE
@@ -1453,6 +1471,9 @@ SHELLDLL
shellex
SHELLEXECUTEINFO
SHELLEXECUTEINFOW
SHELLEXTENSION
SHELLICONSIZE
SHELLNEWVALUE
SHFILEINFO
SHFILEOPSTRUCT
SHGDN
@@ -1460,6 +1481,7 @@ SHGDNF
SHGFI
SHGFIICON
SHGFILARGEICON
SHIL
shinfo
shlwapi
shobjidl
@@ -1491,6 +1513,7 @@ SIDs
siex
sigdn
SIGNINGSCENARIO
signtool
Signtool
SINGLEKEY
sipolicy
@@ -1557,6 +1580,7 @@ stdcpp
stdcpplatest
STDMETHODCALLTYPE
STDMETHODIMP
steamapps
STGC
STGM
STGMEDIUM
@@ -1638,6 +1662,7 @@ telephon
templatenamespace
testprocess
TEXCOORD
TEXTBOXNEWLINE
TEXTEXTRACTOR
TEXTINCLUDE
tfopen
@@ -1919,6 +1944,7 @@ WUX
Wwanpp
XAxis
xclip
xcopy
XDocument
XElement
xfd
@@ -1957,3 +1983,11 @@ ZOOMITX
ZXk
ZXNs
zzz
ACIE
AOklab
BCIE
BOklab
culori
Evercoder
LCh
CIELCh

26
.github/workflows/dependency-review.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request,
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
# Once installed, if the workflow run is marked as required,
# PRs introducing known-vulnerable packages will be blocked from merging.
#
# As recommended by Microsoft's security guidelines (https://docs.opensource.microsoft.com/security/tsg/actions/#requirements-for-security-hardening-your-own-github-actions),
# 3rd-party actions should be pinned to a specific commit hash to prevent supply chain attacks.
# This update aligns with best practices; 1st/2nd-party actions is not required hash pinning.
#
# Source repository: https://github.com/actions/dependency-review-action
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4

View File

@@ -93,7 +93,7 @@ jobs:
steps:
- name: check-spelling
id: spelling
uses: check-spelling/check-spelling@v0.0.24
uses: check-spelling/check-spelling@67debf50669c7fc76fc8f5d7f996384535a72b77 # v0.0.24
with:
config: .github/actions/spell-check
suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }}
@@ -156,7 +156,7 @@ jobs:
if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push'
steps:
- name: comment
uses: check-spelling/check-spelling@v0.0.24
uses: check-spelling/check-spelling@67debf50669c7fc76fc8f5d7f996384535a72b77 # v0.0.24
with:
config: .github/actions/spell-check
checkout: true
@@ -175,7 +175,7 @@ jobs:
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
steps:
- name: comment
uses: check-spelling/check-spelling@v0.0.24
uses: check-spelling/check-spelling@67debf50669c7fc76fc8f5d7f996384535a72b77 # v0.0.24
with:
config: .github/actions/spell-check
checkout: true
@@ -202,7 +202,7 @@ jobs:
cancel-in-progress: false
steps:
- name: apply spelling updates
uses: check-spelling/check-spelling@v0.0.24
uses: check-spelling/check-spelling@67debf50669c7fc76fc8f5d7f996384535a72b77 # v0.0.24
with:
experimental_apply_changes_via_bot: ${{ github.repository_owner != 'microsoft' && 1 }}
checkout: true

View File

@@ -127,6 +127,7 @@
"PowerToys.KeyboardManager.dll",
"KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe",
"KeyboardManagerEditorUI\\PowerToys.KeyboardManagerEditorUI.exe",
"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe",
"PowerToys.KeyboardManagerEditorLibraryWrapper.dll",
@@ -220,6 +221,7 @@
"WinUI3Apps\\PowerToys.Settings.exe",
"PowerToys.CmdPalModuleInterface.dll",
"CmdPalKeyboardService.dll",
"*Microsoft.CmdPal.UI_*.msix"
],
"SigningInfo": {
@@ -330,6 +332,8 @@
"TestableIO.System.IO.Abstractions.Wrappers.dll",
"WinUI3Apps\\TestableIO.System.IO.Abstractions.Wrappers.dll",
"WinUI3Apps\\OpenAI.dll",
"Testably.Abstractions.FileSystem.Interface.dll",
"WinUI3Apps\\Testably.Abstractions.FileSystem.Interface.dll",
"ColorCode.Core.dll",
"ColorCode.UWP.dll",
"UnitsNet.dll",

View File

@@ -1,16 +1,24 @@
Param(
# Using the default value of 1.6 for winAppSdkVersionNumber and useExperimentalVersion as false
# Using the default value of 1.7 for winAppSdkVersionNumber and useExperimentalVersion as false
[Parameter(Mandatory=$False,Position=1)]
[string]$winAppSdkVersionNumber = "1.6",
[string]$winAppSdkVersionNumber = "1.7",
# When the pipeline calls the PS1 file, the passed parameters are converted to string type
[Parameter(Mandatory=$False,Position=2)]
[boolean]$useExperimentalVersion = $False
[boolean]$useExperimentalVersion = $False,
# Root folder Path for processing
[Parameter(Mandatory=$False,Position=3)]
[string]$rootPath = $(Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)),
# Root folder Path for processing
[Parameter(Mandatory=$False,Position=4)]
[string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
)
function Update-NugetConfig {
param (
[string]$filePath = "nuget.config"
[string]$filePath = [System.IO.Path]::Combine($rootPath, "nuget.config")
)
Write-Host "Updating nuget.config file"
@@ -35,7 +43,33 @@ function Update-NugetConfig {
$xml.Save($filePath)
}
$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
function Read-FileWithEncoding {
param (
[string]$Path
)
$reader = New-Object System.IO.StreamReader($Path, $true) # auto-detect encoding
$content = $reader.ReadToEnd()
$encoding = $reader.CurrentEncoding
$reader.Close()
return [PSCustomObject]@{
Content = $content
Encoding = $encoding
}
}
function Write-FileWithEncoding {
param (
[string]$Path,
[string]$Content,
[System.Text.Encoding]$Encoding
)
$writer = New-Object System.IO.StreamWriter($Path, $false, $Encoding)
$writer.Write($Content)
$writer.Close()
}
# Execute nuget list and capture the output
if ($useExperimentalVersion) {
@@ -79,50 +113,54 @@ if ($latestVersion) {
}
# Update packages.config files
Get-ChildItem -Recurse packages.config | ForEach-Object {
$content = Get-Content $_.FullName -Raw
Get-ChildItem -Path $rootPath -Recurse packages.config | ForEach-Object {
$file = Read-FileWithEncoding -Path $_.FullName
$content = $file.Content
if ($content -match 'package id="Microsoft.WindowsAppSDK"') {
$newVersionString = 'package id="Microsoft.WindowsAppSDK" version="' + $WinAppSDKVersion + '"'
$oldVersionString = 'package id="Microsoft.WindowsAppSDK" version="[-.0-9a-zA-Z]*"'
$content = $content -replace $oldVersionString, $newVersionString
Set-Content -Path $_.FullName -Value $content
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
# Update Directory.Packages.props file
$propsFile = "Directory.Packages.props"
$propsFile = [System.IO.Path]::Combine($rootPath,"Directory.Packages.props")
if (Test-Path $propsFile) {
$content = Get-Content $propsFile -Raw
$file = Read-FileWithEncoding -Path $propsFile
$content = $file.Content
if ($content -match '<PackageVersion Include="Microsoft.WindowsAppSDK"') {
$newVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="' + $WinAppSDKVersion + '" />'
$oldVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*" />'
$content = $content -replace $oldVersionString, $newVersionString
Set-Content -Path $propsFile -Value $content
Write-FileWithEncoding -Path $propsFile -Content $content -Encoding $file.encoding
Write-Host "Modified " $propsFile
}
}
# Update .vcxproj files
Get-ChildItem -Recurse *.vcxproj | ForEach-Object {
$content = Get-Content $_.FullName -Raw
Get-ChildItem -Path $rootPath -Recurse *.vcxproj | ForEach-Object {
$file = Read-FileWithEncoding -Path $_.FullName
$content = $file.Content
if ($content -match '\\Microsoft.WindowsAppSDK.') {
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion + '\'
$oldVersionString = '\\Microsoft.WindowsAppSDK.[-.0-9a-zA-Z]*\\'
$content = $content -replace $oldVersionString, $newVersionString
Set-Content -Path $_.FullName -Value $content
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
# Update .csproj files
Get-ChildItem -Recurse *.csproj | ForEach-Object {
$content = Get-Content $_.FullName -Raw
Get-ChildItem -Path $rootPath -Recurse *.csproj | ForEach-Object {
$file = Read-FileWithEncoding -Path $_.FullName
$content = $file.Content
if ($content -match 'PackageReference Include="Microsoft.WindowsAppSDK"') {
$newVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="'+ $WinAppSDKVersion + '"'
$oldVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*"'
$content = $content -replace $oldVersionString, $newVersionString
Set-Content -Path $_.FullName -Value $content
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}

View File

@@ -3,5 +3,5 @@
"notificationAliases": ["powertoys@microsoft.com"],
"instanceUrl": "https://microsoft.visualstudio.com",
"projectName": "OS",
"areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys"
"areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\DIVE\\PowerToys"
}

View File

@@ -33,7 +33,7 @@ parameters:
default: true
- name: winAppSDKVersionNumber
type: string
default: 1.6
default: 1.7
- name: useExperimentalVersion
type: boolean
default: false

View File

@@ -148,5 +148,7 @@ extends:
parameters:
versionNumber: ${{ parameters.versionNumber }}
includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }}
${{ if ne(parameters.publishSymbolsToPublic, true) }}:
symbolExpiryTime: 10 # For private builds, expire symbols within 10 days. The default is 100 years.
subscription: $(SymbolPublishingServiceConnection)
symbolProject: $(SymbolPublishingProject)

View File

@@ -17,6 +17,7 @@ steps:
arguments: >
-winAppSdkVersionNumber ${{ parameters.versionNumber }}
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
-rootPath "$(build.sourcesdirectory)"
- script: echo $(WinAppSDKVersion)
displayName: 'Display WinAppSDK Version Found'

View File

@@ -10,14 +10,14 @@
<PackageVersion Include="Azure.AI.OpenAI" Version="1.0.0-beta.17" />
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.250129-preview2" />
<PackageVersion Include="CommunityToolkit.WinUI.Collections" Version="8.2.250129-preview2" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250129-preview2" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250129-preview2" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250129-preview2" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.250129-preview2" />
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250129-preview2" />
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250129-preview2" />
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Collections" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.Markdown" Version="7.1.2" />
<PackageVersion Include="ControlzEx" Version="6.0.0" />
@@ -45,7 +45,7 @@
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.4" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.120-preview" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.4" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
<!-- CsWinRT version needs to be set to have a WinRT.Runtime.dll at the same version contained inside the NET SDK we're currently building on CI. -->
@@ -55,7 +55,7 @@
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.6.250205002" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250401001" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />

View File

@@ -75,6 +75,40 @@ OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
```
## Utility: Command Palette Built-in Extensions
### Calculator
#### Mages
We use the Mages NuGet package for calculating the result of expression.
**Source**: [https://github.com/FlorianRappl/Mages](https://github.com/FlorianRappl/Mages)
```
The MIT License (MIT)
Copyright (c) 2016 - 2025 Florian Rappl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## Utility: File Explorer Add-ins
### Monaco Editor
@@ -1400,14 +1434,14 @@ SOFTWARE.
- Azure.AI.OpenAI 1.0.0-beta.17
- CommunityToolkit.Common 8.4.0
- CommunityToolkit.Mvvm 8.4.0
- CommunityToolkit.WinUI.Animations 8.2.250129-preview2
- CommunityToolkit.WinUI.Collections 8.2.250129-preview2
- CommunityToolkit.WinUI.Controls.Primitives 8.2.250129-preview2
- CommunityToolkit.WinUI.Controls.Segmented 8.2.250129-preview2
- CommunityToolkit.WinUI.Controls.SettingsControls 8.2.250129-preview2
- CommunityToolkit.WinUI.Controls.Sizers 8.2.250129-preview2
- CommunityToolkit.WinUI.Converters 8.2.250129-preview2
- CommunityToolkit.WinUI.Extensions 8.2.250129-preview2
- CommunityToolkit.WinUI.Animations 8.2.250402
- CommunityToolkit.WinUI.Collections 8.2.250402
- CommunityToolkit.WinUI.Controls.Primitives 8.2.250402
- CommunityToolkit.WinUI.Controls.Segmented 8.2.250402
- CommunityToolkit.WinUI.Controls.SettingsControls 8.2.250402
- CommunityToolkit.WinUI.Controls.Sizers 8.2.250402
- CommunityToolkit.WinUI.Converters 8.2.250402
- CommunityToolkit.WinUI.Extensions 8.2.250402
- CommunityToolkit.WinUI.UI.Controls.DataGrid 7.1.2
- CommunityToolkit.WinUI.UI.Controls.Markdown 7.1.2
- ControlzEx 6.0.0
@@ -1438,8 +1472,8 @@ SOFTWARE.
- Microsoft.Windows.CsWin32 0.2.46-beta
- Microsoft.Windows.CsWinRT 2.2.0
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
- Microsoft.WindowsAppSDK 1.6.250205002
- Microsoft.WindowsPackageManager.ComInterop 1.10.120-preview
- Microsoft.WindowsAppSDK 1.7.250401001
- Microsoft.WindowsPackageManager.ComInterop 1.10.340
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
- ModernWpfUI 0.9.4

View File

@@ -624,19 +624,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CommandPalette", "CommandPa
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Built-in Extensions", "Built-in Extensions", "{ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Apps", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj", "{6CE438DF-C245-4997-A360-0A0939E4BA34}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Apps", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj", "{6CE438DF-C245-4997-A360-0A0939E4BA34}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Bookmarks", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj", "{E09AA983-C755-474F-83D6-A5CDF528C070}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Bookmarks", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj", "{E09AA983-C755-474F-83D6-A5CDF528C070}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Calc", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj", "{6D56B64D-FF1F-488F-AFED-9B9854A5D399}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Calc", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj", "{6D56B64D-FF1F-488F-AFED-9B9854A5D399}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Registry", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj", "{92EC89E4-9972-453A-8A1A-3A9E230C146A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Registry", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj", "{92EC89E4-9972-453A-8A1A-3A9E230C146A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsServices", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowsServices\Microsoft.CmdPal.Ext.WindowsServices.csproj", "{51939B4F-1F62-4BFF-A6A2-C08646E5BE95}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsServices", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowsServices\Microsoft.CmdPal.Ext.WindowsServices.csproj", "{51939B4F-1F62-4BFF-A6A2-C08646E5BE95}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsSettings", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowsSettings\Microsoft.CmdPal.Ext.WindowsSettings.csproj", "{D1160404-D3D1-497A-883A-4059C07C2273}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsSettings", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowsSettings\Microsoft.CmdPal.Ext.WindowsSettings.csproj", "{D1160404-D3D1-497A-883A-4059C07C2273}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsTerminal", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowsTerminal\Microsoft.CmdPal.Ext.WindowsTerminal.csproj", "{40F6D69D-E321-400F-A767-5628C7AE453D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsTerminal", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowsTerminal\Microsoft.CmdPal.Ext.WindowsTerminal.csproj", "{40F6D69D-E321-400F-A767-5628C7AE453D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extension SDK", "Extension SDK", "{F3D09629-59A2-4924-A4B9-D6BFAA2C1B49}"
EndProject
@@ -650,9 +650,9 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.UI", "sr
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sample Extensions", "Sample Extensions", "{071E18A4-A530-46B8-AB7D-B862EE55E24E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProcessMonitorExtension", "src\modules\cmdpal\Exts\ProcessMonitorExtension\ProcessMonitorExtension.csproj", "{C846F7A7-792A-47D9-B0CB-417C900EE03D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProcessMonitorExtension", "src\modules\cmdpal\ext\ProcessMonitorExtension\ProcessMonitorExtension.csproj", "{C846F7A7-792A-47D9-B0CB-417C900EE03D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SamplePagesExtension", "src\modules\cmdpal\Exts\SamplePagesExtension\SamplePagesExtension.csproj", "{C831231F-891C-4572-9694-45062534B42A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SamplePagesExtension", "src\modules\cmdpal\ext\SamplePagesExtension\SamplePagesExtension.csproj", "{C831231F-891C-4572-9694-45062534B42A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UI", "UI", "{7520A2FE-00A2-49B8-83ED-DB216E874C04}"
EndProject
@@ -660,7 +660,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI", "src\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI.ViewModels", "src\modules\cmdpal\Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj", "{C66020D1-CB10-4CF7-8715-84C97FD5E5E2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.ClipboardHistory", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj", "{79775343-7A3D-445D-9104-3DD5B2893DF9}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.ClipboardHistory", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj", "{79775343-7A3D-445D-9104-3DD5B2893DF9}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalModuleInterface", "src\modules\cmdpal\CmdPalModuleInterface\CmdPalModuleInterface.vcxproj", "{0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}"
EndProject
@@ -668,15 +668,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkspacesCsharpLibrary", "
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NewPlus.ShellExtension.win10", "src\modules\NewPlus\NewShellExtensionContextMenu.win10\NewPlus.ShellExtension.win10.vcxproj", "{0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Indexer", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj", "{453CBB73-A3CB-4D0B-8D24-6940B86FE21D}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Indexer", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj", "{453CBB73-A3CB-4D0B-8D24-6940B86FE21D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Shell", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj", "{C0CE3B5E-16D3-495D-B335-CA791B660162}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Shell", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj", "{C0CE3B5E-16D3-495D-B335-CA791B660162}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowWalker", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowWalker\Microsoft.CmdPal.Ext.WindowWalker.csproj", "{3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowWalker", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowWalker\Microsoft.CmdPal.Ext.WindowWalker.csproj", "{3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSearch", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj", "{605E914B-7232-4789-AF46-BF5D3DDFC14E}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSearch", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj", "{605E914B-7232-4789-AF46-BF5D3DDFC14E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WinGet", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WinGet\Microsoft.CmdPal.Ext.WinGet.csproj", "{E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WinGet", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WinGet\Microsoft.CmdPal.Ext.WinGet.csproj", "{E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.UnitTests", "src\modules\AdvancedPaste\AdvancedPaste.UnitTests\AdvancedPaste.UnitTests.csproj", "{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}"
EndProject
@@ -690,7 +690,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItModuleInterface", "sr
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItSettingsInterop", "src\modules\ZoomIt\ZoomItSettingsInterop\ZoomItSettingsInterop.vcxproj", "{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.TimeDate", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj", "{DCC6BD67-17BB-47AA-B507-FB0FE43A7449}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.TimeDate", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj", "{DCC6BD67-17BB-47AA-B507-FB0FE43A7449}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITestAutomation", "src\common\UITestAutomation\UITestAutomation.csproj", "{A558C25D-2007-498E-8B6F-43405AFAE9E2}"
EndProject
@@ -704,7 +704,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hosts.UITests", "src\module
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegistryPreview.FuzzTests", "src\modules\registrypreview\RegistryPreview.FuzzTests\RegistryPreview.FuzzTests.csproj", "{5702B3CC-8575-48D5-83D8-15BB42269CD3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj", "{64B88F02-CD88-4ED8-9624-989A800230F9}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj", "{64B88F02-CD88-4ED8-9624-989A800230F9}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalKeyboardService", "src\modules\cmdpal\CmdPalKeyboardService\CmdPalKeyboardService.vcxproj", "{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRename.FuzzingTest", "src\modules\powerrename\PowerRename.FuzzingTest\PowerRename.FuzzingTest.vcxproj", "{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -2574,14 +2578,24 @@ Global
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|ARM64.Build.0 = Debug|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x64.ActiveCfg = Debug|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x64.Build.0 = Debug|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x86.ActiveCfg = Debug|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x86.Build.0 = Debug|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.ActiveCfg = Release|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.Build.0 = Release|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.ActiveCfg = Release|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.Build.0 = Release|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x86.ActiveCfg = Release|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x86.Build.0 = Release|x64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|ARM64.ActiveCfg = Debug|ARM64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|ARM64.Build.0 = Debug|ARM64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|x64.ActiveCfg = Debug|x64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|x64.Build.0 = Debug|x64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|ARM64.ActiveCfg = Release|ARM64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|ARM64.Build.0 = Release|ARM64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.ActiveCfg = Release|x64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.Build.0 = Release|x64
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|ARM64.ActiveCfg = Debug|ARM64
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|x64.ActiveCfg = Debug|x64
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|x64.Build.0 = Debug|x64
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|ARM64.ActiveCfg = Release|ARM64
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|x64.ActiveCfg = Release|x64
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2852,6 +2866,8 @@ Global
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
{5702B3CC-8575-48D5-83D8-15BB42269CD3} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
{64B88F02-CD88-4ED8-9624-989A800230F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2} = {3846508C-77EB-4034-A702-F8BB263C4F79}
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -0,0 +1,117 @@
# 🧪 C++ Project Fuzzing Test Guide
This guide walks you through setting up a **fuzzing test** project for a C++ module using [libFuzzer](https://llvm.org/docs/LibFuzzer.html).
.
---
## 🏗️ Step-by-Step Setup
### 1. Create a New C++ Project
- Use **Empty Project** template.
- Name it `<ModuleName>.FuzzingTest`.
---
### 2. Update Build Configuration
- In **Configuration Manager**, Uncheck Build for both Release|ARM64, Debug|ARM64 and Debug|x64 configurations.
- Note: ARM64 is not supported in this case, so leave ARM64 configurations build disabled.
---
### 3. Enable ASan and libFuzzer in `.vcxproj`
Edit the project file to enable fuzzing:
```xml
<PropertyGroup>
<EnableASAN>true</EnableASAN>
<EnableFuzzer>true</EnableFuzzer>
</PropertyGroup>
```
---
### 4. Add Fuzzing Compiler Flags
Add this to `AdditionalOptions` under the `Fuzzing` configuration:
```xml
/fsanitize=address
/fsanitize-coverage=inline-8bit-counters
/fsanitize-coverage=edge
/fsanitize-coverage=trace-cmp
/fsanitize-coverage=trace-div
%(AdditionalOptions)
```
---
### 5. Link the Sanitizer Coverage Runtime
In `Linker → Input → Additional Dependencies`, add:
```text
$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib
```
---
### 6. Copy Required Runtime DLL
Add a `PostBuildEvent` to copy the ASAN DLL:
```xml
<Command>
xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)"
</Command>
```
---
### 7. Add Preprocessor Definitions
To avoid annotation issues, add these to the `Preprocessor Definitions`:
```text
_DISABLE_VECTOR_ANNOTATION;_DISABLE_STRING_ANNOTATION
```
---
## 🧬 Required Code
### `LLVMFuzzerTestOneInput` Entry Point
Every fuzzing project must expose this function:
```cpp
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
std::string input(reinterpret_cast<const char*>(data), size);
try
{
// Call your module with the input here.
}
catch (...) {}
return 0;
}
```
---
## ⚙️ [Test run in the cloud](https://eng.ms/docs/cloud-ai-platform/azure-edge-platform-aep/aep-security/epsf-edge-and-platform-security-fundamentals/the-onefuzz-service/onefuzz/faq/notwindows/walkthrough)
To submit a job to the cloud you can run with this command:
```
oip submit --config .\OneFuzzConfig.json --drop-path <your_submission_directory> --platform windows --do-not-file-bugs --duration 1
```
You want to run with --do-not-file-bugs because if there is an issue with running the parser in the cloud (which is very possible), you don't want bugs to be created if there is an issue. The --duration task is the number of hours you want the task to run. I recommend just running for 1 hour to make sure things work initially. If you don't specify this parameter, it will default to 48 hours. You can find more about submitting a test job here.
OneFuzz will send you an email when the job has started.
---

View File

@@ -68,3 +68,4 @@ Below are community created plugins that target a website or software. They are
| [Bilibili](https://github.com/Whuihuan/PowerToysRun-Bilibili) | [Whuihuan](https://github.com/Whuihuan) | Use AVID or BVID to parse and jump to Bilibili |
| [YubicoOauthOTP](https://github.com/dlnilsson/Community.PowerToys.Run.Plugin.YubicoOauthOTP) | [dlnilsson](https://github.com/dlnilsson) | Display generated codes from OATH accounts stored on the YubiKey in powerToys Run |
| [Firefox Bookmark](https://github.com/8LWXpg/PowerToysRun-FirefoxBookmark) | [8LWXpg](https://github.com/8LWXpg) | Open bookmarks in Firefox based browser |
[Linear](https://github.com/vednig/powertoys-linear) | [vednig](https://github.com/vednig) | Create Linear Issues directly from Powertoys Run |

View File

@@ -79,10 +79,7 @@
<ComponentGroupRef Id="ToolComponentGroup" />
<ComponentGroupRef Id="MonacoSRCHeatGenerated" />
<ComponentGroupRef Id="WorkspacesComponentGroup" />
<?if $(var.CIBuild) = "true" ?>
<ComponentGroupRef Id="CmdPalComponentGroup" />
<?endif?>
<ComponentGroupRef Id="CmdPalComponentGroup" />
</Feature>
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />

View File

@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Some items may be set in Directory.Build.props in root -->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project=".\Common.Dotnet.PrepareGeneratedFolder.targets" />
<PropertyGroup>
<WindowsSdkPackageVersion>10.0.22621.57</WindowsSdkPackageVersion>
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework>

View File

@@ -0,0 +1,16 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="EnsureGeneratedBaseFolder" BeforeTargets="XamlPreCompile">
<PropertyGroup>
<!-- Only create the base 'generated' folder -->
<CompilerGeneratedFilesOutputPath>$(ProjectDir)obj\g</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<!-- Create 'generated' folder if missing -->
<MakeDir Directories="$(CompilerGeneratedFilesOutputPath)" />
<!-- Optional logging for debugging -->
<Message Text="Ensured: $(GeneratedBasePath)" Importance="Low" />
</Target>
</Project>

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -63,8 +63,16 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "<Dotnet port with style preservation>", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "<Dotnet port with style preservation>", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")]
// AOT
// AOT MVVMTK0045
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "AdvancedPaste.ViewModels")]
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "HostsUILib")]
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "EnvironmentVariablesUILib")]
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "Peek.FilePreviewer")]
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "Peek.UI")]
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "Peek.UI.Views")]
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib")]
// AOT MVVMTK0049
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0049:Using [INotifyPropertyChanged] is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "Peek.FilePreviewer")]
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "MVVMTK0049:Using [INotifyPropertyChanged] is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "type", Target = "~T:Peek.UI.Views.TitleBar")]
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0049:Using [INotifyPropertyChanged] is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib")]

View File

@@ -141,6 +141,40 @@ namespace ManagedCommon
return lab;
}
/// <summary>
/// Convert a given <see cref="Color"/> to a Oklab color
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]</returns>
public static (double Lightness, double ChromaticityA, double ChromaticityB) ConvertToOklabColor(Color color)
{
var linear = ConvertSRGBToLinearRGB(color.R / 255d, color.G / 255d, color.B / 255d);
var oklab = GetOklabColorFromLinearRGB(linear.R, linear.G, linear.B);
return oklab;
}
/// <summary>
/// Convert a given <see cref="Color"/> to a Oklch color
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]</returns>
public static (double Lightness, double Chroma, double Hue) ConvertToOklchColor(Color color)
{
var oklab = ConvertToOklabColor(color);
var oklch = GetOklchColorFromOklab(oklab.Lightness, oklab.ChromaticityA, oklab.ChromaticityB);
return oklch;
}
public static (double R, double G, double B) ConvertSRGBToLinearRGB(double r, double g, double b)
{
// inverse companding, gamma correction must be undone
double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
return (rLinear, gLinear, bLinear);
}
/// <summary>
/// Convert a given <see cref="Color"/> to a CIE XYZ color (XYZ)
/// The constants of the formula matches this Wikipedia page, but at a higher precision:
@@ -156,10 +190,7 @@ namespace ManagedCommon
double g = color.G / 255d;
double b = color.B / 255d;
// inverse companding, gamma correction must be undone
double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
(double rLinear, double gLinear, double bLinear) = ConvertSRGBToLinearRGB(r, g, b);
return (
(rLinear * 0.41239079926595948) + (gLinear * 0.35758433938387796) + (bLinear * 0.18048078840183429),
@@ -210,6 +241,63 @@ namespace ManagedCommon
return (l, a, b);
}
/// <summary>
/// Convert a linear RGB color <see cref="double"/> to an Oklab color.
/// The constants of this formula come from https://github.com/Evercoder/culori/blob/2bedb8f0507116e75f844a705d0b45cf279b15d0/src/oklab/convertLrgbToOklab.js
/// and the implementation is based on https://bottosson.github.io/posts/oklab/
/// </summary>
/// <param name="r">Linear R value</param>
/// <param name="g">Linear G value</param>
/// <param name="b">Linear B value</param>
/// <returns>The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]</returns>
private static (double Lightness, double ChromaticityA, double ChromaticityB)
GetOklabColorFromLinearRGB(double r, double g, double b)
{
double l = (0.41222147079999993 * r) + (0.5363325363 * g) + (0.0514459929 * b);
double m = (0.2119034981999999 * r) + (0.6806995450999999 * g) + (0.1073969566 * b);
double s = (0.08830246189999998 * r) + (0.2817188376 * g) + (0.6299787005000002 * b);
double l_ = Math.Cbrt(l);
double m_ = Math.Cbrt(m);
double s_ = Math.Cbrt(s);
return (
(0.2104542553 * l_) + (0.793617785 * m_) - (0.0040720468 * s_),
(1.9779984951 * l_) - (2.428592205 * m_) + (0.4505937099 * s_),
(0.0259040371 * l_) + (0.7827717662 * m_) - (0.808675766 * s_)
);
}
/// <summary>
/// Convert an Oklab color <see cref="double"/> from Cartesian form to its polar form Oklch
/// https://bottosson.github.io/posts/oklab/#the-oklab-color-space
/// </summary>
/// <param name="lightness">The <see cref="lightness"/></param>
/// <param name="chromaticity_a">The <see cref="chromaticity_a"/></param>
/// <param name="chromaticity_b">The <see cref="chromaticity_b"/></param>
/// <returns>The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]</returns>
private static (double Lightness, double Chroma, double Hue)
GetOklchColorFromOklab(double lightness, double chromaticity_a, double chromaticity_b)
{
return GetLCHColorFromLAB(lightness, chromaticity_a, chromaticity_b);
}
/// <summary>
/// Convert a color in Cartesian form (Lab) to its polar form (LCh)
/// </summary>
/// <param name="lightness">The <see cref="lightness"/></param>
/// <param name="chromaticity_a">The <see cref="chromaticity_a"/></param>
/// <param name="chromaticity_b">The <see cref="chromaticity_b"/></param>
/// <returns>The lightness, chroma, and hue angle</returns>
private static (double Lightness, double Chroma, double Hue)
GetLCHColorFromLAB(double lightness, double chromaticity_a, double chromaticity_b)
{
// Lab to LCh transformation
double chroma = Math.Sqrt(Math.Pow(chromaticity_a, 2) + Math.Pow(chromaticity_b, 2));
double hue = Math.Round(chroma, 3) == 0 ? 0.0 : ((Math.Atan2(chromaticity_b, chromaticity_a) * 180d / Math.PI) + 360d) % 360d;
return (lightness, chroma, hue);
}
/// <summary>
/// Convert a given <see cref="Color"/> to a natural color (hue, whiteness, blackness)
/// </summary>
@@ -276,12 +364,17 @@ namespace ManagedCommon
{ "Br", 'p' }, // brightness percent
{ "In", 'p' }, // intensity percent
{ "Ll", 'p' }, // lightness (HSL) percent
{ "Lc", 'p' }, // lightness(CIELAB)percent
{ "Va", 'p' }, // value percent
{ "Wh", 'p' }, // whiteness percent
{ "Bn", 'p' }, // blackness percent
{ "Ca", 'p' }, // chromaticityA percent
{ "Cb", 'p' }, // chromaticityB percent
{ "Lc", 'p' }, // lightness (CIE) percent
{ "Ca", 'p' }, // chromaticityA (CIELAB) percent
{ "Cb", 'p' }, // chromaticityB (CIELAB) percent
{ "Lo", 'p' }, // lightness (Oklab/Oklch) percent
{ "Oa", 'p' }, // chromaticityA (Oklab) percent
{ "Ob", 'p' }, // chromaticityB (Oklab) percent
{ "Oc", 'p' }, // chroma (Oklch) percent
{ "Oh", 'p' }, // hue angle (Oklch) percent
{ "Xv", 'i' }, // X value int
{ "Yv", 'i' }, // Y value int
{ "Zv", 'i' }, // Z value int
@@ -424,6 +517,10 @@ namespace ManagedCommon
var (lightnessC, _, _) = ConvertToCIELABColor(color);
lightnessC = Math.Round(lightnessC, 2);
return lightnessC.ToString(CultureInfo.InvariantCulture);
case "Lo":
var (lightnessO, _, _) = ConvertToOklabColor(color);
lightnessO = Math.Round(lightnessO, 2);
return lightnessO.ToString(CultureInfo.InvariantCulture);
case "Wh":
var (_, whiteness, _) = ConvertToHWBColor(color);
whiteness = Math.Round(whiteness * 100);
@@ -440,6 +537,22 @@ namespace ManagedCommon
var (_, _, chromaticityB) = ConvertToCIELABColor(color);
chromaticityB = Math.Round(chromaticityB, 2);
return chromaticityB.ToString(CultureInfo.InvariantCulture);
case "Oa":
var (_, chromaticityAOklab, _) = ConvertToOklabColor(color);
chromaticityAOklab = Math.Round(chromaticityAOklab, 2);
return chromaticityAOklab.ToString(CultureInfo.InvariantCulture);
case "Ob":
var (_, _, chromaticityBOklab) = ConvertToOklabColor(color);
chromaticityBOklab = Math.Round(chromaticityBOklab, 2);
return chromaticityBOklab.ToString(CultureInfo.InvariantCulture);
case "Oc":
var (_, chromaOklch, _) = ConvertToOklchColor(color);
chromaOklch = Math.Round(chromaOklch, 2);
return chromaOklch.ToString(CultureInfo.InvariantCulture);
case "Oh":
var (_, _, hueOklch) = ConvertToOklchColor(color);
hueOklch = Math.Round(hueOklch, 2);
return hueOklch.ToString(CultureInfo.InvariantCulture);
case "Xv":
var (x, _, _) = ConvertToCIEXYZColor(color);
x = Math.Round(x * 100, 4);
@@ -495,8 +608,10 @@ namespace ManagedCommon
case "HSI": return "hsi(%Hu, %Si%, %In%)";
case "HWB": return "hwb(%Hu, %Wh%, %Bn%)";
case "NCol": return "%Hn, %Wh%, %Bn%";
case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
case "CIEXYZ": return "XYZ(%Xv, %Yv, %Zv)";
case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
case "Oklab": return "oklab(%Lo, %Oa, %Ob)";
case "Oklch": return "oklch(%Lo, %Oc, %Oh)";
case "VEC4": return "(%Reff, %Grff, %Blff, 1f)";
case "Decimal": return "%Dv";
case "HEX Int": return "0xFF%ReX%GrX%BlX";

View File

@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ManagedCommon
{
public static class IdRecoveryHelper
{
/// <summary>
/// Fixes invalid IDs in the given list by assigning unique values.
/// It ensures that all IDs are non-empty and unique, correcting any duplicates or empty IDs.
/// </summary>
/// <param name="items">The list of items that may contain invalid IDs.</param>
public static void RecoverInvalidIds<T>(IEnumerable<T> items)
where T : class, IHasId
{
var idSet = new HashSet<int>();
int newId = 0;
var sortedItems = items.OrderBy(i => i.Id).ToList(); // Sort items by ID for consistent processing
// Iterate through the list and fix invalid IDs
foreach (var item in sortedItems)
{
// If the ID is invalid or already exists in the set (duplicate), assign a new unique ID
if (!idSet.Add(item.Id))
{
// Find the next available unique ID
while (idSet.Contains(newId))
{
newId++;
}
item.Id = newId;
idSet.Add(newId); // Add the newly assigned ID to the set
}
}
}
}
public interface IHasId
{
int Id { get; set; }
}
}

View File

@@ -301,6 +301,7 @@ namespace package
if (!std::filesystem::exists(directoryPath))
{
Logger::error(L"The directory '" + directoryPath + L"' does not exist.");
return {};
}
const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase);

View File

@@ -18,10 +18,19 @@ public static class OcrHelpers
{
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap, CancellationToken cancellationToken)
{
var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language");
var ocrLanguage = GetOCRLanguage();
cancellationToken.ThrowIfCancellationRequested();
var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
OcrEngine ocrEngine;
if (ocrLanguage is not null)
{
ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine from specified language");
}
else
{
ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages() ?? throw new InvalidOperationException("Unable to create OCR engine from user profile language");
}
cancellationToken.ThrowIfCancellationRequested();
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);

View File

@@ -15,8 +15,6 @@
<ProjectPriFileName>PowerToys.EnvironmentVariablesUILib.pri</ProjectPriFileName>
<GenerateLibraryLayout>true</GenerateLibraryLayout>
<IsPackable>true</IsPackable>
<!-- The default generated file path exceeds the length limit 260 on the build agent. Using a shorter path as a workaround. -->
<CompilerGeneratedFilesOutputPath>$(ProjectDir)obj\g</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<PropertyGroup>
@@ -56,7 +54,4 @@
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<Target Name="EnsureCompilerGeneratedFilesOutputPathExists" BeforeTargets="XamlPreCompile">
<MakeDir Directories="$(CompilerGeneratedFilesOutputPath)" />
</Target>
</Project>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
@@ -141,7 +141,7 @@
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
@@ -153,7 +153,7 @@
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets'))" />
</Target>
</Project>

View File

@@ -4,5 +4,5 @@
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.22621.2428" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.6.250205002" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.7.250401001" targetFramework="native" />
</packages>

View File

@@ -121,6 +121,22 @@ namespace AppLauncher
// packaged apps: try launching first by AppUserModel.ID
// usage example: elevated Terminal
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
{
Logger::trace(L"Launching {} as {} - {app.packageFullName}", app.name, app.appUserModelId, app.packageFullName);
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
if (res.isOk())
{
launched = true;
}
else
{
launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
}
}
// win32 app with appUserModelId:
// usage example: steam games
if (!launched && !app.appUserModelId.empty())
{
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);

View File

@@ -1,5 +1,6 @@
#include "pch.h"
#include "AppUtils.h"
#include "SteamHelper.h"
#include <atlbase.h>
#include <propvarutil.h>
@@ -34,6 +35,8 @@ namespace Utils
constexpr const wchar_t* EdgeFilename = L"msedge.exe";
constexpr const wchar_t* ChromeFilename = L"chrome.exe";
constexpr const wchar_t* SteamUrlProtocol = L"steam:";
}
AppList IterateAppsFolder()
@@ -138,6 +141,34 @@ namespace Utils
else if (prop == NonLocalizable::PackageInstallPathProp || prop == NonLocalizable::InstallPathProp)
{
data.installPath = propVariantString.m_pData;
if (!data.installPath.empty())
{
const bool isSteamProtocol = data.installPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
if (isSteamProtocol)
{
Logger::info(L"Found steam game: protocol path: {}", data.installPath);
data.protocolPath = data.installPath;
try
{
auto gameId = Steam::GetGameIdFromUrlProtocolPath(data.installPath);
auto gameFolder = Steam::GetSteamGameInfoFromAcfFile(gameId);
if (gameFolder)
{
data.installPath = gameFolder->gameInstallationPath;
Logger::info(L"Found steam game: physical path: {}", data.installPath);
}
}
catch (std::exception ex)
{
Logger::error(L"Failed to get installPath for game {}", data.installPath);
Logger::error("Error: {}", ex.what());
}
}
}
}
}
@@ -397,5 +428,10 @@ namespace Utils
{
return installPath.ends_with(NonLocalizable::ChromeFilename);
}
bool AppData::IsSteamGame() const
{
return protocolPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
}
}
}

View File

@@ -13,10 +13,12 @@ namespace Utils
std::wstring packageFullName;
std::wstring appUserModelId;
std::wstring pwaAppId;
std::wstring protocolPath;
bool canLaunchElevated = false;
bool IsEdge() const;
bool IsChrome() const;
bool IsSteamGame() const;
};
using AppList = std::vector<AppData>;

View File

@@ -0,0 +1,171 @@
#include "pch.h"
#include "SteamHelper.h"
#include <fstream>
#include <sstream>
#include <unordered_map>
#include <filesystem>
#include <regex>
#include <string>
namespace Utils
{
static std::wstring Utf8ToWide(const std::string& utf8)
{
if (utf8.empty())
return L"";
int size = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), nullptr, 0);
if (size <= 0)
return L"";
std::wstring wide(size, L'\0');
MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), wide.data(), size);
return wide;
}
namespace Steam
{
using namespace std;
namespace fs = std::filesystem;
static std::optional<std::wstring> GetSteamExePathFromRegistry()
{
static std::optional<std::wstring> cachedPath;
if (cachedPath.has_value())
{
return cachedPath;
}
const std::vector<HKEY> roots = { HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, HKEY_USERS };
const std::vector<std::wstring> subKeys = {
L"steam\\shell\\open\\command",
L"Software\\Classes\\steam\\shell\\open\\command",
};
for (HKEY root : roots)
{
for (const auto& subKey : subKeys)
{
HKEY hKey;
if (RegOpenKeyExW(root, subKey.c_str(), 0, KEY_READ, &hKey) == ERROR_SUCCESS)
{
wchar_t value[512];
DWORD size = sizeof(value);
DWORD type = 0;
if (RegQueryValueExW(hKey, nullptr, nullptr, &type, reinterpret_cast<LPBYTE>(value), &size) == ERROR_SUCCESS &&
(type == REG_SZ || type == REG_EXPAND_SZ))
{
std::wregex exeRegex(LR"delim("([^"]+steam\.exe)")delim");
std::wcmatch match;
if (std::regex_search(value, match, exeRegex) && match.size() > 1)
{
RegCloseKey(hKey);
cachedPath = match[1].str();
return cachedPath;
}
}
RegCloseKey(hKey);
}
}
}
cachedPath = std::nullopt;
return std::nullopt;
}
static fs::path GetSteamBasePath()
{
auto steamFolderOpt = GetSteamExePathFromRegistry();
if (!steamFolderOpt)
{
return {};
}
return fs::path(*steamFolderOpt).parent_path() / L"steamapps";
}
static fs::path GetAcfFilePath(const std::wstring& gameId)
{
auto steamFolderOpt = GetSteamExePathFromRegistry();
if (!steamFolderOpt)
{
return {};
}
return GetSteamBasePath() / (L"appmanifest_" + gameId + L".acf");
}
static fs::path GetGameInstallPath(const std::wstring& gameFolderName)
{
auto steamFolderOpt = GetSteamExePathFromRegistry();
if (!steamFolderOpt)
{
return {};
}
return GetSteamBasePath() / L"common" / gameFolderName;
}
static unordered_map<wstring, wstring> ParseAcfFile(const fs::path& acfPath)
{
unordered_map<wstring, wstring> result;
ifstream file(acfPath);
if (!file.is_open())
return result;
string line;
while (getline(file, line))
{
smatch matches;
static const regex pattern(R"delim("([^"]+)"\s+"([^"]+)")delim");
if (regex_search(line, matches, pattern) && matches.size() == 3)
{
wstring key = Utf8ToWide(matches[1].str());
wstring value = Utf8ToWide(matches[2].str());
result[key] = value;
}
}
return result;
}
std::unique_ptr<Steam::SteamGame> GetSteamGameInfoFromAcfFile(const std::wstring& gameId)
{
fs::path acfPath = Steam::GetAcfFilePath(gameId);
if (!fs::exists(acfPath))
return nullptr;
auto kv = ParseAcfFile(acfPath);
if (kv.empty() || kv.find(L"installdir") == kv.end())
return nullptr;
fs::path gamePath = Steam::GetGameInstallPath(kv[L"installdir"]);
if (!fs::exists(gamePath))
return nullptr;
auto game = std::make_unique<Steam::SteamGame>();
game->gameId = gameId;
game->gameInstallationPath = gamePath.wstring();
return game;
}
std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath)
{
const std::wstring steamGamePrefix = L"steam://rungameid/";
if (urlPath.rfind(steamGamePrefix, 0) == 0)
{
return urlPath.substr(steamGamePrefix.length());
}
return L"";
}
}
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include "pch.h"
namespace Utils
{
namespace NonLocalizable
{
const std::wstring AcfFileNameTemplate = L"appmanifest_<gameid>.acfs";
}
namespace Steam
{
struct SteamGame
{
std::wstring gameId;
std::wstring gameInstallationPath;
};
std::unique_ptr<SteamGame> GetSteamGameInfoFromAcfFile(const std::wstring& gameId);
std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath);
}
}

View File

@@ -41,6 +41,7 @@
<ClInclude Include="pch.h" />
<ClInclude Include="PwaHelper.h" />
<ClInclude Include="Result.h" />
<ClInclude Include="SteamHelper.h" />
<ClInclude Include="StringUtils.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="WbemHelper.h" />
@@ -57,6 +58,7 @@
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="PwaHelper.cpp" />
<ClCompile Include="SteamGameHelper.cpp" />
<ClCompile Include="two_way_pipe_message_ipc.cpp" />
<ClCompile Include="WbemHelper.cpp" />
<ClCompile Include="WorkspacesData.cpp" />

View File

@@ -53,6 +53,9 @@
<ClInclude Include="StringUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SteamHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@@ -88,6 +91,9 @@
<ClCompile Include="WbemHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SteamGameHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -71,6 +71,8 @@ namespace SnapshotUtils
continue;
}
Logger::info("Try to get window app:{}", reinterpret_cast<void*>(window));
DWORD pid{};
GetWindowThreadProcessId(window, &pid);
@@ -118,10 +120,19 @@ namespace SnapshotUtils
auto data = Utils::Apps::GetApp(processPath, pid, installedApps);
if (!data.has_value() || data->name.empty())
{
Logger::info(L"Installed app not found: {}", processPath);
Logger::info(L"Installed app not found:{},{}", reinterpret_cast<void*>(window), processPath);
continue;
}
if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
{
// Only care about steam games if it has no thick frame to remain consistent with
// the behavior as before.
continue;
}
Logger::info(L"Found app for window:{},{}", reinterpret_cast<void*>(window), processPath);
auto appData = data.value();
bool isEdge = appData.IsEdge();

View File

@@ -200,6 +200,14 @@ std::optional<WindowWithDistance> WindowArranger::GetNearestWindow(const Workspa
}
auto data = Utils::Apps::GetApp(processPath, pid, m_installedApps);
if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
{
// Only care about steam games if it has no thick frame to remain consistent with
// the behavior as before.
continue;
}
if (!data.has_value())
{
continue;

View File

@@ -9,10 +9,12 @@ namespace WindowFilter
{
auto style = GetWindowLong(window, GWL_STYLE);
bool isPopup = WindowUtils::HasStyle(style, WS_POPUP);
bool hasThickFrame = WindowUtils::HasStyle(style, WS_THICKFRAME);
bool hasCaption = WindowUtils::HasStyle(style, WS_CAPTION);
bool hasMinimizeMaximizeButtons = WindowUtils::HasStyle(style, WS_MINIMIZEBOX) || WindowUtils::HasStyle(style, WS_MAXIMIZEBOX);
if (isPopup && !(hasThickFrame && (hasCaption || hasMinimizeMaximizeButtons)))
Logger::info("Style for window: {}, {:#x}", reinterpret_cast<void*>(window), style);
if (isPopup && !(hasCaption || hasMinimizeMaximizeButtons))
{
// popup windows we want to snap: e.g. Calculator, Telegram
// popup windows we don't want to snap: start menu, notification popup, tray window, etc.

View File

@@ -121,4 +121,11 @@ namespace WindowUtils
return std::wstring(title);
}
inline bool HasThickFrame(HWND window)
{
auto style = GetWindowLong(window, GWL_STYLE);
return WindowUtils::HasStyle(style, WS_THICKFRAME);
}
}

View File

@@ -0,0 +1,3 @@
EXPORTS
DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE
DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE

View File

@@ -0,0 +1,40 @@
#include <windows.h>
#include "resource.h"
#include "../../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
#include "winres.h"
#undef APSTUDIO_READONLY_SYMBOLS
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END

View File

@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
<CppWinRTGenerateWindowsMetadata>true</CppWinRTGenerateWindowsMetadata>
<ProjectGuid>{5f63c743-f6ce-4dba-a200-2b3f8a14e8c2}</ProjectGuid>
<ProjectName>CmdPalKeyboardService</ProjectName>
<RootNamespace>CmdPalKeyboardService</RootNamespace>
<AppxPackage>false</AppxPackage>
</PropertyGroup>
<!-- BEGIN common.build.pre.props -->
<PropertyGroup Label="Configuration">
<EnableHybridCRT>true</EnableHybridCRT>
<UseCrtSDKReferenceStaticWarning Condition="'$(EnableHybridCRT)'=='true'">false</UseCrtSDKReferenceStaticWarning>
</PropertyGroup>
<!-- END common.build.pre.props -->
<!-- BEGIN cppwinrt.build.pre.props -->
<PropertyGroup Label="Globals">
<CppWinRTEnabled>true</CppWinRTEnabled>
<CppWinRTOptimized>true</CppWinRTOptimized>
<DefaultLanguage>en-US</DefaultLanguage>
<MinimumVisualStudioVersion>17.0</MinimumVisualStudioVersion>
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
</PropertyGroup>
<PropertyGroup>
<MinimalCoreWin>true</MinimalCoreWin>
<AppContainerApplication>true</AppContainerApplication>
<WindowsStoreApp>true</WindowsStoreApp>
<ApplicationType>Windows Store</ApplicationType>
<UseCrtSDKReference Condition="'$(EnableHybridCRT)'=='true'">false</UseCrtSDKReference> <!-- The SDK reference breaks the Hybrid CRT -->
</PropertyGroup>
<PropertyGroup>
<!-- We have to use the Desktop platform for Hybrid CRT to work. -->
<_VC_Target_Library_Platform>Desktop</_VC_Target_Library_Platform>
<_NoWinAPIFamilyApp>true</_NoWinAPIFamilyApp>
</PropertyGroup>
<!-- END cppwinrt.build.pre.props -->
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<GenerateManifest>false</GenerateManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="PropertySheet.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<TargetName>CmdPalKeyboardService</TargetName>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
<WarningLevel>Level4</WarningLevel>
<AdditionalOptions>%(AdditionalOptions) /bigobj</AdditionalOptions>
<PreprocessorDefinitions>_WINRT_DLL;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>../../..;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalUsingDirectories>$(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories)</AdditionalUsingDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
<ModuleDefinitionFile>CmdPalKeyboardService.def</ModuleDefinitionFile>
<AdditionalDependencies>Shell32.lib;user32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="KeyboardListener.h">
<DependentUpon>KeyboardListener.idl</DependentUpon>
</ClInclude>
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="KeyboardListener.cpp">
<DependentUpon>KeyboardListener.idl</DependentUpon>
</ClCompile>
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
</ItemGroup>
<ItemGroup>
<Midl Include="KeyboardListener.idl" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="CmdPalKeyboardService.def" />
</ItemGroup>
<ItemGroup>
<None Include="PropertySheet.props" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="CmdPalKeyboardService.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
<!-- BEGIN common.build.post.props -->
<!--
The Hybrid CRT model statically links the runtime and STL and dynamically
links the UCRT instead of the VC++ CRT. The UCRT ships with Windows.
WinAppSDK asserts that this is "supported according to the CRT maintainer."
This must come before Microsoft.Cpp.targets because it manipulates ClCompile.RuntimeLibrary.
-->
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and '$(Configuration)'=='Debug'">
<ClCompile>
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and ('$(Configuration)'=='Release' or '$(Configuration)'=='AuditMode')">
<ClCompile>
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
</Link>
</ItemDefinitionGroup>
<!-- END common.build.post.props -->
</Project>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Resources">
<UniqueIdentifier>accd3aa8-1ba0-4223-9bbe-0c431709210b</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Filter Include="Generated Files">
<UniqueIdentifier>{926ab91d-31b4-48c3-b9a4-e681349f27f0}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp" />
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<Midl Include="KeyboardListener.idl" />
</ItemGroup>
<ItemGroup>
<None Include="CmdPalKeyboardService.def" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<None Include="PropertySheet.props" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="CmdPalKeyboardService.rc">
<Filter>Resources</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,165 @@
#include "pch.h"
#include "KeyboardListener.h"
#include "KeyboardListener.g.cpp"
// #include <common/logger/logger.h>
// #include <common/utils/logger_helper.h>
#include <common/utils/winapi_error.h>
namespace
{
}
namespace winrt::CmdPalKeyboardService::implementation
{
KeyboardListener::KeyboardListener()
{
s_instance = this;
}
void KeyboardListener::Start()
{
#if defined(DISABLE_LOWLEVEL_HOOKS_WHEN_DEBUGGED)
const bool hook_disabled = IsDebuggerPresent();
#else
const bool hook_disabled = false;
#endif
if (!hook_disabled)
{
if (!s_llKeyboardHook)
{
s_llKeyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, NULL, NULL);
if (!s_llKeyboardHook)
{
DWORD errorCode = GetLastError();
show_last_error_message(L"SetWindowsHookEx", errorCode, L"CmdPalKeyboardService");
}
}
}
}
void KeyboardListener::Stop()
{
if (s_llKeyboardHook && UnhookWindowsHookEx(s_llKeyboardHook))
{
s_llKeyboardHook = NULL;
}
}
void KeyboardListener::SetHotkeyAction(bool win, bool ctrl, bool shift, bool alt, uint8_t key, hstring const& id)
{
Hotkey hotkey = { .win = win, .ctrl = ctrl, .shift = shift, .alt = alt, .key = key };
std::unique_lock lock{ mutex };
HotkeyDescriptor desc = { .hotkey = hotkey, .id = std::wstring(id) };
hotkeyDescriptors.insert(desc);
}
void KeyboardListener::ClearHotkey(hstring const& id)
{
{
std::unique_lock lock{ mutex };
auto it = hotkeyDescriptors.begin();
while (it != hotkeyDescriptors.end())
{
if (it->id == id)
{
it = hotkeyDescriptors.erase(it);
}
else
{
++it;
}
}
}
}
void KeyboardListener::ClearHotkeys()
{
{
std::unique_lock lock{ mutex };
auto it = hotkeyDescriptors.begin();
while (it != hotkeyDescriptors.end())
{
it = hotkeyDescriptors.erase(it);
}
}
}
void KeyboardListener::SetProcessCommand(ProcessCommand processCommand)
{
m_processCommandCb = [trigger = std::move(processCommand)](hstring const& id) {
trigger(id);
};
}
LRESULT KeyboardListener::DoLowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
const auto& keyPressInfo = *reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
if ((wParam != WM_KEYDOWN) && (wParam != WM_SYSKEYDOWN))
{
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
Hotkey hotkey{
.win = (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000),
.ctrl = static_cast<bool>(GetAsyncKeyState(VK_CONTROL) & 0x8000),
.shift = static_cast<bool>(GetAsyncKeyState(VK_SHIFT) & 0x8000),
.alt = static_cast<bool>(GetAsyncKeyState(VK_MENU) & 0x8000),
.key = static_cast<unsigned char>(keyPressInfo.vkCode)
};
if (hotkey == Hotkey{})
{
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
bool do_action = false;
std::wstring actionId{};
{
// Hold the lock for the shortest possible duration
std::unique_lock lock{ mutex };
HotkeyDescriptor dummy{ .hotkey = hotkey };
auto it = hotkeyDescriptors.find(dummy);
if (it != hotkeyDescriptors.end())
{
do_action = true;
actionId = it->id;
}
}
if (do_action)
{
m_processCommandCb(hstring{ actionId });
// After invoking the hotkey send a dummy key to prevent Start Menu from activating
INPUT dummyEvent[1] = {};
dummyEvent[0].type = INPUT_KEYBOARD;
dummyEvent[0].ki.wVk = 0xFF;
dummyEvent[0].ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(1, dummyEvent, sizeof(INPUT));
// Swallow the key press
return 1;
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
LRESULT KeyboardListener::LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (s_instance == nullptr)
{
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
if (nCode < 0)
{
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
return s_instance->DoLowLevelKeyboardProc(nCode, wParam, lParam);
}
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include "KeyboardListener.g.h"
#include <mutex>
#include <spdlog/stopwatch.h>
#include <set>
namespace winrt::CmdPalKeyboardService::implementation
{
struct KeyboardListener : KeyboardListenerT<KeyboardListener>
{
struct Hotkey
{
bool win = false;
bool ctrl = false;
bool shift = false;
bool alt = false;
unsigned char key = 0;
std::strong_ordering operator<=>(const Hotkey&) const = default;
};
struct HotkeyDescriptor
{
Hotkey hotkey;
std::wstring id;
bool operator<(const HotkeyDescriptor& other) const
{
return hotkey < other.hotkey;
};
};
KeyboardListener();
void Start();
void Stop();
void SetHotkeyAction(bool win, bool ctrl, bool shift, bool alt, uint8_t key, hstring const& id);
void ClearHotkey(hstring const& id);
void ClearHotkeys();
void SetProcessCommand(ProcessCommand processCommand);
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
private:
LRESULT CALLBACK DoLowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
static inline KeyboardListener* s_instance;
HHOOK s_llKeyboardHook = nullptr;
// Max DWORD for key code to disable keys.
const DWORD VK_DISABLED = 0x100;
DWORD vkCodePressed = VK_DISABLED;
std::multiset<HotkeyDescriptor> hotkeyDescriptors;
std::mutex mutex;
std::function<void(hstring const&)> m_processCommandCb;
};
}
namespace winrt::CmdPalKeyboardService::factory_implementation
{
struct KeyboardListener : KeyboardListenerT<KeyboardListener, implementation::KeyboardListener>
{
};
}

View File

@@ -0,0 +1,16 @@
namespace CmdPalKeyboardService
{
[version(1.0), uuid(78ab07cd-e128-4e73-86aa-e48e6b6d01ff)] delegate void ProcessCommand(String id);
[default_interface] runtimeclass KeyboardListener {
KeyboardListener();
void Start();
void Stop();
void SetHotkeyAction(Boolean win, Boolean ctrl, Boolean shift, Boolean alt, UInt8 key, String id);
void ClearHotkey(String id);
void ClearHotkeys();
void SetProcessCommand(ProcessCommand processCommand);
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<!--
To customize common C++/WinRT project properties:
* right-click the project node
* expand the Common Properties item
* select the C++/WinRT property page
For more advanced scenarios, and complete documentation, please see:
https://github.com/Microsoft/cppwinrt/tree/master/nuget
-->
<PropertyGroup />
<ItemDefinitionGroup />
</Project>

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
#pragma once
#include <Unknwn.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>

View File

@@ -0,0 +1,13 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by PowerToys.MeasureToolCore.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "CmdPalKeyboardService"
#define INTERNAL_NAME "CmdPalKeyboardService"
#define ORIGINAL_FILENAME "CmdPalKeyboardService.dll"
// Non-localizable
//////////////////////////////

View File

@@ -2,6 +2,7 @@
<Project DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<Import Project="..\Microsoft.CmdPal.UI\CmdPal.pre.props" Condition="Exists('..\Microsoft.CmdPal.UI\CmdPal.pre.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
@@ -49,13 +50,20 @@
</ItemGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>
EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;
%(PreprocessorDefinitions);
</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(CommandPaletteBranding)'=='' or '$(CommandPaletteBranding)'=='Dev'">
IS_DEV_BRANDING;%(PreprocessorDefinitions)
</PreprocessorDefinitions>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />

View File

@@ -3,6 +3,7 @@
#include <interface/powertoy_module_interface.h>
#include <atomic>
#include <common/logger/logger.h>
#include <common/utils/logger_helper.h>
#include <common/SettingsAPI/settings_helpers.h>
@@ -10,10 +11,11 @@
#include <common/utils/resources.h>
#include <common/utils/package.h>
#include <common/utils/process_path.h>
#include <common/utils/winapi_error.h>
#include <common/interop/shared_constants.h>
#include <Psapi.h>
#include <TlHelp32.h>
#include <common/utils/winapi_error.h>
#include <thread>
HINSTANCE g_hInst_cmdPal = 0;
@@ -37,8 +39,6 @@ BOOL APIENTRY DllMain(HMODULE hInstance,
class CmdPal : public PowertoyModuleIface
{
private:
bool m_enabled = false;
std::wstring app_name;
//contains the non localized key of the powertoy
@@ -46,7 +46,10 @@ private:
HANDLE m_hTerminateEvent;
void LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated)
// Track if this is the first call to enable
bool firstEnableCall = true;
static bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, bool silentFail)
{
std::wstring dir = std::filesystem::path(appPath).parent_path();
@@ -54,6 +57,10 @@ private:
sei.cbSize = sizeof(SHELLEXECUTEINFO);
sei.hwnd = nullptr;
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
if (silentFail)
{
sei.fMask = sei.fMask | SEE_MASK_FLAG_NO_UI;
}
sei.lpVerb = elevated ? L"runas" : L"open";
sei.lpFile = appPath.c_str();
sei.lpParameters = commandLineArgs.c_str();
@@ -64,7 +71,11 @@ private:
{
std::wstring error = get_last_error_or_default(GetLastError());
Logger::error(L"Failed to launch process. {}", error);
return false;
}
m_launched.store(true);
return true;
}
std::vector<DWORD> GetProcessesIdByName(const std::wstring& processName)
@@ -122,6 +133,9 @@ private:
}
public:
static std::atomic<bool> m_enabled;
static std::atomic<bool> m_launched;
CmdPal()
{
app_name = L"CmdPal";
@@ -133,10 +147,7 @@ public:
~CmdPal()
{
if (m_enabled)
{
}
m_enabled = false;
CmdPal::m_enabled.store(false);
}
// Destroy the powertoy and free memory
@@ -203,16 +214,23 @@ public:
{
Logger::trace("CmdPal::enable()");
m_enabled = true;
CmdPal::m_enabled.store(true);
try
std::wstring packageName = L"Microsoft.CommandPalette";
std::wstring launchPath = L"shell:AppsFolder\\Microsoft.CommandPalette_8wekyb3d8bbwe!App";
#ifdef IS_DEV_BRANDING
packageName = L"Microsoft.CommandPalette.Dev";
launchPath = L"shell:AppsFolder\\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App";
#endif
if (!package::GetRegisteredPackage(packageName, false).has_value())
{
if (!package::GetRegisteredPackage(L"Microsoft.CommandPalette", false).has_value())
try
{
Logger::info(L"CmdPal not installed. Installing...");
std::wstring installationFolder = get_module_folderpath();
#if _DEBUG
#ifdef _DEBUG
std::wstring archSubdir = L"x64";
#ifdef _M_ARM64
archSubdir = L"ARM64";
@@ -234,19 +252,34 @@ public:
}
}
}
}
catch (std::exception& e)
{
std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
errorMessage += e.what();
Logger::error(errorMessage);
catch (std::exception& e)
{
std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
errorMessage += e.what();
Logger::error(errorMessage);
}
}
#if _DEBUG
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App", L"RunFromPT", false);
#else
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette_8wekyb3d8bbwe!App", L"RunFromPT", false);
#endif
if (!package::GetRegisteredPackage(packageName, false).has_value())
{
Logger::error("Cmdpal is not registered, quit..");
return;
}
if (!firstEnableCall)
{
Logger::trace("Not first attempt, try to launch");
LaunchApp(launchPath, L"RunFromPT", false /*no elevated*/, false /*error pop up*/);
}
else
{
// If first time enable, do retry launch.
Logger::trace("First attempt, try to launch");
std::thread launchThread(&CmdPal::RetryLaunch, launchPath);
launchThread.detach();
}
firstEnableCall = false;
}
virtual void disable()
@@ -254,7 +287,44 @@ public:
Logger::trace("CmdPal::disable()");
TerminateCmdPal();
m_enabled = false;
CmdPal::m_enabled.store(false);
}
static void RetryLaunch(std::wstring path)
{
const int base_delay_milliseconds = 1000;
int max_retry = 9; // 2**9 - 1 seconds. Control total wait time within 10 min.
int retry = 0;
do
{
auto launch_result = LaunchApp(path, L"RunFromPT", false, retry < max_retry);
if (launch_result)
{
Logger::info(L"CmdPal launched successfully after {} retries.", retry);
return;
}
else
{
Logger::error(L"Retry {} launch CmdPal launch failed.", retry);
}
// When we got max retry, we don't need to wait for the next retry.
if (retry < max_retry)
{
int delay = base_delay_milliseconds * (1 << (retry));
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
}
++retry;
} while (retry <= max_retry && m_enabled.load() && !m_launched.load());
if (!m_enabled.load() || m_launched.load())
{
Logger::error(L"Retry cancelled. CmdPal is disabled or already launched.");
}
else
{
Logger::error(L"CmdPal launch failed after {} attempts.", retry);
}
}
virtual bool on_hotkey(size_t) override
@@ -269,11 +339,14 @@ public:
virtual bool is_enabled() override
{
return m_enabled;
return CmdPal::m_enabled.load();
}
};
std::atomic<bool> CmdPal::m_enabled{ false };
std::atomic<bool> CmdPal::m_launched{ false };
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new CmdPal();
}
}

View File

@@ -9,7 +9,7 @@
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.6.250205002" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250401001" />
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="System.Text.Json" Version="9.0.3" />

View File

@@ -1,177 +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.Data;
using System.Globalization;
using System.Text;
using Microsoft.CmdPal.Ext.Calc.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Calc;
public partial class CalculatorCommandProvider : CommandProvider
{
private readonly ListItem _listItem = new(new CalculatorListPage()) { Subtitle = Resources.calculator_top_level_subtitle };
private readonly FallbackCalculatorItem _fallback = new();
public CalculatorCommandProvider()
{
Id = "Calculator";
DisplayName = Resources.calculator_display_name;
Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
}
public override ICommandItem[] TopLevelCommands() => [_listItem];
public override IFallbackCommandItem[] FallbackCommands() => [_fallback];
}
// The calculator page is a dynamic list page
// * The first command is where we display the results. Title=result, Subtitle=query
// - The default command is `SaveCommand`.
// - When you save, insert into list at spot 1
// - change SearchText to the result
// - MoreCommands: a single `CopyCommand` to copy the result to the clipboard
// * The rest of the items are previously saved results
// - Command is a CopyCommand
// - Each item also sets the TextToSuggest to the result
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
public sealed partial class CalculatorListPage : DynamicListPage
{
private readonly List<ListItem> _items = [];
private readonly SaveCommand _saveCommand = new();
private readonly CopyTextCommand _copyContextCommand;
private readonly CommandContextItem _copyContextMenuItem;
private static readonly CompositeFormat ErrorMessage = System.Text.CompositeFormat.Parse(Properties.Resources.calculator_error);
public CalculatorListPage()
{
Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
Name = Resources.calculator_title;
PlaceholderText = Resources.calculator_placeholder_text;
Id = "com.microsoft.cmdpal.calculator";
_copyContextCommand = new CopyTextCommand(string.Empty);
_copyContextMenuItem = new CommandContextItem(_copyContextCommand);
_items.Add(new(_saveCommand) { Icon = new IconInfo("\uE94E") });
UpdateSearchText(string.Empty, string.Empty);
_saveCommand.SaveRequested += HandleSave;
}
private void HandleSave(object sender, object args)
{
var lastResult = _items[0].Title;
if (!string.IsNullOrEmpty(lastResult))
{
var li = new ListItem(new CopyTextCommand(lastResult))
{
Title = _items[0].Title,
Subtitle = _items[0].Subtitle,
TextToSuggest = lastResult,
};
_items.Insert(1, li);
_items[0].Subtitle = string.Empty;
SearchText = lastResult;
this.RaiseItemsChanged(this._items.Count);
}
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
var firstItem = _items[0];
if (string.IsNullOrEmpty(newSearch))
{
firstItem.Title = Resources.calculator_placeholder_text;
firstItem.Subtitle = string.Empty;
firstItem.MoreCommands = [];
}
else
{
_copyContextCommand.Text = ParseQuery(newSearch, out var result) ? result : string.Empty;
firstItem.Title = result;
firstItem.Subtitle = newSearch;
firstItem.MoreCommands = [_copyContextMenuItem];
}
}
internal static bool ParseQuery(string equation, out string result)
{
try
{
var resultNumber = new DataTable().Compute(equation, null);
result = resultNumber.ToString() ?? string.Empty;
return true;
}
catch (Exception e)
{
result = string.Format(CultureInfo.CurrentCulture, ErrorMessage, e.Message);
return false;
}
}
public override IListItem[] GetItems() => _items.ToArray();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
public sealed partial class SaveCommand : InvokableCommand
{
public event TypedEventHandler<object, object> SaveRequested;
public SaveCommand()
{
Name = Resources.calculator_save_command_name;
}
public override ICommandResult Invoke()
{
SaveRequested?.Invoke(this, this);
return CommandResult.KeepOpen();
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
internal sealed partial class FallbackCalculatorItem : FallbackCommandItem
{
private readonly CopyTextCommand _copyCommand = new(string.Empty);
private static readonly IconInfo _cachedIcon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
public FallbackCalculatorItem()
: base(new NoOpCommand(), Resources.calculator_title)
{
Command = _copyCommand;
_copyCommand.Name = string.Empty;
Title = string.Empty;
Subtitle = Resources.calculator_placeholder_text;
Icon = _cachedIcon;
}
public override void UpdateQuery(string query)
{
if (CalculatorListPage.ParseQuery(query, out var result))
{
_copyCommand.Text = result;
_copyCommand.Name = string.IsNullOrWhiteSpace(query) ? string.Empty : Resources.calculator_copy_command_name;
Title = result;
// we have to make the subtitle the equation,
// so that we will still string match the original query
// Otherwise, something like 1+2 will have a title of "3" and not match
Subtitle = query;
}
else
{
_copyCommand.Text = string.Empty;
_copyCommand.Name = string.Empty;
Title = string.Empty;
Subtitle = string.Empty;
}
}
}

View File

@@ -1,51 +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.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.System.Helpers;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Ext.System;
public sealed partial class SystemCommandsCache
{
public SystemCommandsCache(SettingsManager manager)
{
var list = new List<IListItem>();
var listLock = new object();
var a = Task.Run(() =>
{
var isBootedInUefiMode = Win32Helpers.GetSystemFirmwareType() == FirmwareType.Uefi;
var separateEmptyRB = manager.HideEmptyRecycleBin;
var confirmSystemCommands = manager.ShowDialogToConfirmCommand;
var showSuccessOnEmptyRB = manager.ShowSuccessMessageAfterEmptyingRecycleBin;
// normal system commands are fast and can be returned immediately
var systemCommands = Commands.GetSystemCommands(isBootedInUefiMode, separateEmptyRB, confirmSystemCommands, showSuccessOnEmptyRB);
lock (listLock)
{
list.AddRange(systemCommands);
}
});
var b = Task.Run(() =>
{
// Network (ip and mac) results are slow with many network cards and returned delayed.
// On global queries the first word/part has to be 'ip', 'mac' or 'address' for network results
var networkConnectionResults = Commands.GetNetworkConnectionResults(manager);
lock (listLock)
{
list.AddRange(networkConnectionResults);
}
});
Task.WaitAll(a, b);
CachedCommands = list.ToArray();
}
public IListItem[] CachedCommands { get; }
}

View File

@@ -1,189 +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.RegularExpressions;
namespace Microsoft.CmdPal.Ext.TimeDate.Helpers;
internal static class TimeAndDateHelper
{
/// <summary>
/// Get the format for the time string
/// </summary>
/// <param name="targetFormat">Type of format</param>
/// <param name="timeLong">Show date with weekday and name of month (long format)</param>
/// <param name="dateLong">Show time with seconds (long format)</param>
/// <returns>String that identifies the time/date format (<see href="https://learn.microsoft.com/dotnet/api/system.datetime.tostring"/>)</returns>
internal static string GetStringFormat(FormatStringType targetFormat, bool timeLong, bool dateLong)
{
switch (targetFormat)
{
case FormatStringType.Time:
return timeLong ? "T" : "t";
case FormatStringType.Date:
return dateLong ? "D" : "d";
case FormatStringType.DateTime:
if (timeLong & dateLong)
{
return "F"; // Friday, October 31, 2008 5:04:32 PM
}
else if (timeLong & !dateLong)
{
return "G"; // 10/31/2008 5:04:32 PM
}
else if (!timeLong & dateLong)
{
return "f"; // Friday, October 31, 2008 5:04 PM
}
else
{
// (!timeLong & !dateLong)
return "g"; // 10/31/2008 5:04 PM
}
default:
return string.Empty; // Windows default based on current culture settings
}
}
/// <summary>
/// Returns the number week in the month (Used code from 'David Morton' from <see href="https://social.msdn.microsoft.com/Forums/vstudio/bf504bba-85cb-492d-a8f7-4ccabdf882cb/get-week-number-for-month"/>)
/// </summary>
/// <param name="date">date</param>
/// <returns>Number of week in the month</returns>
internal static int GetWeekOfMonth(DateTime date, DayOfWeek formatSettingFirstDayOfWeek)
{
var beginningOfMonth = new DateTime(date.Year, date.Month, 1);
var adjustment = 1; // We count from 1 to 7 and not from 0 to 6
while (date.Date.AddDays(1).DayOfWeek != formatSettingFirstDayOfWeek)
{
date = date.AddDays(1);
}
return (int)Math.Truncate((double)date.Subtract(beginningOfMonth).TotalDays / 7f) + adjustment;
}
/// <summary>
/// Returns the number of the day in the week
/// </summary>
/// <param name="date">Date</param>
/// <returns>Number of the day in the week</returns>
internal static int GetNumberOfDayInWeek(DateTime date, DayOfWeek formatSettingFirstDayOfWeek)
{
var daysInWeek = 7;
var adjustment = 1; // We count from 1 to 7 and not from 0 to 6
return ((date.DayOfWeek + daysInWeek - formatSettingFirstDayOfWeek) % daysInWeek) + adjustment;
}
/// <summary>
/// Convert input string to a <see cref="DateTime"/> object in local time
/// </summary>
/// <param name="input">String with date/time</param>
/// <param name="timestamp">The new <see cref="DateTime"/> object</param>
/// <returns>True on success, otherwise false</returns>
internal static bool ParseStringAsDateTime(in string input, out DateTime timestamp)
{
if (DateTime.TryParse(input, out timestamp))
{
// Known date/time format
return true;
}
else if (Regex.IsMatch(input, @"^u[\+-]?\d{1,10}$") && long.TryParse(input.TrimStart('u'), out var secondsU))
{
// Unix time stamp
// We use long instead of int, because int is too small after 03:14:07 UTC 2038-01-19
timestamp = DateTimeOffset.FromUnixTimeSeconds(secondsU).LocalDateTime;
return true;
}
else if (Regex.IsMatch(input, @"^ums[\+-]?\d{1,13}$") && long.TryParse(input.TrimStart("ums".ToCharArray()), out var millisecondsUms))
{
// Unix time stamp in milliseconds
// We use long instead of int because int is too small after 03:14:07 UTC 2038-01-19
timestamp = DateTimeOffset.FromUnixTimeMilliseconds(millisecondsUms).LocalDateTime;
return true;
}
else if (Regex.IsMatch(input, @"^ft\d+$") && long.TryParse(input.TrimStart("ft".ToCharArray()), out var secondsFt))
{
// Windows file time
// DateTime.FromFileTime returns as local time.
timestamp = DateTime.FromFileTime(secondsFt);
return true;
}
else
{
timestamp = new DateTime(1, 1, 1, 1, 1, 1);
return false;
}
}
/// <summary>
/// Test if input is special parsing for Unix time, Unix time in milliseconds or File time.
/// </summary>
/// <param name="input">String with date/time</param>
/// <returns>True if yes, otherwise false</returns>
internal static bool IsSpecialInputParsing(string input)
{
return Regex.IsMatch(input, @"^.*(u|ums|ft)\d");
}
/// <summary>
/// Returns a CalendarWeekRule enum value based on the plugin setting.
/// </summary>
internal static CalendarWeekRule GetCalendarWeekRule(int pluginSetting)
{
switch (pluginSetting)
{
case 0:
return CalendarWeekRule.FirstDay;
case 1:
return CalendarWeekRule.FirstFullWeek;
case 2:
return CalendarWeekRule.FirstFourDayWeek;
default:
// Wrong json value and system setting (-1).
return DateTimeFormatInfo.CurrentInfo.CalendarWeekRule;
}
}
/// <summary>
/// Returns a DayOfWeek enum value based on the FirstDayOfWeek plugin setting.
/// </summary>
internal static DayOfWeek GetFirstDayOfWeek(int pluginSetting)
{
switch (pluginSetting)
{
case 0:
return DayOfWeek.Sunday;
case 1:
return DayOfWeek.Monday;
case 2:
return DayOfWeek.Tuesday;
case 3:
return DayOfWeek.Wednesday;
case 4:
return DayOfWeek.Thursday;
case 5:
return DayOfWeek.Friday;
case 6:
return DayOfWeek.Saturday;
default:
// Wrong json value and system setting (-1).
return DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek;
}
}
}
/// <summary>
/// Type of time/date format
/// </summary>
internal enum FormatStringType
{
Time,
Date,
DateTime,
}

View File

@@ -1,132 +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.Threading;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension;
public partial class EvilSamplesPage : ListPage
{
private readonly IListItem[] _commands = [
new ListItem(new EvilSampleListPage())
{
Title = "List Page without items",
Subtitle = "Throws exception on GetItems",
},
new ListItem(new ExplodeInFiveSeconds(false))
{
Title = "Page that will throw an exception after loading it",
Subtitle = "Throws exception on GetItems _after_ a ItemsChanged",
},
new ListItem(new ExplodeInFiveSeconds(true))
{
Title = "Page that keeps throwing exceptions",
Subtitle = "Will throw every 5 seconds once you open it",
},
new ListItem(new ExplodeOnPropChange())
{
Title = "Throw in the middle of a PropChanged",
Subtitle = "Will throw every 5 seconds once you open it",
},
new ListItem(new SelfImmolateCommand())
{
Title = "Terminate this extension",
Subtitle = "Will exit this extension (while it's loaded!)",
},
new ListItem(new NoOpCommand())
{
Title = "I have lots of nulls",
Subtitle = null,
MoreCommands = null,
Tags = null,
Details = new Details()
{
Title = null,
HeroImage = null,
Metadata = null,
},
},
new ListItem(new NoOpCommand())
{
Title = "I also have nulls",
Subtitle = null,
MoreCommands = null,
Details = new Details()
{
Title = null,
HeroImage = null,
Metadata = [new DetailsElement() { Key = "Oops all nulls", Data = new DetailsTags() { Tags = null } }],
},
},
new ListItem(new AnonymousCommand(action: () =>
{
ToastStatusMessage toast = new("I should appear immediately");
toast.Show();
Thread.Sleep(5000);
}) { Result = CommandResult.KeepOpen() })
{
Title = "I take just forever to return something",
Subtitle = "The toast should appear immediately.",
MoreCommands = null,
Details = new Details()
{
Body = "This is a test for GH#512. If it doesn't appear immediately, it's likely InvokeCommand is happening on the UI thread.",
},
}
];
public EvilSamplesPage()
{
Name = "Evil Samples";
Icon = new IconInfo("👿"); // Info
}
public override IListItem[] GetItems() => _commands;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
internal sealed partial class ExplodeOnPropChange : ListPage
{
private bool _explode;
public override string Title
{
get => _explode ? Commands[9001].Title : base.Title;
set => base.Title = value;
}
private IListItem[] Commands => [
new ListItem(new NoOpCommand())
{
Title = "This page will explode in five seconds!",
Subtitle = "I'll change my Name, then explode",
},
];
public ExplodeOnPropChange()
{
Icon = new IconInfo(string.Empty);
Name = "Open";
}
public override IListItem[] GetItems()
{
_ = Task.Run(() =>
{
Thread.Sleep(1000);
Title = "Ready? 3...";
Thread.Sleep(1000);
Title = "Ready? 2...";
Thread.Sleep(1000);
Title = "Ready? 1...";
Thread.Sleep(1000);
_explode = true;
Title = "boom";
});
return Commands;
}
}

View File

@@ -1,97 +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 Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension;
internal sealed partial class SampleListPage : ListPage
{
public SampleListPage()
{
Icon = new IconInfo("\uEA37");
Name = "Sample List Page";
}
public override IListItem[] GetItems()
{
var confirmOnceArgs = new ConfirmationArgs
{
PrimaryCommand = new AnonymousCommand(
() =>
{
var t = new ToastStatusMessage("The dialog was confirmed");
t.Show();
})
{
Name = "Confirm",
Result = CommandResult.KeepOpen(),
},
Title = "You can set a title for the dialog",
Description = "Are you really sure you want to do the thing?",
};
var confirmTwiceArgs = new ConfirmationArgs
{
PrimaryCommand = new AnonymousCommand(() => { })
{
Name = "How sure are you?",
Result = CommandResult.Confirm(confirmOnceArgs),
},
Title = "You can ask twice too",
Description = "You probably don't want to though, that'd be annoying.",
};
return [
new ListItem(new NoOpCommand())
{
Title = "This is a basic item in the list",
Subtitle = "I don't do anything though",
},
new ListItem(new SampleListPageWithDetails())
{
Title = "This item will take you to another page",
Subtitle = "This allows for nested lists of items",
},
new ListItem(new OpenUrlCommand("https://github.com/microsoft/powertoys"))
{
Title = "Or you can go to links",
Subtitle = "This takes you to the PowerToys repo on GitHub",
},
new ListItem(new SampleMarkdownPage())
{
Title = "Items can have tags",
Subtitle = "and I'll take you to a page with markdown content",
Tags = [new Tag("Sample Tag")],
},
new ListItem(new SendMessageCommand())
{
Title = "I send lots of messages",
Subtitle = "Status messages can be used to provide feedback to the user in-app",
},
new SendSingleMessageItem(),
new ListItem(new IndeterminateProgressMessageCommand())
{
Title = "Do a thing with a spinner",
Subtitle = "Messages can have progress spinners, to indicate something is happening in the background",
},
new ListItem(
new AnonymousCommand(() => { })
{
Result = CommandResult.Confirm(confirmOnceArgs),
})
{
Title = "Confirm before doing something",
},
new ListItem(
new AnonymousCommand(() => { })
{
Result = CommandResult.Confirm(confirmTwiceArgs),
})
{
Title = "Confirm twice before doing something",
}
];
}
}

View File

@@ -1,14 +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.
namespace Microsoft.CmdPal.Common.Contracts;
public interface IFileService
{
T Read<T>(string folderPath, string fileName);
void Save<T>(string folderPath, string fileName, T content);
void Delete(string folderPath, string fileName);
}

View File

@@ -1,16 +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.Threading.Tasks;
namespace Microsoft.CmdPal.Common.Contracts;
public interface ILocalSettingsService
{
Task<bool> HasSettingAsync(string key);
Task<T?> ReadSettingAsync<T>(string key);
Task SaveSettingAsync<T>(string key, T value);
}

View File

@@ -1,40 +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 Microsoft.CmdPal.Common.Services;
using Microsoft.UI.Xaml;
namespace Microsoft.CmdPal.Common.Extensions;
/// <summary>
/// Extension class implementing extension methods for <see cref="Application"/>.
/// </summary>
public static class ApplicationExtensions
{
/// <summary>
/// Get registered services at the application level from anywhere in the
/// application.
///
/// Note:
/// https://learn.microsoft.com/uwp/api/windows.ui.xaml.application.current?view=winrt-22621#windows-ui-xaml-application-current
/// "Application is a singleton that implements the static Current property
/// to provide shared access to the Application instance for the current
/// application. The singleton pattern ensures that state managed by
/// Application, including shared resources and properties, is available
/// from a single, shared location."
///
/// Example of usage:
/// <code>
/// Application.Current.GetService<T>()
/// </code>
/// </summary>
/// <typeparam name="T">Service type.</typeparam>
/// <param name="application">Current application.</param>
/// <returns>Service reference.</returns>
public static T GetService<T>(this Application application)
where T : class
{
return (application as IApp)!.GetService<T>();
}
}

View File

@@ -1,40 +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 Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Microsoft.CmdPal.Common.Extensions;
public static class IHostExtensions
{
/// <summary>
/// <inheritdoc cref="ActivatorUtilities.CreateInstance(IServiceProvider, Type, object[])"/>
/// </summary>
public static T CreateInstance<T>(this IHost host, params object[] parameters)
{
return ActivatorUtilities.CreateInstance<T>(host.Services, parameters);
}
/// <summary>
/// Gets the service object for the specified type, or throws an exception
/// if type was not registered.
/// </summary>
/// <typeparam name="T">Service type</typeparam>
/// <param name="host">Host object</param>
/// <returns>Service object</returns>
/// <exception cref="ArgumentException">Throw an exception if the specified
/// type is not registered</exception>
public static T GetService<T>(this IHost host)
where T : class
{
if (host.Services.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices.");
}
return service;
}
}

View File

@@ -1,36 +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.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Common.Helpers;
public static class Json
{
public static async Task<T> ToObjectAsync<T>(string value)
{
if (typeof(T) == typeof(bool))
{
return (T)(object)bool.Parse(value);
}
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(value));
return (await JsonSerializer.DeserializeAsync<T>(stream))!;
}
public static async Task<string> StringifyAsync<T>(T value)
{
if (typeof(T) == typeof(bool))
{
return value!.ToString()!.ToLowerInvariant();
}
await using var stream = new MemoryStream();
await JsonSerializer.SerializeAsync(stream, value);
return Encoding.UTF8.GetString(stream.ToArray());
}
}

View File

@@ -9,7 +9,7 @@ using Microsoft.UI.Dispatching;
namespace Microsoft.CmdPal.Common.Helpers;
public static class NativeEventWaiter
public static partial class NativeEventWaiter
{
public static void WaitForEventLoop(string eventName, Action callback)
{

View File

@@ -9,7 +9,7 @@ using Windows.Win32.Foundation;
namespace Microsoft.CmdPal.Common.Helpers;
public static class RuntimeHelper
public static partial class RuntimeHelper
{
public static bool IsMSIX
{

View File

@@ -4,6 +4,6 @@
namespace Microsoft.CmdPal.Common.Messages;
public record HideWindowMessage()
public partial record HideWindowMessage()
{
}

View File

@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Common</RootNamespace>
<Nullable>enable</Nullable>

View File

@@ -1,18 +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.
namespace Microsoft.CmdPal.Common.Models;
public class LocalSettingsOptions
{
public string? ApplicationDataFolder
{
get; set;
}
public string? LocalSettingsFile
{
get; set;
}
}

View File

@@ -1,16 +1,7 @@
EnableWindow
CoCreateInstance
FileOpenDialog
FileSaveDialog
IFileOpenDialog
IFileSaveDialog
SHCreateItemFromParsingName
GetCurrentPackageFullName
SetWindowLong
GetWindowLong
WINDOW_EX_STYLE
SHLoadIndirectString
StrFormatByteSizeEx
SFBS_FLAGS
MAX_PATH
GetDpiForWindow

View File

@@ -1,48 +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.IO;
using System.Text;
using System.Text.Json;
using Microsoft.CmdPal.Common.Contracts;
namespace Microsoft.CmdPal.Common.Services;
public class FileService : IFileService
{
private static readonly Encoding _encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
#pragma warning disable CS8603 // Possible null reference return.
public T Read<T>(string folderPath, string fileName)
{
var path = Path.Combine(folderPath, fileName);
if (File.Exists(path))
{
using var fileStream = File.OpenText(path);
return JsonSerializer.Deserialize<T>(fileStream.BaseStream);
}
return default;
}
#pragma warning restore CS8603 // Possible null reference return.
public void Save<T>(string folderPath, string fileName, T content)
{
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
}
var fileContent = JsonSerializer.Serialize(content);
File.WriteAllText(Path.Combine(folderPath, fileName), fileContent, _encoding);
}
public void Delete(string folderPath, string fileName)
{
if (fileName != null && File.Exists(Path.Combine(folderPath, fileName)))
{
File.Delete(Path.Combine(folderPath, fileName));
}
}
}

View File

@@ -1,18 +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.
namespace Microsoft.CmdPal.Common.Services;
/// <summary>
/// Interface for the current application singleton object exposing the API
/// that can be accessed from anywhere in the application.
/// </summary>
public interface IApp
{
/// <summary>
/// Gets services registered at the application level.
/// </summary>
public T GetService<T>()
where T : class;
}

View File

@@ -1,120 +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.IO;
using System.Threading.Tasks;
using Microsoft.CmdPal.Common.Contracts;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Models;
using Microsoft.Extensions.Options;
using Windows.Storage;
namespace Microsoft.CmdPal.Common.Services;
public class LocalSettingsService : ILocalSettingsService
{
// TODO! for now, we're hardcoding the path as effectively:
// %localappdata%\CmdPal\LocalSettings.json
private const string DefaultApplicationDataFolder = "CmdPal";
private const string DefaultLocalSettingsFile = "LocalSettings.json";
private readonly IFileService _fileService;
private readonly LocalSettingsOptions _options;
private readonly string _localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
private readonly string _applicationDataFolder;
private readonly string _localSettingsFile;
private readonly bool _isMsix;
private Dictionary<string, object> _settings;
private bool _isInitialized;
public LocalSettingsService(IFileService fileService, IOptions<LocalSettingsOptions> options)
{
_isMsix = false; // RuntimeHelper.IsMSIX;
_fileService = fileService;
_options = options.Value;
_applicationDataFolder = Path.Combine(_localApplicationData, _options.ApplicationDataFolder ?? DefaultApplicationDataFolder);
_localSettingsFile = _options.LocalSettingsFile ?? DefaultLocalSettingsFile;
_settings = new Dictionary<string, object>();
}
private async Task InitializeAsync()
{
if (!_isInitialized)
{
_settings = await Task.Run(() => _fileService.Read<Dictionary<string, object>>(_applicationDataFolder, _localSettingsFile)) ?? new Dictionary<string, object>();
_isInitialized = true;
}
}
public async Task<bool> HasSettingAsync(string key)
{
if (_isMsix)
{
return ApplicationData.Current.LocalSettings.Values.ContainsKey(key);
}
else
{
await InitializeAsync();
if (_settings != null)
{
return _settings.ContainsKey(key);
}
}
return false;
}
public async Task<T?> ReadSettingAsync<T>(string key)
{
if (_isMsix)
{
if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out var obj))
{
return await Json.ToObjectAsync<T>((string)obj);
}
}
else
{
await InitializeAsync();
if (_settings != null && _settings.TryGetValue(key, out var obj))
{
var s = obj.ToString();
if (s != null)
{
return await Json.ToObjectAsync<T>(s);
}
}
}
return default;
}
public async Task SaveSettingAsync<T>(string key, T value)
{
if (_isMsix)
{
ApplicationData.Current.LocalSettings.Values[key] = await Json.StringifyAsync(value!);
}
else
{
await InitializeAsync();
_settings[key] = await Json.StringifyAsync(value!);
await Task.Run(() => _fileService.Save(_applicationDataFolder, _localSettingsFile, _settings));
}
}
}

View File

@@ -49,7 +49,7 @@ public partial class AppStateModel : ObservableObject
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, _deserializerOptions);
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, JsonSerializationContext.Default.AppStateModel);
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
@@ -73,7 +73,7 @@ public partial class AppStateModel : ObservableObject
try
{
// Serialize the main dictionary to JSON and save it to the file
var settingsJson = JsonSerializer.Serialize(model, _serializerOptions);
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel);
// Is it valid JSON?
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
@@ -89,7 +89,7 @@ public partial class AppStateModel : ObservableObject
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
}
var serialized = savedSettings.ToJsonString(_serializerOptions);
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
@@ -122,18 +122,19 @@ public partial class AppStateModel : ObservableObject
return Path.Combine(directory, "state.json");
}
private static readonly JsonSerializerOptions _serializerOptions = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
// [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
// private static readonly JsonSerializerOptions _serializerOptions = new()
// {
// WriteIndented = true,
// Converters = { new JsonStringEnumConverter() },
// };
private static readonly JsonSerializerOptions _deserializerOptions = new()
{
PropertyNameCaseInsensitive = true,
IncludeFields = true,
AllowTrailingCommas = true,
PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
ReadCommentHandling = JsonCommentHandling.Skip,
};
// private static readonly JsonSerializerOptions _deserializerOptions = new()
// {
// PropertyNameCaseInsensitive = true,
// IncludeFields = true,
// AllowTrailingCommas = true,
// PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
// ReadCommentHandling = JsonCommentHandling.Skip,
// };
}

View File

@@ -4,9 +4,9 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -25,6 +25,8 @@ public partial class CommandBarViewModel : ObservableObject,
field = value;
SetSelectedItem(value);
OnPropertyChanged(nameof(SelectedItem));
}
}
@@ -47,7 +49,9 @@ public partial class CommandBarViewModel : ObservableObject,
public partial PageViewModel? CurrentPage { get; set; }
[ObservableProperty]
public partial ObservableCollection<CommandContextItemViewModel> ContextCommands { get; set; } = [];
public partial ObservableCollection<ContextMenuStackViewModel> ContextMenuStack { get; set; } = [];
public ContextMenuStackViewModel? ContextMenu => ContextMenuStack.LastOrDefault();
public CommandBarViewModel()
{
@@ -100,35 +104,97 @@ public partial class CommandBarViewModel : ObservableObject,
if (SelectedItem.MoreCommands.Count() > 1)
{
ShouldShowContextMenu = true;
ContextCommands = [.. SelectedItem.AllCommands];
ContextMenuStack.Clear();
ContextMenuStack.Add(new ContextMenuStackViewModel(SelectedItem));
OnPropertyChanged(nameof(ContextMenu));
}
else
{
ShouldShowContextMenu = false;
}
OnPropertyChanged(nameof(HasSecondaryCommand));
OnPropertyChanged(nameof(SecondaryCommand));
OnPropertyChanged(nameof(ShouldShowContextMenu));
}
// InvokeItemCommand is what this will be in Xaml due to source generator
// this comes in when an item in the list is tapped
[RelayCommand]
private void InvokeItem(CommandContextItemViewModel item) =>
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
// [RelayCommand]
public ContextKeybindingResult InvokeItem(CommandContextItemViewModel item) =>
PerformCommand(item);
// this comes in when the primary button is tapped
public void InvokePrimaryCommand()
{
if (PrimaryCommand != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
}
PerformCommand(SecondaryCommand);
}
// this comes in when the secondary button is tapped
public void InvokeSecondaryCommand()
{
if (SecondaryCommand != null)
PerformCommand(SecondaryCommand);
}
public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var matchedItem = ContextMenu?.CheckKeybinding(ctrl, alt, shift, win, key);
return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
}
private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
{
if (command == null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
return ContextKeybindingResult.Unhandled;
}
if (command.HasMoreCommands)
{
ContextMenuStack.Add(new ContextMenuStackViewModel(command));
OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
return ContextKeybindingResult.KeepOpen;
}
else
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(command.Command.Model, command.Model));
return ContextKeybindingResult.Hide;
}
}
public bool CanPopContextStack()
{
return ContextMenuStack.Count > 1;
}
public void PopContextStack()
{
if (ContextMenuStack.Count > 1)
{
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
}
public void ClearContextStack()
{
while (ContextMenuStack.Count > 1)
{
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
}
}
public enum ContextKeybindingResult
{
Unhandled,
Hide,
KeepOpen,
}

View File

@@ -9,12 +9,16 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context)
{
private readonly KeyChord nullKeyChord = new(0, 0, 0);
public new ExtensionObject<ICommandContextItem> Model { get; } = new(contextItem);
public bool IsCritical { get; private set; }
public KeyChord? RequestedShortcut { get; private set; }
public bool HasRequestedShortcut => RequestedShortcut != null && (RequestedShortcut.Value != nullKeyChord);
public override void InitializeProperties()
{
if (IsInitialized)
@@ -31,6 +35,9 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
}
IsCritical = contextItem.IsCritical;
// I actually don't think this will ever actually be null, because
// KeyChord is a struct, which isn't nullable in WinRT
if (contextItem.RequestedShortcut != null)
{
RequestedShortcut = new(

View File

@@ -48,7 +48,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public List<CommandContextItemViewModel> MoreCommands { get; private set; } = [];
IEnumerable<CommandContextItemViewModel> ICommandBarContext.MoreCommands => MoreCommands;
IEnumerable<CommandContextItemViewModel> IContextMenuContext.MoreCommands => MoreCommands;
public bool HasMoreCommands => MoreCommands.Count > 0;
@@ -187,23 +187,26 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
// use Initialize straight up
MoreCommands.ForEach(contextItem =>
{
contextItem.InitializeProperties();
contextItem.SlowInitializeProperties();
});
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
if (!string.IsNullOrEmpty(model.Command.Name))
{
_itemTitle = Name,
Subtitle = Subtitle,
Command = Command,
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
{
_itemTitle = Name,
Subtitle = Subtitle,
Command = Command,
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
};
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
};
// Only set the icon on the context item for us if our command didn't
// have its own icon
if (!Command.HasIcon)
{
_defaultCommandContextItem._listItemIcon = _listItemIcon;
// Only set the icon on the context item for us if our command didn't
// have its own icon
if (!Command.HasIcon)
{
_defaultCommandContextItem._listItemIcon = _listItemIcon;
}
}
Initialized |= InitializedState.SelectionInitialized;

View File

@@ -13,12 +13,13 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
{
public CreatedExtensionForm(string name, string displayName, string path)
{
var serializeString = (string? s) => JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
TemplateJson = CardTemplate;
DataJson = $$"""
{
"name": {{JsonSerializer.Serialize(name)}},
"directory": {{JsonSerializer.Serialize(path)}},
"displayName": {{JsonSerializer.Serialize(displayName)}}
"name": {{serializeString(name)}},
"directory": {{serializeString(path)}},
"displayName": {{serializeString(displayName)}}
}
""";
_name = name;
@@ -28,13 +29,13 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
public override ICommandResult SubmitForm(string inputs, string data)
{
JsonObject? dataInput = JsonNode.Parse(data)?.AsObject();
var dataInput = JsonNode.Parse(data)?.AsObject();
if (dataInput == null)
{
return CommandResult.KeepOpen();
}
string verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
var verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
return verb switch
{
"sln" => OpenSolution(),
@@ -47,7 +48,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
private ICommandResult OpenSolution()
{
string[] parts = [_path, _name, $"{_name}.sln"];
string pathToSolution = Path.Combine(parts);
var pathToSolution = Path.Combine(parts);
ShellHelpers.OpenInShell(pathToSolution);
return CommandResult.Hide();
}
@@ -55,7 +56,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
private ICommandResult OpenDirectory()
{
string[] parts = [_path, _name];
string pathToDir = Path.Combine(parts);
var pathToDir = Path.Combine(parts);
ShellHelpers.OpenInShell(pathToDir);
return CommandResult.Hide();
}

View File

@@ -194,9 +194,8 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
Directory.Delete(tempDir, true);
}
private string FormatJsonString(string str)
{
private string FormatJsonString(string str) =>
// Escape the string for JSON
return JsonSerializer.Serialize(str);
}
JsonSerializer.Serialize(str, JsonSerializationContext.Default.String);
}

View File

@@ -51,15 +51,16 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
// If we fail to parse the card JSON, then display _our own card_
// with the exception
AdaptiveCardTemplate template = new(ErrorCardJson);
var serializeString = (string? s) => JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
// todo: we could probably stick Card.Errors in there too
var dataJson = $$"""
{
"error_message": {{JsonSerializer.Serialize(e.Message)}},
"error_stack": {{JsonSerializer.Serialize(e.StackTrace)}},
"inner_exception": {{JsonSerializer.Serialize(e.InnerException?.Message)}},
"template_json": {{JsonSerializer.Serialize(TemplateJson)}},
"data_json": {{JsonSerializer.Serialize(DataJson)}}
"error_message": {{serializeString(e.Message)}},
"error_stack": {{serializeString(e.StackTrace)}},
"inner_exception": {{serializeString(e.InnerException?.Message)}},
"template_json": {{serializeString(TemplateJson)}},
"data_json": {{serializeString(DataJson)}}
}
""";
var cardJson = template.Expand(dataJson);

View File

@@ -167,7 +167,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
Commands.ForEach(contextItem =>
{
contextItem.InitializeProperties();
contextItem.SlowInitializeProperties();
});
}
else

View File

@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ContextMenuStackViewModel : ObservableObject
{
[ObservableProperty]
public partial ObservableCollection<CommandContextItemViewModel> FilteredItems { get; set; }
private readonly IContextMenuContext _context;
private string _lastSearchText = string.Empty;
// private Dictionary<KeyChord, CommandContextItemViewModel>? _contextKeybindings;
public ContextMenuStackViewModel(IContextMenuContext context)
{
_context = context;
FilteredItems = [.. context.AllCommands];
}
public void SetSearchText(string searchText)
{
if (searchText == _lastSearchText)
{
return;
}
_lastSearchText = searchText;
var commands = _context.AllCommands.Where(c => c.ShouldBeVisible);
if (string.IsNullOrEmpty(searchText))
{
ListHelpers.InPlaceUpdateList(FilteredItems, commands);
return;
}
var newResults = ListHelpers.FilterList<CommandContextItemViewModel>(commands, searchText, ScoreContextCommand);
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
}
private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
{
if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
{
return 1;
}
if (string.IsNullOrEmpty(item.Title))
{
return 0;
}
var nameMatch = StringMatcher.FuzzySearch(query, item.Title);
var descriptionMatch = StringMatcher.FuzzySearch(query, item.Subtitle);
return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
}
public CommandContextItemViewModel? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var keybindings = _context.Keybindings();
if (keybindings != null)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (keybindings.TryGetValue(pressedKeyChord, out var item))
{
return item;
}
}
return null;
}
}

View File

@@ -52,6 +52,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
public string SearchText { get; private set; } = string.Empty;
public string InitialSearchText { get; private set; } = string.Empty;
public CommandItemViewModel EmptyContent { get; private set; }
private bool _isDynamic;
@@ -128,7 +130,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
try
{
IListItem[] newItems = _model.Unsafe!.GetItems();
var newItems = _model.Unsafe!.GetItems();
// Collect all the items into new viewmodels
Collection<ListItemViewModel> newViewModels = [];
@@ -136,7 +138,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
// TODO we can probably further optimize this by also keeping a
// HashSet of every ExtensionObject we currently have, and only
// building new viewmodels for the ones we haven't already built.
foreach (IListItem? item in newItems)
foreach (var item in newItems)
{
ListItemViewModel viewModel = new(item, new(this));
@@ -147,8 +149,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
}
}
IEnumerable<ListItemViewModel> firstTwenty = newViewModels.Take(20);
foreach (ListItemViewModel? item in firstTwenty)
var firstTwenty = newViewModels.Take(20);
foreach (var item in firstTwenty)
{
item?.SafeInitializeProperties();
}
@@ -233,7 +235,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
iterable = Items.ToArray();
}
foreach (ListItemViewModel item in iterable)
foreach (var item in iterable)
{
ct.ThrowIfCancellationRequested();
@@ -266,8 +268,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
return 1;
}
MatchResult nameMatch = StringMatcher.FuzzySearch(query, listItem.Title);
MatchResult descriptionMatch = StringMatcher.FuzzySearch(query, listItem.Subtitle);
var nameMatch = StringMatcher.FuzzySearch(query, listItem.Title);
var descriptionMatch = StringMatcher.FuzzySearch(query, listItem.Subtitle);
return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
}
@@ -280,7 +282,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
// Similarly stolen from ListHelpers.FilterList
public static IEnumerable<ListItemViewModel> FilterList(IEnumerable<ListItemViewModel> items, string query)
{
IOrderedEnumerable<ScoredListItemViewModel> scores = items
var scores = items
.Where(i => !i.IsInErrorState)
.Select(li => new ScoredListItemViewModel() { ViewModel = li, Score = ScoreListItem(query, li) })
.Where(score => score.Score > 0)
@@ -359,7 +361,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
{
base.InitializeProperties();
IListPage? model = _model.Unsafe;
var model = _model.Unsafe;
if (model == null)
{
return; // throw?
@@ -373,8 +375,9 @@ public partial class ListViewModel : PageViewModel, IDisposable
_modelPlaceholderText = model.PlaceholderText;
UpdateProperty(nameof(PlaceholderText));
SearchText = model.SearchText;
InitialSearchText = SearchText = model.SearchText;
UpdateProperty(nameof(SearchText));
UpdateProperty(nameof(InitialSearchText));
EmptyContent = new(new(model.EmptyContent), PageContext);
EmptyContent.SlowInitializeProperties();
@@ -385,7 +388,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
public void LoadMoreIfNeeded()
{
IListPage? model = this._model.Unsafe;
var model = this._model.Unsafe;
if (model == null)
{
return;
@@ -412,7 +415,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
{
base.FetchProperty(propertyName);
IListPage? model = this._model.Unsafe;
var model = this._model.Unsafe;
if (model == null)
{
return; // throw?
@@ -431,7 +434,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
break;
case nameof(EmptyContent):
EmptyContent = new(new(model.EmptyContent), PageContext);
EmptyContent.InitializeProperties();
EmptyContent.SlowInitializeProperties();
break;
case nameof(IsLoading):
UpdateEmptyContent();
@@ -449,6 +452,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
return;
}
UpdateProperty(nameof(EmptyContent));
DoOnUiThread(
() =>
{
@@ -475,13 +480,13 @@ public partial class ListViewModel : PageViewModel, IDisposable
lock (_listLock)
{
foreach (ListItemViewModel item in Items)
foreach (var item in Items)
{
item.SafeCleanup();
}
Items.Clear();
foreach (ListItemViewModel item in FilteredItems)
foreach (var item in FilteredItems)
{
item.SafeCleanup();
}
@@ -489,7 +494,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
FilteredItems.Clear();
}
IListPage? model = _model.Unsafe;
var model = _model.Unsafe;
if (model != null)
{
model.ItemsChanged -= Model_ItemsChanged;

View File

@@ -51,6 +51,12 @@ public record PerformCommandMessage
Context = context.Unsafe;
}
public PerformCommandMessage(CommandContextItemViewModel contextCommand)
{
Command = contextCommand.Command.Model;
Context = contextCommand.Model.Unsafe;
}
public PerformCommandMessage(ConfirmResultViewModel vm)
{
Command = vm.PrimaryCommand.Model;

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record TryCommandKeybindingMessage(bool Ctrl, bool Alt, bool Shift, bool Win, VirtualKey Key)
{
public bool Handled { get; set; }
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
@@ -13,22 +14,42 @@ public record UpdateCommandBarMessage(ICommandBarContext? ViewModel)
{
}
// Represents everything the command bar needs to know about to show command
// buttons at the bottom.
//
// This is implemented by both ListItemViewModel and ContentPageViewModel,
// the two things with sub-commands.
public interface ICommandBarContext : INotifyPropertyChanged
public interface IContextMenuContext : INotifyPropertyChanged
{
public IEnumerable<CommandContextItemViewModel> MoreCommands { get; }
public bool HasMoreCommands { get; }
public List<CommandContextItemViewModel> AllCommands { get; }
/// <summary>
/// Generates a mapping of key -> command item for this particular item's
/// MoreCommands. (This won't include the primary Command, but it will
/// include the secondary one). This map can be used to quickly check if a
/// shortcut key was pressed
/// </summary>
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
/// that have a shortcut key set.</returns>
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
return MoreCommands
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
c => c);
}
}
// Represents everything the command bar needs to know about to show command
// buttons at the bottom.
//
// This is implemented by both ListItemViewModel and ContentPageViewModel,
// the two things with sub-commands.
public interface ICommandBarContext : IContextMenuContext
{
public string SecondaryCommandName { get; }
public CommandItemViewModel? PrimaryCommand { get; }
public CommandItemViewModel? SecondaryCommand { get; }
public List<CommandContextItemViewModel> AllCommands { get; }
}

View File

@@ -1,5 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -31,7 +33,7 @@
<ProjectReference Include="..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<ProjectReference Include="..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\Exts\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
@@ -67,4 +69,15 @@
</Content>
</ItemGroup>
<!-- Just mark it as AOT compatible. Do not publish with AOT now. We need fully test before we really publish it as AOT enabled-->
<!--<PropertyGroup>
<SelfContained>true</SelfContained>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<PublishTrimmed>true</PublishTrimmed>
<PublishSingleFile>true</PublishSingleFile>
--><!-- <DisableRuntimeMarshalling>true</DisableRuntimeMarshalling> --><!--
<PublishAot>true</PublishAot>
<EnableMsixTooling>true</EnableMsixTooling>
</PropertyGroup>-->
</Project>

View File

@@ -11,7 +11,7 @@ using Windows.Foundation.Collections;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public class ExtensionService : IExtensionService, IDisposable
public partial class ExtensionService : IExtensionService, IDisposable
{
public event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionAdded;

View File

@@ -12,6 +12,7 @@ using Windows.Win32;
using Windows.Win32.System.Com;
using WinRT;
// [assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public class ExtensionWrapper : IExtensionWrapper
@@ -113,25 +114,36 @@ public class ExtensionWrapper : IExtensionWrapper
// -2147467262: E_NOINTERFACE
// -2147024893: E_PATH_NOT_FOUND
var guid = typeof(IExtension).GUID;
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
if (hr.Value == -2147024893)
unsafe
{
Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
// We don't really need to throw this exception.
// We'll just return out nothing.
return;
if (hr.Value == -2147024893)
{
Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
// We don't really need to throw this exception.
// We'll just return out nothing.
return;
}
extensionPtr = Marshal.GetIUnknownForObject((nint)extensionObj);
if (hr < 0)
{
Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
Marshal.ThrowExceptionForHR(hr);
}
// extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
extensionPtr = (nint)extensionObj;
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
_extensionObject = MarshalInterface<IExtension>.FromAbi(extensionPtr);
}
extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
if (hr < 0)
{
Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
Marshal.ThrowExceptionForHR(hr);
}
_extensionObject = MarshalInterface<IExtension>.FromAbi(extensionPtr);
}
finally
{

View File

@@ -0,0 +1,4 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": false
}

View File

@@ -99,7 +99,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
//// Run on background thread from ListPage.xaml.cs
[RelayCommand]
private Task<bool> InitializeAsync()
internal Task<bool> InitializeAsync()
{
// TODO: We may want a SemaphoreSlim lock here.
@@ -182,6 +182,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
return; // throw?
}
var updateProperty = true;
switch (propertyName)
{
case nameof(Name):
@@ -198,9 +199,21 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
case nameof(Icon):
this.Icon = new(model.Icon);
break;
default:
updateProperty = false;
break;
}
UpdateProperty(propertyName);
// GH #38829: If we always UpdateProperty here, then there's a possible
// race condition, where we raise the PropertyChanged(SearchText)
// before the subclass actually retrieves the new SearchText from the
// model. In that race situation, if the UI thread handles the
// PropertyChanged before ListViewModel fetches the SearchText, it'll
// think that the old search text is the _new_ value.
if (updateProperty)
{
UpdateProperty(propertyName);
}
}
public new void ShowException(Exception ex, string? extensionHint = null)

View File

@@ -10,7 +10,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class RecentCommandsManager : ObservableObject
{
[JsonInclude]
private List<HistoryItem> History { get; set; } = [];
internal List<HistoryItem> History { get; set; } = [];
public RecentCommandsManager()
{

View File

@@ -26,6 +26,8 @@ public partial class SettingsModel : ObservableObject
public HotkeySettings? Hotkey { get; set; } = DefaultActivationShortcut;
public bool UseLowLevelGlobalHotkey { get; set; }
public bool ShowAppDetails { get; set; }
public bool HotkeyGoesHome { get; set; }
@@ -38,6 +40,8 @@ public partial class SettingsModel : ObservableObject
public bool ShowSystemTrayIcon { get; set; } = true;
public bool IgnoreShortcutWhenFullscreen { get; set; } = true;
public Dictionary<string, ProviderSettings> ProviderSettings { get; set; } = [];
public Dictionary<string, CommandAlias> Aliases { get; set; } = [];
@@ -89,7 +93,7 @@ public partial class SettingsModel : ObservableObject
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, _deserializerOptions);
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel);
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
@@ -113,7 +117,7 @@ public partial class SettingsModel : ObservableObject
try
{
// Serialize the main dictionary to JSON and save it to the file
var settingsJson = JsonSerializer.Serialize(model, _serializerOptions);
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.SettingsModel);
// Is it valid JSON?
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
@@ -129,7 +133,7 @@ public partial class SettingsModel : ObservableObject
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
}
var serialized = savedSettings.ToJsonString(_serializerOptions);
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
@@ -162,19 +166,34 @@ public partial class SettingsModel : ObservableObject
return Path.Combine(directory, "settings.json");
}
private static readonly JsonSerializerOptions _serializerOptions = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
// [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
// private static readonly JsonSerializerOptions _serializerOptions = new()
// {
// WriteIndented = true,
// Converters = { new JsonStringEnumConverter() },
// };
// private static readonly JsonSerializerOptions _deserializerOptions = new()
// {
// PropertyNameCaseInsensitive = true,
// IncludeFields = true,
// Converters = { new JsonStringEnumConverter() },
// AllowTrailingCommas = true,
// };
}
private static readonly JsonSerializerOptions _deserializerOptions = new()
{
PropertyNameCaseInsensitive = true,
IncludeFields = true,
Converters = { new JsonStringEnumConverter() },
AllowTrailingCommas = true,
};
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(HistoryItem))]
[JsonSerializable(typeof(SettingsModel))]
[JsonSerializable(typeof(AppStateModel))]
[JsonSerializable(typeof(List<HistoryItem>), TypeInfoPropertyName = "HistoryList")]
[JsonSerializable(typeof(Dictionary<string, object>), TypeInfoPropertyName = "Dictionary")]
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")]
internal sealed partial class JsonSerializationContext : JsonSerializerContext
{
}
public enum MonitorBehavior

View File

@@ -27,6 +27,17 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
public bool UseLowLevelGlobalHotkey
{
get => _settings.UseLowLevelGlobalHotkey;
set
{
_settings.UseLowLevelGlobalHotkey = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey)));
Save();
}
}
public bool ShowAppDetails
{
get => _settings.ShowAppDetails;
@@ -97,6 +108,16 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
public bool IgnoreShortcutWhenFullscreen
{
get => _settings.IgnoreShortcutWhenFullscreen;
set
{
_settings.IgnoreShortcutWhenFullscreen = value;
Save();
}
}
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)

View File

@@ -109,9 +109,12 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
// TODO GH #239 switch back when using the new MD text block
// _ = _queue.EnqueueAsync(() =>
_ = Task.Factory.StartNew(
() =>
async () =>
{
var result = (bool)viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
// bool f = await viewModel.InitializeCommand.ExecutionTask.;
// var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
// var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault<bool?>()!;
var result = await viewModel.InitializeAsync();
CurrentPage = viewModel; // result ? viewModel : null;
////LoadedState = result ? ViewModelLoadedState.Loaded : ViewModelLoadedState.Error;

View File

@@ -36,7 +36,7 @@
<!-- Template for actions in the mode actions dropdown button -->
<DataTemplate x:Key="ContextMenuViewModelTemplate" x:DataType="viewmodels:CommandContextItemViewModel">
<Grid>
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32" />
<ColumnDefinition Width="*" />
@@ -53,37 +53,16 @@
Grid.Column="1"
VerticalAlignment="Center"
Text="{x:Bind Title, Mode=OneWay}" />
<!--<TextBlock
<TextBlock
Grid.Column="2"
Margin="16,0,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Foreground="{ThemeResource MenuFlyoutItemKeyboardAcceleratorTextForeground}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />-->
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
</Grid>
</DataTemplate>
<animations:ImplicitAnimationSet x:Name="ShowAnimations">
<animations:OpacityAnimation
From="0"
To="1.0"
Duration="0:0:0.340" />
<animations:ScaleAnimation
From="0.85"
To="1"
Duration="0:0:0.350" />
</animations:ImplicitAnimationSet>
<animations:ImplicitAnimationSet x:Name="HideAnimations">
<animations:OpacityAnimation
From="1.0"
To="0"
Duration="0:0:0.240" />
<animations:ScaleAnimation
From="1"
To="0.85"
Duration="0:0:0.350" />
</animations:ImplicitAnimationSet>
</ResourceDictionary>
</UserControl.Resources>
@@ -92,9 +71,9 @@
Padding="4"
ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
@@ -135,9 +114,6 @@
<Button
x:Name="SettingsIconButton"
x:Uid="SettingsButton"
animations:Implicit.HideAnimations="{StaticResource HideAnimations}"
animations:Implicit.ShowAnimations="{StaticResource ShowAnimations}"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Style="{StaticResource SubtleButtonStyle}"
Tapped="SettingsIcon_Tapped"
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
@@ -157,6 +133,8 @@
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind CurrentPageViewModel.Title, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}" />
<StackPanel
Grid.Column="2"
@@ -167,10 +145,8 @@
<Button
x:Name="PrimaryButton"
Padding="6,4,4,4"
animations:Implicit.HideAnimations="{StaticResource HideAnimations}"
animations:Implicit.ShowAnimations="{StaticResource ShowAnimations}"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
AutomationProperties.Name="{x:Bind ViewModel.PrimaryCommand.Name, Mode=OneWay}"
Background="Transparent"
Style="{StaticResource SubtleButtonStyle}"
Tapped="PrimaryButton_Tapped"
@@ -198,10 +174,8 @@
<Button
x:Name="SecondaryButton"
Padding="6,4,4,4"
animations:Implicit.HideAnimations="{StaticResource HideAnimations}"
animations:Implicit.ShowAnimations="{StaticResource ShowAnimations}"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
AutomationProperties.Name="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}"
Style="{StaticResource SubtleButtonStyle}"
Tapped="SecondaryButton_Tapped"
Visibility="{x:Bind ViewModel.HasSecondaryCommand, Mode=OneWay}">
@@ -245,35 +219,48 @@
x:Name="MoreCommandsButton"
x:Uid="MoreCommandsButton"
Padding="4"
animations:Implicit.HideAnimations="{StaticResource HideAnimations}"
animations:Implicit.ShowAnimations="{StaticResource ShowAnimations}"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Content="{ui:FontIcon Glyph=&#xE712;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}"
ToolTipService.ToolTip="Ctrl+k"
ToolTipService.ToolTip="Ctrl+K"
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
<Button.Flyout>
<Flyout Placement="TopEdgeAlignedRight">
<ListView
x:Name="CommandsDropdown"
MinWidth="248"
Margin="-16,-12,-16,-12"
IsItemClickEnabled="True"
ItemClick="CommandsDropdown_ItemClick"
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ContextCommands, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="12,7,12,7" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
</ListView>
<Flyout
Closing="Flyout_Closing"
Opened="Flyout_Opened"
Placement="TopEdgeAlignedRight">
<StackPanel>
<ListView
x:Name="CommandsDropdown"
MinWidth="248"
Margin="-16,-12,-16,-12"
IsItemClickEnabled="True"
ItemClick="CommandsDropdown_ItemClick"
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ContextMenu.FilteredItems, Mode=OneWay}"
KeyDown="CommandsDropdown_KeyDown"
SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="12,7,12,7" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
</ListView>
<TextBox
x:Name="ContextFilterBox"
x:Uid="ContextFilterBox"
Margin="-12,12,-12,-12"
KeyDown="ContextFilterBox_KeyDown"
PreviewKeyDown="ContextFilterBox_PreviewKeyDown"
TextChanged="ContextFilterBox_TextChanged" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>

View File

@@ -6,18 +6,22 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.Views;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Windows.System;
using Windows.UI.Core;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class CommandBar : UserControl,
IRecipient<OpenContextMenuMessage>,
IRecipient<TryCommandKeybindingMessage>,
ICurrentPageAware
{
public CommandBarViewModel ViewModel { get; set; } = new();
public CommandBarViewModel ViewModel { get; } = new();
public PageViewModel? CurrentPageViewModel
{
@@ -35,6 +39,9 @@ public sealed partial class CommandBar : UserControl,
// RegisterAll isn't AOT compatible
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
public void Receive(OpenContextMenuMessage message)
@@ -49,8 +56,41 @@ public sealed partial class CommandBar : UserControl,
ShowMode = FlyoutShowMode.Standard,
};
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
CommandsDropdown.SelectedIndex = 0;
CommandsDropdown.Focus(FocusState.Programmatic);
UpdateUiForStackChange();
}
public void Receive(TryCommandKeybindingMessage msg)
{
if (!ViewModel.ShouldShowContextMenu)
{
return;
}
var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key);
if (result == ContextKeybindingResult.Hide)
{
msg.Handled = true;
}
else if (result == ContextKeybindingResult.KeepOpen)
{
if (!MoreCommandsButton.Flyout.IsOpen)
{
var options = new FlyoutShowOptions
{
ShowMode = FlyoutShowMode.Standard,
};
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
}
UpdateUiForStackChange();
msg.Handled = true;
}
else if (result == ContextKeybindingResult.Unhandled)
{
msg.Handled = false;
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
@@ -85,8 +125,160 @@ public sealed partial class CommandBar : UserControl,
{
if (e.ClickedItem is CommandContextItemViewModel item)
{
ViewModel?.InvokeItemCommand.Execute(item);
MoreCommandsButton.Flyout.Hide();
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
{
MoreCommandsButton.Flyout.Hide();
}
else
{
UpdateUiForStackChange();
}
}
}
private void CommandsDropdown_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Handled)
{
return;
}
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
var result = ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
if (result == ContextKeybindingResult.Hide)
{
e.Handled = true;
MoreCommandsButton.Flyout.Hide();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
else if (result == ContextKeybindingResult.KeepOpen)
{
e.Handled = true;
}
else if (result == ContextKeybindingResult.Unhandled)
{
e.Handled = false;
}
}
private void Flyout_Opened(object sender, object e)
{
UpdateUiForStackChange();
}
private void Flyout_Closing(FlyoutBase sender, FlyoutBaseClosingEventArgs args)
{
ViewModel?.ClearContextStack();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var prop = e.PropertyName;
if (prop == nameof(ViewModel.ContextMenu))
{
UpdateUiForStackChange();
}
}
private void ContextFilterBox_TextChanged(object sender, TextChangedEventArgs e)
{
ViewModel.ContextMenu?.SetSearchText(ContextFilterBox.Text);
if (CommandsDropdown.SelectedIndex == -1)
{
CommandsDropdown.SelectedIndex = 0;
}
}
private void ContextFilterBox_KeyDown(object sender, KeyRoutedEventArgs e)
{
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
if (e.Key == VirtualKey.Enter)
{
if (CommandsDropdown.SelectedItem is CommandContextItemViewModel item)
{
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
{
MoreCommandsButton.Flyout.Hide();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
else
{
UpdateUiForStackChange();
}
e.Handled = true;
}
}
else if (e.Key == VirtualKey.Escape ||
(e.Key == VirtualKey.Left && altPressed))
{
if (ViewModel.CanPopContextStack())
{
ViewModel.PopContextStack();
UpdateUiForStackChange();
}
else
{
MoreCommandsButton.Flyout.Hide();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
e.Handled = true;
}
CommandsDropdown_KeyDown(sender, e);
}
private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Up)
{
// navigate previous
if (CommandsDropdown.SelectedIndex > 0)
{
CommandsDropdown.SelectedIndex--;
}
else
{
CommandsDropdown.SelectedIndex = CommandsDropdown.Items.Count - 1;
}
e.Handled = true;
}
else if (e.Key == VirtualKey.Down)
{
// navigate next
if (CommandsDropdown.SelectedIndex < CommandsDropdown.Items.Count - 1)
{
CommandsDropdown.SelectedIndex++;
}
else
{
CommandsDropdown.SelectedIndex = 0;
}
e.Handled = true;
}
}
private void UpdateUiForStackChange()
{
ContextFilterBox.Text = string.Empty;
ViewModel.ContextMenu?.SetSearchText(string.Empty);
CommandsDropdown.SelectedIndex = 0;
ContextFilterBox.Focus(FocusState.Programmatic);
}
}

View File

@@ -105,7 +105,9 @@ public sealed partial class SearchBar : UserControl,
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
if (ctrlPressed && e.Key == VirtualKey.Enter)
{
// ctrl+enter
@@ -164,6 +166,16 @@ public sealed partial class SearchBar : UserControl,
{
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
}
if (!e.Handled)
{
// The CommandBar is responsible for handling all the item keybindings,
// since the bound context item may need to then show another
// context menu
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
WeakReferenceMessenger.Default.Send(msg);
e.Handled = msg.Handled;
}
}
private void FilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
@@ -250,23 +262,36 @@ public sealed partial class SearchBar : UserControl,
private void Page_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var property = e.PropertyName;
if (CurrentPageViewModel is ListViewModel list &&
property == nameof(ListViewModel.SearchText))
{
// Only if the text actually changed...
// (sometimes this triggers on a round-trip of the SearchText)
if (FilterBox.Text != list.SearchText)
{
// ... Update our displayed text, and...
FilterBox.Text = list.SearchText;
// ... Move the cursor to the end of the input
FilterBox.Select(FilterBox.Text.Length, 0);
if (CurrentPageViewModel is ListViewModel list)
{
if (property == nameof(ListViewModel.SearchText))
{
// Only if the text actually changed...
// (sometimes this triggers on a round-trip of the SearchText)
if (FilterBox.Text != list.SearchText)
{
// ... Update our displayed text, and...
FilterBox.Text = list.SearchText;
// ... Move the cursor to the end of the input
FilterBox.Select(FilterBox.Text.Length, 0);
}
}
else if (property == nameof(ListViewModel.InitialSearchText))
{
// GH #38712:
// The ListPage will notify us of the `InitialSearchText` when
// we first load the viewmodel. We can use that as an
// opportunity to immediately select the search text. That lets
// the user start typing a new search without manually
// selecting the old one.
SelectSearch();
}
}
}
public void Receive(GoHomeMessage message) => ClearSearch();
public void Receive(FocusSearchBoxMessage message) => this.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
public void Receive(FocusSearchBoxMessage message) => FilterBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
}

View File

@@ -38,6 +38,7 @@
<Setter Property="IsTabStop" Value="False" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="AutomationProperties.AutomationControlType" Value="Custom" />
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
<Setter Property="Padding" Value="{ThemeResource TagPadding}" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
@@ -56,6 +57,7 @@
Padding="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
AutomationProperties.Name="{TemplateBinding Text}"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{TemplateBinding BorderBrush}"

View File

@@ -28,21 +28,22 @@
NotEmptyValue="Visible" />
<DataTemplate x:Key="TagTemplate" x:DataType="viewmodels:TagViewModel">
<ItemContainer>
<cpcontrols:Tag
BackgroundColor="{x:Bind Background, Mode=OneWay}"
FontSize="12"
ForegroundColor="{x:Bind Foreground, Mode=OneWay}"
Icon="{x:Bind Icon, Mode=OneWay}"
Text="{x:Bind Text, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" />
</ItemContainer>
<cpcontrols:Tag
AutomationProperties.Name="{x:Bind Text, Mode=OneWay}"
BackgroundColor="{x:Bind Background, Mode=OneWay}"
FontSize="12"
ForegroundColor="{x:Bind Foreground, Mode=OneWay}"
Icon="{x:Bind Icon, Mode=OneWay}"
Text="{x:Bind Text, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" />
</DataTemplate>
<!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
<DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="viewmodels:ListItemViewModel">
<Grid Padding="0,12,0,12" ColumnSpacing="12">
<Grid
Padding="0,12,0,12"
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="28" />
<ColumnDefinition Width="*" />
@@ -55,6 +56,7 @@
Width="20"
Height="20"
Margin="4,0,4,0"
AutomationProperties.AccessibilityView="Raw"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
@@ -136,14 +138,15 @@
</controls:Case>
<controls:Case Value="True">
<StackPanel
Margin="24"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Vertical"
Spacing="4">
<cpcontrols:IconBox
x:Name="IconBorder"
Width="56"
Height="56"
Width="48"
Height="48"
Margin="8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
@@ -152,11 +155,15 @@
Margin="0,4,0,0"
HorizontalAlignment="Center"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.EmptyContent.Title, Mode=OneWay}" />
Text="{x:Bind ViewModel.EmptyContent.Title, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.EmptyContent.Subtitle, Mode=OneWay}" />
Text="{x:Bind ViewModel.EmptyContent.Subtitle, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</controls:Case>
</controls:SwitchPresenter>

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using System.Security;
namespace Microsoft.CmdPal.UI.Helpers;
[SuppressUnmanagedCodeSecurity]
internal static class NativeMethods
{
[DllImport("shell32.dll")]
public static extern int SHQueryUserNotificationState(out UserNotificationState state);
}
internal enum UserNotificationState : int
{
QUNS_NOT_PRESENT = 1,
QUNS_BUSY,
QUNS_RUNNING_D3D_FULL_SCREEN,
QUNS_PRESENTATION_MODE,
QUNS_ACCEPTS_NOTIFICATIONS,
QUNS_QUIET_TIME,
QUNS_APP,
}

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