Compare commits

..

145 Commits

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

View File

@@ -29,6 +29,8 @@ shortcutguide
# 8LWXpg is user name but user folder causes a flag
LWXpg
# 0x6f677548 is user name but user folder causes a flag
x6f677548
Adoumie
Advaith
alekhyareddy

View File

@@ -121,10 +121,6 @@
^src/modules/MouseWithoutBorders/App/Helper/.*\.resx$
^src/modules/MouseWithoutBorders/ModuleInterface/generateSecurityDescriptor\.h$
^src/modules/peek/Peek.Common/NativeMethods\.txt$
^src/modules/peek/Peek.UITests/TestAssets/4\.qoi$
^src/modules/powerrename/PowerRenameUITest/testItems/folder1/testCase2\.txt$
^src/modules/powerrename/PowerRenameUITest/testItems/folder2/SpecialCase\.txt$
^src/modules/powerrename/PowerRenameUITest/testItems/testCase1\.txt$
^src/modules/previewpane/SvgPreviewHandler/SvgHTMLPreviewGenerator\.cs$
^src/modules/previewpane/UnitTests-MarkdownPreviewHandler/HelperFiles/MarkdownWithHTMLImageTag\.txt$
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$

View File

@@ -26,6 +26,8 @@ ADMINS
adml
admx
advancedpaste
advancedpasteui
advancedpasteuishortcut
advapi
advfirewall
AFeature
@@ -43,6 +45,7 @@ ALLINPUT
Allman
Allmodule
ALLOWUNDO
allpc
ALLVIEW
ALPHATYPE
AModifier
@@ -133,6 +136,7 @@ bla
BLACKFRAME
BLENDFUNCTION
Blockquotes
blogs
Blt
BLURBEHIND
BLURREGION
@@ -364,7 +368,6 @@ desktopshorcutinstalled
DESKTOPVERTRES
devblogs
devdocs
devenv
devmgmt
DEVMODE
DEVMODEW
@@ -508,6 +511,7 @@ FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
FARPROC
fesf
fff
FFFF
FILEEXPLORER
fileexploreraddons
@@ -580,7 +584,6 @@ GETSCREENSAVERRUNNING
GETSECKEY
GETSTICKYKEYS
GETTEXTLENGTH
gitmodules
GHND
GMEM
GNumber
@@ -668,7 +671,11 @@ Hostx
hotfixes
hotkeycontrol
HOTKEYF
hotkeylockmachine
hotkeyreconnect
hotkeys
hotkeyswitch
hotkeytoggleeasymouse
hotlight
hotspot
HPAINTBUFFER
@@ -727,6 +734,8 @@ IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT
imageresizerinput
imageresizersettings
imagetotext
imagetotextshortcut
imagingdevices
ime
imgflip
@@ -817,7 +826,6 @@ killrunner
kmph
kvp
Kybd
LARGEICON
lastcodeanalysissucceeded
LASTEXITCODE
LAYOUTRTL
@@ -846,7 +854,6 @@ linkid
LINKOVERLAY
LINQTo
listview
LIVEDRAW
LIVEZOOM
LLKH
llkhf
@@ -858,6 +865,7 @@ localappdata
localpackage
LOCALSYSTEM
LOCATIONCHANGE
LOCKMACHINE
LOCKTYPE
LOGFONT
LOGFONTW
@@ -866,6 +874,7 @@ LOGMSG
LOGPIXELSX
LOGPIXELSY
lng
LOn
lon
longdate
LONGNAMES
@@ -917,10 +926,12 @@ luid
LUMA
lusrmgr
LVal
lvm
LWA
lwin
LZero
MAGTRANSFORM
MAJORMINOR
MAKEINTRESOURCE
MAKEINTRESOURCEA
MAKEINTRESOURCEW
@@ -945,6 +956,7 @@ MDL
mdtext
mdtxt
mdwn
measuretool
meme
memicmp
MENUITEMINFO
@@ -994,6 +1006,7 @@ MOUSEHWHEEL
MOUSEINPUT
mousejump
mousepointer
mousepointercrosshairs
mouseutils
MOVESIZEEND
MOVESIZESTART
@@ -1038,6 +1051,7 @@ MWBEx
MYICON
NAMECHANGE
namespaceanddescendants
Namotion
nao
NCACTIVATE
ncc
@@ -1075,6 +1089,7 @@ NEWPLUSSHELLEXTENSIONWIN
newrow
nicksnettravels
NIF
NJson
NLog
NLSTEXT
NMAKE
@@ -1194,13 +1209,23 @@ PACL
PAINTSTRUCT
PALETTEWINDOW
PARENTNOTIFY
PARENTRELATIVE
PARENTRELATIVEEDITING
PARENTRELATIVEFORADDRESSBAR
PARENTRELATIVEFORUI
PARENTRELATIVEPARSING
parray
PARTIALCONFIRMATIONDIALOGTITLE
pasteashtmlfile
pasteashtmlfileshortcut
pasteasjson
pasteasjsonshortcut
pasteasmarkdown
pasteasmarkdownshortcut
pasteasplaintext
pasteasplaintextshortcut
pasteaspngfile
pasteaspngfileshortcut
pasteastxtfile
pasteastxtfileshortcut
PATCOPY
PATHMUSTEXIST
PATINVERT
@@ -1208,7 +1233,6 @@ PATPAINT
pbc
pbi
PBlob
pbrush
pcb
pcch
pcelt
@@ -1242,7 +1266,6 @@ pgp
pguid
phbm
phbmp
phicon
phwnd
pici
pidl
@@ -1251,7 +1274,6 @@ pinfo
pinvoke
pipename
PKBDLLHOOKSTRUCT
pkgfamily
plib
ploc
ploca
@@ -1271,6 +1293,7 @@ Pomodoro
Popups
POPUPWINDOW
POSITIONITEM
powerocr
POWERRENAMECONTEXTMENU
powerrenameinput
POWERRENAMETEST
@@ -1321,6 +1344,7 @@ PRODUCTVERSION
Progman
programdata
projectname
projitems
PROPERTYKEY
Propset
PROPVARIANT
@@ -1328,7 +1352,6 @@ PRTL
prvpane
psapi
pscid
pscustomobject
PSECURITY
psfgao
psfi
@@ -1414,6 +1437,7 @@ Removelnk
renamable
RENAMEONCOLLISION
reparented
reparenthotkey
reparenting
reportfileaccesses
requery
@@ -1439,6 +1463,7 @@ RIDEV
RIGHTSCROLLBAR
riid
RKey
Rns
RNumber
rop
ROUNDSMALL
@@ -1662,6 +1687,7 @@ STYLECHANGED
STYLECHANGING
subkeys
sublang
Subdomain
SUBMODULEUPDATE
subresource
Superbar
@@ -1734,6 +1760,7 @@ THICKFRAME
THEMECHANGED
THISCOMPONENT
throughs
thumbnailhotkey
TILEDWINDOW
TILLSON
timedate
@@ -1749,7 +1776,9 @@ tlbimp
tlc
tmain
TNP
TOGGLEEASYMOUSE
Toolhelp
toolkitconverters
toolwindow
TOPDOWNDIB
TOUCHEVENTF
@@ -1761,9 +1790,11 @@ tracelogging
tracerpt
trackbar
trafficmanager
transcodetomp
transicc
TRAYMOUSEMESSAGE
triaging
Tru
trl
trx
tsa
@@ -1799,6 +1830,7 @@ ULONGLONG
ums
uncompilable
UNCPRIORITY
undefining
UNDNAME
UNICODETEXT
unins
@@ -1965,7 +1997,6 @@ WMI
WMICIM
wmimgmt
wmp
wmsg
WMSYSCOMMAND
wnd
WNDCLASS
@@ -1979,7 +2010,6 @@ WORKSPACESEDITOR
WORKSPACESLAUNCHER
WORKSPACESSNAPSHOTTOOL
WORKSPACESWINDOWARRANGER
Worktree
wox
wparam
wpf

View File

@@ -1,10 +1,5 @@
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns
# marker to ignore all code on line
^.*/\* #no-spell-check-line \*/.*$
# marker for ignoring a comment to the end of the line
// #no-spell-check.*$
# Gaelic
Gàidhlig
@@ -269,7 +264,3 @@ St&yle
# This matches a relative clause where the relative pronoun "that" is omitted.
# Example: "Gets or sets the window the TitleBar should configure."
\bthe\s+\w+\s+the\b
# Usernames with numbers
# 0x6f677548 is user name but user folder causes a flag
\bx6f677548\b

View File

@@ -44,9 +44,6 @@ foreach ($csprojFile in $csprojFilesArray) {
if ($csprojFile -like '*Microsoft.CmdPal.Core.*.csproj') {
continue
}
if ($csprojFile -like '*Microsoft.CmdPal.Ext.Shell.csproj') {
continue
}
$importExists = Test-ImportSharedCsWinRTProps -filePath $csprojFile
if (!$importExists) {

View File

@@ -22,7 +22,7 @@
<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.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.251002-build.2316" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250910-build.2249" />
<PackageVersion Include="ControlzEx" Version="6.0.0" />
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />

View File

@@ -825,8 +825,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewModels.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UI.ViewModels.UnitTests\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj", "{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2997,14 +2995,6 @@ Global
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.ActiveCfg = Debug|ARM64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.Build.0 = Debug|ARM64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|x64.ActiveCfg = Debug|x64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|x64.Build.0 = Debug|x64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.ActiveCfg = Release|ARM64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.Build.0 = Release|ARM64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.ActiveCfg = Release|x64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3333,7 +3323,6 @@ Global
{3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477}
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94}
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -1,36 +1,32 @@
#include <windows.h>
#include "resource.h"
#include "../../../common/version/version.h"
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEVERSION 0,1,0,0
PRODUCTVERSION 0,1,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
FILEOS 0x40004L
FILETYPE 0x2L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BLOCK "040904b0"
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
VALUE "CompanyName", "Company Name"
VALUE "FileDescription", "Light Switch Module"
VALUE "FileVersion", "0.1.0.0"
VALUE "InternalName", "Light Switch"
VALUE "LegalCopyright", "Copyright (C) 2019 Company Name"
VALUE "OriginalFilename", "PowerToys.LightSwitchModuleInterface.dll"
VALUE "ProductName", "Light Switch"
VALUE "ProductVersion", "0.1.0.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
VALUE "Translation", 0x409, 1200
END
END

View File

@@ -108,7 +108,7 @@ public:
m_force_light_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_LIGHT");
m_force_dark_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_DARK");
m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
m_manual_override_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
init_settings();
};
@@ -460,7 +460,7 @@ public:
}
else if (hotkeyId == 0)
{
// get current will return true if in light mode; otherwise false
// get current will return true if in light mode, otherwise false
Logger::info(L"[Light Switch] Hotkey triggered: Toggle Theme");
if (g_settings.m_changeSystem)
{

View File

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

View File

@@ -8,9 +8,6 @@
#include <string>
#include <LightSwitchSettings.h>
#include <common/utils/gpo.h>
#include <logger/logger_settings.h>
#include <logger/logger.h>
#include <utils/logger_helper.h>
SERVICE_STATUS g_ServiceStatus = {};
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
@@ -38,8 +35,6 @@ int _tmain(int argc, TCHAR* argv[])
wchar_t serviceName[] = L"LightSwitchService";
SERVICE_TABLE_ENTRYW table[] = { { serviceName, ServiceMain }, { nullptr, nullptr } };
LoggerHelpers::init_logger(L"LightSwitch", L"Service", LogSettings::lightSwitchLoggerName);
if (!StartServiceCtrlDispatcherW(table))
{
DWORD err = GetLastError();
@@ -111,7 +106,6 @@ VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl)
SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
// Signal the service to stop
Logger::info(L"[LightSwitchService] Stop requested, signaling worker thread to exit.");
SetEvent(g_ServiceStopEvent);
break;
@@ -132,21 +126,13 @@ static void update_sun_times(auto& settings)
int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute;
int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute;
try
{
auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
values.add_property(L"lightTime", newLightTime);
values.add_property(L"darkTime", newDarkTime);
values.save_to_settings_file();
Logger::info(L"[LightSwitchService] Updated sun times and saved to config.");
}
catch (const std::exception& e)
{
std::wstring wmsg(e.what(), e.what() + strlen(e.what()));
Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg);
}
auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
values.add_property(L"lightTime", newLightTime);
values.add_property(L"darkTime", newDarkTime);
values.save_to_settings_file();
OutputDebugString(L"[LightSwitchService] Updated sun times and saved to config.\n");
}
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
@@ -156,8 +142,7 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
if (parentPid)
hParent = OpenProcess(SYNCHRONIZE, FALSE, parentPid);
Logger::info(L"[LightSwitchService] Worker thread starting...");
Logger::info(L"[LightSwitchService] Parent PID: {}", parentPid);
OutputDebugString(L"[LightSwitchService] Worker thread starting...\n");
// Initialize settings system
LightSwitchSettings::instance().InitFileWatcher();
@@ -229,19 +214,19 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
update_sun_times(settings);
g_lastUpdatedDay = st.wDay;
Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary.");
OutputDebugString(L"[LightSwitchService] Recalculated sun times at new day boundary.\n");
}
wchar_t msg[160];
swprintf_s(msg,
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d",
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d\n",
st.wHour,
st.wMinute,
settings.lightTime / 60,
settings.lightTime % 60,
settings.darkTime / 60,
settings.darkTime % 60);
Logger::info(msg);
OutputDebugString(msg);
// --- Manual override check ---
bool manualOverrideActive = false;
@@ -257,11 +242,11 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
nowMinutes == (settings.darkTime + settings.sunset_offset) % 1440)
{
ResetEvent(hManualOverride);
Logger::info(L"[LightSwitchService] Manual override cleared at boundary\n");
OutputDebugString(L"[LightSwitchService] Manual override cleared at boundary\n");
}
else
{
Logger::info(L"[LightSwitchService] Skipping schedule due to manual override\n");
OutputDebugString(L"[LightSwitchService] Skipping schedule due to manual override\n");
goto sleep_until_next_minute;
}
}
@@ -276,17 +261,10 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
msToNextMinute = 50;
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute);
if (wait == WAIT_OBJECT_0)
{
Logger::info(L"[LightSwitchService] Stop event triggered <20> exiting worker loop.");
if (wait == WAIT_OBJECT_0) // stop event
break;
}
if (hParent && wait == WAIT_OBJECT_0 + 1) // parent process exited
{
Logger::info(L"[LightSwitchService] Parent process exited <20> stopping service.");
if (hParent && wait == WAIT_OBJECT_0 + 1) // parent exited
break;
}
}
if (hManualOverride)
@@ -304,8 +282,8 @@ int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
wchar_t msg[160];
swprintf_s(
msg,
L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
Logger::info(msg);
L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.\n");
OutputDebugString(msg);
return 0;
}

View File

@@ -28,6 +28,19 @@
<ProjectName>LightSwitchService</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
@@ -41,25 +54,84 @@
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
<TargetName>PowerToys.LightSwitchService</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>
./../;
..\..\..\common;
..\..\..\common\logger;
..\..\..\common\utils;
..\..\..\common\SettingsAPI;
..\..\..\common\Telemetry;
..\..\..\common;
..\..\..\;
..\..\..\..\deps\spdlog\include;
./;
@@ -73,27 +145,8 @@
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="LightSwitchService.cpp" />
<ClCompile Include="LightSwitchSettings.cpp" />
<ClCompile Include="SettingsConstants.cpp" />
<ClCompile Include="ThemeHelper.cpp" />
<ClCompile Include="ThemeScheduler.cpp" />
<ClCompile Include="WinHookEventIDs.cpp" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="LightSwitchService.rc" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="LightSwitchSettings.h" />
<ClInclude Include="SettingsConstants.h" />
<ClInclude Include="SettingsObserver.h" />
<ClInclude Include="ThemeHelper.h" />
<ClInclude Include="ThemeScheduler.h" />
<ClInclude Include="WinHookEventIDs.h" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj">
<Project>{4aed67b6-55fd-486f-b917-e543dee2cb3c}</Project>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
@@ -105,10 +158,62 @@
<Project>{8f021b46-362b-485c-bfba-ccf83e820cbd}</Project>
</ProjectReference>
</ItemGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="LightSwitchService.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="LightSwitchSettings.cpp" />
<ClCompile Include="SettingsConstants.cpp" />
<ClCompile Include="ThemeHelper.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="ThemeScheduler.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="WinHookEventIDs.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="LightSwitchService.rc" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="LightSwitchSettings.h" />
<ClInclude Include="SettingsConstants.h" />
<ClInclude Include="SettingsObserver.h" />
<ClInclude Include="ThemeHelper.h" />
<ClInclude Include="ThemeScheduler.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">false</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="WinHookEventIDs.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -24,6 +24,15 @@
<ClCompile Include="ThemeHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\..\common\SettingsAPI\settings_helpers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\..\common\SettingsAPI\settings_objects.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\..\common\SettingsAPI\FileWatcher.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LightSwitchSettings.cpp">
<Filter>Source Files</Filter>
</ClCompile>
@@ -34,6 +43,9 @@
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="ThemeScheduler.h">
<Filter>Header Files</Filter>
@@ -57,9 +69,4 @@
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="LightSwitchService.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

View File

@@ -1,32 +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.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace LightSwitch.UITests
{
[TestClass]
public class TestUserSelectedLocation : UITestBase
{
public TestUserSelectedLocation()
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
{
}
[TestMethod("LightSwitch.UserSelectedLocation")]
[TestCategory("Location")]
public void TestUserSelectedLocationUpdate()
{
TestHelper.InitializeTest(this, "user selected location test");
TestHelper.PerformUserSelectedLocationTest(this);
TestHelper.CleanupTest(this);
}
}
}

View File

@@ -12,8 +12,8 @@ enum struct FindMyMouseActivationMethod : int
constexpr bool FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE = true;
// Default colors now include full alpha. Opacity is encoded directly in color alpha (legacy overlay_opacity migrated into A channel)
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(128, 0, 0, 0);
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(128, 255, 255, 255);
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 0, 0, 0);
const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255);
constexpr int FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS = 100;
constexpr int FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS = 500;
constexpr int FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM = 9;
@@ -43,4 +43,4 @@ int FindMyMouseMain(HINSTANCE hinst, const FindMyMouseSettings& settings);
void FindMyMouseDisable();
bool FindMyMouseIsEnabled();
void FindMyMouseApplySettings(const FindMyMouseSettings& settings);
HWND GetSonarHwnd() noexcept;
HWND GetSonarHwnd() noexcept;

View File

@@ -1055,13 +1055,8 @@ namespace MouseWithoutBorders.Class
if (machineId == 0)
{
var newMachineId = Common.Ran.Next();
_properties.MachineID.Value = newMachineId;
machineId = newMachineId;
if (!PauseInstantSaving)
{
SaveSettings();
}
_properties.MachineID.Value = Common.Ran.Next();
machineId = _properties.MachineID.Value;
}
}
@@ -1073,11 +1068,6 @@ namespace MouseWithoutBorders.Class
lock (_loadingSettingsLock)
{
_properties.MachineID.Value = value;
machineId = value;
if (!PauseInstantSaving)
{
SaveSettings();
}
}
}
}

View File

@@ -96,10 +96,7 @@ typedef struct {
#define SHALLOW_DESTROY 2
#define LIVE_DRAW_ZOOM 3
#define PEN_COLOR_HIGHLIGHT(Pencolor) ((Pencolor >> 24) != 0xFF)
#define PEN_COLOR_BLUR(Pencolor) ((Pencolor & 0x00FFFFFF) == COLOR_BLUR)
#define CURSOR_SAVE_MARGIN 4
#define PEN_COLOR_HIGHLIGHT(Pencolor) (Pencolor >> 24) != 0xFF
typedef BOOL (__stdcall *type_pGetMonitorInfo)(
@@ -146,14 +143,7 @@ typedef BOOL(__stdcall *type_pMagSetWindowFilterList)(
int count,
HWND* pHWND
);
typedef BOOL(__stdcall* type_pMagSetLensUseBitmapSmoothing)(
_In_ HWND,
_In_ BOOL
);
typedef BOOL(__stdcall* type_MagSetFullscreenUseBitmapSmoothing)(
BOOL fUseBitmapSmoothing
);
typedef BOOL(__stdcall* type_pMagInitialize)(VOID);
typedef BOOL (__stdcall *type_pMagInitialize)(VOID);
typedef BOOL(__stdcall *type_pGetPointerType)(
_In_ UINT32 pointerId,

View File

@@ -121,8 +121,8 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
BEGIN
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
LTEXT "ZoomIt v9.10",IDC_VERSION,42,7,73,10
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
LTEXT "ZoomIt v9.01",IDC_VERSION,42,7,73,10
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,166,8
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
"SysLink",WS_TABSTOP,42,26,150,9
ICON "APPICON",IDC_STATIC,12,9,20,20
@@ -149,8 +149,7 @@ BEGIN
CONTROL "",IDC_TIMER_POS7,"Button",BS_AUTORADIOBUTTON,63,108,10,10
CONTROL "",IDC_TIMER_POS8,"Button",BS_AUTORADIOBUTTON,79,108,10,10
CONTROL "",IDC_TIMER_POS9,"Button",BS_AUTORADIOBUTTON,97,108,10,10
CONTROL "Show background bitmap:",IDC_CHECK_BACKGROUND_FILE,
"Button",BS_AUTOCHECKBOX | WS_TABSTOP,3,122,99,10,WS_EX_RIGHT
CONTROL "Show background bitmap:",IDC_CHECK_BACKGROUND_FILE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,3,122,99,10,WS_EX_RIGHT
CONTROL "Use faded desktop as background",IDC_STATIC_DESKTOP_BACKGROUND,
"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,46,135,125,10
CONTROL "Use image file as background",IDC_STATIC_BACKGROUND_FILE,
@@ -166,25 +165,23 @@ BEGIN
CONTROL "",IDC_STATIC,"Static",SS_BLACKFRAME | SS_SUNKEN,7,196,193,1,WS_EX_CLIENTEDGE
END
ZOOM DIALOGEX 0, 0, 260, 170
ZOOM DIALOGEX 0, 0, 260, 158
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,59,57,80,12
LTEXT "After toggling ZoomIt you can zoom in with the mouse wheel or up and down arrow keys. Exit zoom mode with Escape or by pressing the right mouse button.",IDC_STATIC,7,6,246,26
LTEXT "Zoom Toggle:",IDC_STATIC,7,59,51,8
CONTROL "",IDC_ZOOM_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,53,118,150,15,WS_EX_TRANSPARENT
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,105,215,10
LTEXT "1.25",IDC_STATIC,52,136,16,8
LTEXT "1.5",IDC_STATIC,82,136,12,8
LTEXT "1.75",IDC_STATIC,108,136,16,8
LTEXT "2.0",IDC_STATIC,138,136,12,8
LTEXT "3.0",IDC_STATIC,164,136,12,8
LTEXT "4.0",IDC_STATIC,190,136,12,8
CONTROL "",IDC_ZOOM_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,53,104,150,15,WS_EX_TRANSPARENT
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,91,215,10
LTEXT "1.25",IDC_STATIC,52,122,16,8
LTEXT "1.5",IDC_STATIC,82,122,12,8
LTEXT "1.75",IDC_STATIC,108,122,16,8
LTEXT "2.0",IDC_STATIC,138,122,12,8
LTEXT "3.0",IDC_STATIC,164,122,12,8
LTEXT "4.0",IDC_STATIC,190,122,12,8
CONTROL "Animate zoom in and zoom out:",IDC_ANIMATE_ZOOM,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,74,116,10
CONTROL "Smooth zoomed image:",IDC_SMOOTH_IMAGE,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,88,116,10
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,148,246,17
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,6,34,246,18
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,34,246,17
END
DRAW DIALOGEX 0, 0, 260, 228
@@ -298,8 +295,7 @@ BEGIN
LTEXT "DemoType toggle:",IDC_STATIC,7,157,63,8
PUSHBUTTON "&...",IDC_DEMOTYPE_BROWSE,231,137,16,13
CONTROL "",IDC_DEMOTYPE_SPEED_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,52,202,150,11,WS_EX_TRANSPARENT
CONTROL "Drive input with typing:",IDC_DEMOTYPE_USER_DRIVEN,
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,173,88,10
CONTROL "Drive input with typing:",IDC_DEMOTYPE_USER_DRIVEN,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,173,88,10
LTEXT "DemoType typing speed:",IDC_STATIC,7,189,215,10
LTEXT "Slow",IDC_DEMOTYPE_STATIC1,51,213,18,8
LTEXT "Fast",IDC_DEMOTYPE_STATIC2,186,213,17,8
@@ -417,8 +413,8 @@ ACCELERATORS ACCELERATORS
BEGIN
"C", IDC_COPY, VIRTKEY, CONTROL, NOINVERT
"S", IDC_SAVE, VIRTKEY, CONTROL, NOINVERT
"C", IDC_COPY_CROP, VIRTKEY, SHIFT, CONTROL, NOINVERT
"S", IDC_SAVE_CROP, VIRTKEY, SHIFT, CONTROL, NOINVERT
"C", IDC_COPY_CROP, VIRTKEY, SHIFT, CONTROL, NOINVERT
"S", IDC_SAVE_CROP, VIRTKEY, SHIFT, CONTROL, NOINVERT
END

View File

@@ -14,7 +14,6 @@ DWORD g_SnipToggleKey = ((HOTKEYF_CONTROL) << 8) | '6';
DWORD g_ShowExpiredTime = 1;
DWORD g_SliderZoomLevel = 3;
BOOLEAN g_AnimateZoom = TRUE;
BOOLEAN g_SmoothImage = TRUE;
DWORD g_PenColor = COLOR_RED;
DWORD g_BreakPenColor = COLOR_RED;
DWORD g_RootPenWidth = PEN_WIDTH;
@@ -73,7 +72,6 @@ REG_SETTING RegSettings[] = {
{ L"ShowTrayIcon", SETTING_TYPE_BOOLEAN, 0, &g_ShowTrayIcon, static_cast<DOUBLE>(g_ShowTrayIcon) },
// NOTE: AnimateZoom is misspelled, but since it is a user setting stored in the registry we must continue to misspell it.
{ L"AnimnateZoom", SETTING_TYPE_BOOLEAN, 0, &g_AnimateZoom, static_cast<DOUBLE>(g_AnimateZoom) },
{ L"SmoothImage", SETTING_TYPE_BOOLEAN, 0, &g_SmoothImage, static_cast<DOUBLE>(g_SmoothImage) },
{ L"TelescopeZoomOut", SETTING_TYPE_BOOLEAN, 0, &g_TelescopeZoomOut, static_cast<DOUBLE>(g_TelescopeZoomOut) },
{ L"SnapToGrid", SETTING_TYPE_BOOLEAN, 0, &g_SnapToGrid, static_cast<DOUBLE>(g_SnapToGrid) },
{ L"ZoominSliderLevel", SETTING_TYPE_DWORD, 0, &g_SliderZoomLevel, static_cast<DOUBLE>(g_SliderZoomLevel) },

View File

@@ -170,8 +170,6 @@ type_pMagSetFullscreenTransform pMagSetFullscreenTransform;
type_pMagSetInputTransform pMagSetInputTransform;
type_pMagShowSystemCursor pMagShowSystemCursor;
type_pMagSetWindowFilterList pMagSetWindowFilterList;
type_MagSetFullscreenUseBitmapSmoothing pMagSetFullscreenUseBitmapSmoothing;
type_pMagSetLensUseBitmapSmoothing pMagSetLensUseBitmapSmoothing;
type_pMagInitialize pMagInitialize;
type_pDwmIsCompositionEnabled pDwmIsCompositionEnabled;
type_pGetPointerType pGetPointerType;
@@ -1101,8 +1099,6 @@ void DrawHighlightedShape( DWORD Shape, HDC hdcScreenCompat, Gdiplus::Brush *pBr
// Create a new bitmap that's the size of the area covered by the line + 2 * g_PenWidth
Gdiplus::Rect lineBounds(min(x1, x2), min(y1, y2), abs(x2 - x1), abs(y2 - y1));
OutputDebug(L"DrawHighlightedShape\n");
// Expand for line drawing
if (Shape == DRAW_LINE)
lineBounds.Inflate(static_cast<int>(g_PenWidth / 2), static_cast<int>(g_PenWidth / 2));
@@ -1190,7 +1186,7 @@ void DrawHighlightedShape( DWORD Shape, HDC hdcScreenCompat, Gdiplus::Brush *pBr
DeleteDC(hdcDIBOrig);
// Invalidate the updated rectangle
//InvalidateGdiplusRect(hWnd, lineBounds);
// InvalidateGdiplusRect(hWnd, lineBounds);
}
//----------------------------------------------------------------------------
@@ -1288,12 +1284,7 @@ void ScaleImage( HDC hdcDst, float xDst, float yDst, float wDst, float hDst,
{
Gdiplus::Bitmap srcBitmap( bmSrc, NULL );
// Use high quality interpolation when smooth image is enabled
if (g_SmoothImage) {
dstGraphics.SetInterpolationMode( Gdiplus::InterpolationModeHighQuality );
} else {
dstGraphics.SetInterpolationMode( Gdiplus::InterpolationModeLowQuality );
}
dstGraphics.SetInterpolationMode( Gdiplus::InterpolationModeLowQuality );
dstGraphics.SetPixelOffsetMode( Gdiplus::PixelOffsetModeHalf );
dstGraphics.DrawImage( &srcBitmap, Gdiplus::RectF(xDst,yDst,wDst,hDst), xSrc, ySrc, wSrc, hSrc, Gdiplus::UnitPixel );
@@ -2080,8 +2071,6 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
IsAutostartConfigured() ? BST_CHECKED: BST_UNCHECKED );
CheckDlgButton( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ANIMATE_ZOOM,
g_AnimateZoom ? BST_CHECKED: BST_UNCHECKED );
CheckDlgButton( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_SMOOTH_IMAGE,
g_SmoothImage ? BST_CHECKED: BST_UNCHECKED );
SendMessage( GetDlgItem(g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ZOOM_SLIDER), TBM_SETRANGE, false, MAKELONG(0,_countof(g_ZoomLevels)-1) );
SendMessage( GetDlgItem(g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ZOOM_SLIDER), TBM_SETPOS, true, g_SliderZoomLevel );
@@ -2221,7 +2210,6 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
}
g_ShowTrayIcon = IsDlgButtonChecked( hDlg, IDC_SHOW_TRAY_ICON ) == BST_CHECKED;
g_AnimateZoom = IsDlgButtonChecked( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ANIMATE_ZOOM ) == BST_CHECKED;
g_SmoothImage = IsDlgButtonChecked( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_SMOOTH_IMAGE ) == BST_CHECKED;
g_DemoTypeUserDriven = IsDlgButtonChecked( g_OptionsTabs[DEMOTYPE_PAGE].hPage, IDC_DEMOTYPE_USER_DRIVEN ) == BST_CHECKED;
newToggleKey = static_cast<DWORD>(SendMessage( GetDlgItem( g_OptionsTabs[ZOOM_PAGE].hPage, IDC_HOTKEY), HKM_GETHOTKEY, 0, 0 ));
@@ -2735,6 +2723,7 @@ VOID DrawShape( DWORD Shape, HDC hDc, RECT *Rect, bool UseGdiPlus = false )
bool isBlur = false;
Gdiplus::Graphics dstGraphics(hDc);
if( ( GetWindowLong( g_hWndMain, GWL_EXSTYLE ) & WS_EX_LAYERED ) == 0 )
{
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
@@ -2757,7 +2746,6 @@ VOID DrawShape( DWORD Shape, HDC hDc, RECT *Rect, bool UseGdiPlus = false )
InflateRect(Rect, g_PenWidth / 2, g_PenWidth / 2);
isBlur = true;
}
OutputDebug(L"Draw shape: highlight: %d pbrush: %d\n", PEN_COLOR_HIGHLIGHT(g_PenColor), pBrush != NULL);
switch (Shape) {
case DRAW_RECTANGLE:
@@ -2932,7 +2920,7 @@ void InvalidateCursorMoveArea( HWND hWnd, float zoomLevel, int width, int height
{
int x, y;
RECT rc;
int invWidth = g_PenWidth + CURSOR_SAVE_MARGIN;
int invWidth = g_PenWidth;
if( DrawHighlightedCursor( zoomLevel, width, height ) ) {
@@ -2957,7 +2945,7 @@ void InvalidateCursorMoveArea( HWND hWnd, float zoomLevel, int width, int height
void SaveCursorArea( HDC hDcTarget, HDC hDcSource, POINT pt )
{
OutputDebug( L"SaveCursorArea\n");
int penWidth = g_PenWidth + CURSOR_SAVE_MARGIN;
int penWidth = g_PenWidth + 2;
BitBlt( hDcTarget, 0, 0, penWidth +CURSOR_ARM_LENGTH*2, penWidth +CURSOR_ARM_LENGTH*2,
hDcSource, static_cast<INT> (pt.x- penWidth /2)-CURSOR_ARM_LENGTH,
static_cast<INT>(pt.y- penWidth /2)-CURSOR_ARM_LENGTH, SRCCOPY|CAPTUREBLT );
@@ -2971,7 +2959,7 @@ void SaveCursorArea( HDC hDcTarget, HDC hDcSource, POINT pt )
void RestoreCursorArea( HDC hDcTarget, HDC hDcSource, POINT pt )
{
OutputDebug( L"RestoreCursorArea\n");
int penWidth = g_PenWidth + CURSOR_SAVE_MARGIN;
int penWidth = g_PenWidth + 2;
BitBlt( hDcTarget, static_cast<INT>(pt.x- penWidth /2)-CURSOR_ARM_LENGTH,
static_cast<INT>(pt.y- penWidth /2)-CURSOR_ARM_LENGTH, penWidth +CURSOR_ARM_LENGTH*2,
penWidth + CURSOR_ARM_LENGTH*2, hDcSource, 0, 0, SRCCOPY|CAPTUREBLT );
@@ -4190,11 +4178,6 @@ LRESULT APIENTRY MainWndProc(
}
#endif
}
OutputDebug(L"LIVEDRAW SMOOTHING: %d\n", g_SmoothImage);
if (!pMagSetLensUseBitmapSmoothing(g_hWndLiveZoomMag, g_SmoothImage))
{
OutputDebug(L"MagSetLensUseBitmapSmoothing failed: %d\n", GetLastError());
}
if ( g_RecordToggle )
{
@@ -5313,8 +5296,6 @@ LRESULT APIENTRY MainWndProc(
if( g_Drawing ) {
OutputDebug(L"Mousemove: Drawing\n");
POINT currentPt;
// Are we in pen mode on a tablet?
@@ -5353,15 +5334,7 @@ LRESULT APIENTRY MainWndProc(
}
else
{
if (PEN_COLOR_HIGHLIGHT(g_PenColor))
{
// copy original bitmap to screen bitmap to erase previous highlight
BitBlt(hdcScreenCompat, 0, 0, bmp.bmWidth, bmp.bmHeight, drawUndoList->hDc, 0, 0, SRCCOPY | CAPTUREBLT);
}
else
{
DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle, PEN_COLOR_HIGHLIGHT(g_PenColor));
}
DrawShape( g_DrawingShape, hdcScreenCompat, &g_rcRectangle );
}
}
@@ -5407,7 +5380,7 @@ LRESULT APIENTRY MainWndProc(
g_rcRectangle.top != g_rcRectangle.bottom) {
// Draw the new target rectangle.
DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle, PEN_COLOR_HIGHLIGHT(g_PenColor));
DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle);
OutputDebug(L"SHAPE: (%d, %d) - (%d, %d)\n", g_rcRectangle.left, g_rcRectangle.top,
g_rcRectangle.right, g_rcRectangle.bottom);
}
@@ -5445,6 +5418,9 @@ LRESULT APIENTRY MainWndProc(
Gdiplus::BitmapData* lineData = LockGdiPlusBitmap(lineBitmap);
BYTE* pPixels = static_cast<BYTE*>(lineData->Scan0);
// Copy the contents of the screen bitmap to the temporary bitmap
GetOldestUndo(drawUndoList);
// Create a GDI bitmap that's the size of the lineBounds rectangle
Gdiplus::Bitmap *blurBitmap = CreateGdiplusBitmap( hdcScreenCompat, // oldestUndo->hDc,
lineBounds.X, lineBounds.Y, lineBounds.Width, lineBounds.Height);
@@ -5469,8 +5445,6 @@ LRESULT APIENTRY MainWndProc(
}
else if(PEN_COLOR_HIGHLIGHT(g_PenColor)) {
OutputDebug(L"HIGHLIGHT\n");
// This is a highlighting pen color
Gdiplus::Rect lineBounds = GetLineBounds(prevPt, currentPt, g_PenWidth);
Gdiplus::Bitmap* lineBitmap = DrawBitmapLine(lineBounds, prevPt, currentPt, &pen);
@@ -5810,30 +5784,26 @@ LRESULT APIENTRY MainWndProc(
if( !g_DrawingShape ) {
// If the point has changed, draw a line to it
if (!PEN_COLOR_HIGHLIGHT(g_PenColor))
{
if (prevPt.x != LOWORD(lParam) || prevPt.y != HIWORD(lParam))
if (prevPt.x != LOWORD(lParam) || prevPt.y != HIWORD(lParam)) {
Gdiplus::Graphics dstGraphics(hdcScreenCompat);
if ((GetWindowLong(g_hWndMain, GWL_EXSTYLE) & WS_EX_LAYERED) == 0)
{
Gdiplus::Graphics dstGraphics(hdcScreenCompat);
if ((GetWindowLong(g_hWndMain, GWL_EXSTYLE) & WS_EX_LAYERED) == 0)
{
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
}
Gdiplus::Color color = ColorFromColorRef(g_PenColor);
Gdiplus::Pen pen(color, static_cast<Gdiplus::REAL>(g_PenWidth));
Gdiplus::GraphicsPath path;
pen.SetLineJoin(Gdiplus::LineJoinRound);
path.AddLine(prevPt.x, prevPt.y, LOWORD(lParam), HIWORD(lParam));
dstGraphics.DrawPath(&pen, &path);
}
// Draw a dot at the current point, if the point hasn't changed
else
{
MoveToEx(hdcScreenCompat, prevPt.x, prevPt.y, NULL);
LineTo(hdcScreenCompat, LOWORD(lParam), HIWORD(lParam));
InvalidateRect(hWnd, NULL, FALSE);
dstGraphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
}
Gdiplus::Color color = ColorFromColorRef(g_PenColor);
Gdiplus::Pen pen(color, static_cast<Gdiplus::REAL>(g_PenWidth));
Gdiplus::GraphicsPath path;
pen.SetLineJoin(Gdiplus::LineJoinRound);
path.AddLine(prevPt.x, prevPt.y, LOWORD(lParam), HIWORD(lParam));
dstGraphics.DrawPath(&pen, &path);
}
// Draw a dot at the current point, if the point hasn't changed
else {
MoveToEx(hdcScreenCompat, prevPt.x, prevPt.y, NULL);
LineTo(hdcScreenCompat, LOWORD(lParam), HIWORD(lParam));
InvalidateRect(hWnd, NULL, FALSE);
}
prevPt.x = LOWORD( lParam );
prevPt.y = HIWORD( lParam );
@@ -5848,11 +5818,8 @@ LRESULT APIENTRY MainWndProc(
g_rcRectangle.left != g_rcRectangle.right ) {
// erase previous
if (!PEN_COLOR_HIGHLIGHT(g_PenColor))
{
SetROP2(hdcScreenCompat, R2_NOTXORPEN);
DrawShape(g_DrawingShape, hdcScreenCompat, &g_rcRectangle);
}
SetROP2(hdcScreenCompat, R2_NOTXORPEN);
DrawShape( g_DrawingShape, hdcScreenCompat, &g_rcRectangle );
// Draw the final shape
HBRUSH hBrush = static_cast<HBRUSH>(GetStockObject( NULL_BRUSH ));
@@ -6218,14 +6185,8 @@ LRESULT APIENTRY MainWndProc(
SetStretchBltMode( hInterimSaveDc, HALFTONE );
SetStretchBltMode( hSaveDc, HALFTONE );
#else
// Use HALFTONE for better quality when smooth image is enabled
if (g_SmoothImage) {
SetStretchBltMode( hInterimSaveDc, HALFTONE );
SetStretchBltMode( hSaveDc, HALFTONE );
} else {
SetStretchBltMode( hInterimSaveDc, COLORONCOLOR );
SetStretchBltMode( hSaveDc, COLORONCOLOR );
}
SetStretchBltMode( hInterimSaveDc, COLORONCOLOR );
SetStretchBltMode( hSaveDc, COLORONCOLOR );
#endif
StretchBlt( hInterimSaveDc,
0, 0,
@@ -6348,12 +6309,7 @@ LRESULT APIENTRY MainWndProc(
#if SCALE_HALFTONE
SetStretchBltMode( hSaveDc, HALFTONE );
#else
// Use HALFTONE for better quality when smooth image is enabled
if (g_SmoothImage) {
SetStretchBltMode( hSaveDc, HALFTONE );
} else {
SetStretchBltMode( hSaveDc, COLORONCOLOR );
}
SetStretchBltMode( hSaveDc, COLORONCOLOR );
#endif
StretchBlt( hSaveDc,
0, 0,
@@ -6690,8 +6646,8 @@ LRESULT APIENTRY MainWndProc(
(float)x, (float)y,
width/zoomLevel, height/zoomLevel );
} else {
// do a fast, less accurate render (but use smooth if enabled)
SetStretchBltMode( hDc, g_SmoothImage ? HALFTONE : COLORONCOLOR );
// do a fast, less accurate render
SetStretchBltMode( hDc, HALFTONE );
StretchBlt( ps.hdc,
0, 0,
bmp.bmWidth, bmp.bmHeight,
@@ -6704,12 +6660,7 @@ LRESULT APIENTRY MainWndProc(
#if SCALE_HALFTONE
SetStretchBltMode( hDc, zoomLevel == zoomTelescopeTarget ? HALFTONE : COLORONCOLOR );
#else
// Use HALFTONE for better quality when smooth image is enabled
if (g_SmoothImage) {
SetStretchBltMode( hDc, HALFTONE );
} else {
SetStretchBltMode( hDc, COLORONCOLOR );
}
SetStretchBltMode( hDc, COLORONCOLOR );
#endif
StretchBlt( ps.hdc,
0, 0,
@@ -6732,7 +6683,7 @@ LRESULT APIENTRY MainWndProc(
BITMAP local_bmp;
GetObject(g_hBackgroundBmp, sizeof(local_bmp), &local_bmp);
SetStretchBltMode( hdcScreenCompat, g_SmoothImage ? HALFTONE : COLORONCOLOR );
SetStretchBltMode( hdcScreenCompat, HALFTONE );
if( g_BreakBackgroundStretch ) {
StretchBlt( hdcScreenCompat, 0, 0, width, height,
g_hDcBackgroundFile, 0, 0, local_bmp.bmWidth, local_bmp.bmHeight, SRCCOPY|CAPTUREBLT );
@@ -6891,6 +6842,7 @@ LRESULT CALLBACK LiveZoomWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM
WS_CHILD | MS_SHOWMAGNIFIEDCURSOR | WS_VISIBLE,
0, 0, 0, 0, hWnd, NULL, g_hInstance, NULL );
}
ShowWindow( hWnd, SW_SHOW );
InvalidateRect( g_hWndLiveZoomMag, NULL, TRUE );
@@ -7603,10 +7555,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
"MagSetWindowTransform" );
pMagSetFullscreenTransform = (type_pMagSetFullscreenTransform)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
"MagSetFullscreenTransform");
pMagSetFullscreenUseBitmapSmoothing = (type_MagSetFullscreenUseBitmapSmoothing)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
"MagSetFullscreenUseBitmapSmoothing");
pMagSetLensUseBitmapSmoothing = (type_pMagSetLensUseBitmapSmoothing)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
"MagSetLensUseBitmapSmoothing");
pMagSetInputTransform = (type_pMagSetInputTransform)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),
"MagSetInputTransform");
pMagShowSystemCursor = (type_pMagShowSystemCursor)GetProcAddress(LoadLibrarySafe(L"magnification.dll", DLL_LOAD_LOCATION_SYSTEM),

View File

@@ -95,7 +95,6 @@
#define IDC_COPYRIGHT 1075
#define IDC_PEN_WIDTH 1105
#define IDC_TIMER 1106
#define IDC_SMOOTH_IMAGE 1107
#define IDC_SAVE 40002
#define IDC_COPY 40004
#define IDC_RECORD 40006
@@ -110,7 +109,7 @@
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 118
#define _APS_NEXT_COMMAND_VALUE 40013
#define _APS_NEXT_CONTROL_VALUE 1078
#define _APS_NEXT_CONTROL_VALUE 1076
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

View File

@@ -1,153 +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.Runtime.CompilerServices;
using Windows.Win32;
using Windows.Win32.Storage.FileSystem;
namespace Microsoft.CmdPal.Core.Common.Helpers;
public static class PathHelper
{
public static bool Exists(string path, out bool isDirectory)
{
isDirectory = false;
if (string.IsNullOrEmpty(path))
{
return false;
}
string? fullPath;
try
{
fullPath = Path.GetFullPath(path);
}
catch (Exception ex) when (ex is ArgumentException or IOException or UnauthorizedAccessException)
{
return false;
}
var result = ExistsCore(fullPath, out isDirectory);
if (result && IsDirectorySeparator(fullPath[^1]))
{
// Some sys-calls remove all trailing slashes and may give false positives for existing files.
// We want to make sure that if the path ends in a trailing slash, it's truly a directory.
return isDirectory;
}
return result;
}
/// <summary>
/// Normalize potential local/UNC file path text input: trim whitespace and surrounding quotes.
/// Windows file paths cannot contain quotes, but user input can include them.
/// </summary>
public static string Unquote(string? text)
{
return string.IsNullOrWhiteSpace(text) ? (text ?? string.Empty) : text.Trim().Trim('"');
}
/// <summary>
/// Quick heuristic to determine if the string looks like a Windows file path (UNC or drive-letter based).
/// </summary>
public static bool LooksLikeFilePath(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
// UNC path
if (path.StartsWith(@"\\", StringComparison.Ordinal))
{
// Win32 File Namespaces \\?\
if (path.StartsWith(@"\\?\", StringComparison.Ordinal))
{
return IsSlow(path[4..]);
}
// Basic UNC path validation: \\server\share or \\server\share\path
var parts = path[2..].Split('\\', StringSplitOptions.RemoveEmptyEntries);
return parts.Length >= 2; // At minimum: server and share
}
// Drive letter path (e.g., C:\ or C:)
return path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':';
}
/// <summary>
/// Validates path syntax without performing any I/O by using Path.GetFullPath.
/// </summary>
public static bool HasValidPathSyntax(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
try
{
_ = Path.GetFullPath(path);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Checks if a string represents a valid Windows file path (local or network)
/// using fast syntax validation only. Reuses LooksLikeFilePath and HasValidPathSyntax.
/// </summary>
public static bool IsValidFilePath(string? path)
{
return LooksLikeFilePath(path) && HasValidPathSyntax(path);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsDirectorySeparator(char c)
{
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
}
private static bool ExistsCore(string fullPath, out bool isDirectory)
{
var attributes = PInvoke.GetFileAttributes(fullPath);
var result = attributes != PInvoke.INVALID_FILE_ATTRIBUTES;
isDirectory = result && (attributes & (uint)FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) != 0;
return result;
}
public static bool IsSlow(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
try
{
var root = Path.GetPathRoot(path);
if (!string.IsNullOrEmpty(root))
{
if (root.Length > 2 && char.IsLetter(root[0]) && root[1] == ':')
{
return new DriveInfo(root).DriveType is not (DriveType.Fixed or DriveType.Ram);
}
else if (root.StartsWith(@"\\", StringComparison.Ordinal))
{
return !root.StartsWith(@"\\?\", StringComparison.Ordinal) || IsSlow(root[4..]);
}
}
return false;
}
catch
{
return false;
}
}
}

View File

@@ -13,7 +13,7 @@ namespace Microsoft.CmdPal.Core.Common.Helpers;
/// If ExecuteAsync is called while already executing, it cancels the current execution
/// and starts the operation again (superseding behavior).
/// </summary>
public sealed partial class SupersedingAsyncGate : IDisposable
public partial class SupersedingAsyncGate : IDisposable
{
private readonly Func<CancellationToken, Task> _action;
private readonly Lock _lock = new();

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.
namespace Microsoft.CmdPal.Core.Common.Helpers;
/// <summary>
/// An async gate that ensures only one value computation runs at a time.
/// If ExecuteAsync is called while already executing, it cancels the current computation
/// and starts the operation again (superseding behavior).
/// Once a value is successfully computed, it is applied (via the provided <see cref="Action{T}"/>).
/// The apply step uses its own lock so that long-running apply logic does not block the
/// computation / superseding pipeline, while still remaining serialized with respect to
/// other apply calls.
/// </summary>
/// <typeparam name="T">The type of the computed value.</typeparam>
public sealed partial class SupersedingAsyncValueGate<T> : IDisposable
{
private readonly Func<CancellationToken, Task<T>> _valueFactory;
private readonly Action<T> _apply;
private readonly Lock _lock = new(); // Controls scheduling / superseding
private readonly Lock _applyLock = new(); // Serializes application of results
private int _callId;
private TaskCompletionSource<T>? _currentTcs;
private CancellationTokenSource? _currentCancellationSource;
private Task? _executingTask;
public SupersedingAsyncValueGate(
Func<CancellationToken, Task<T>> valueFactory,
Action<T> apply)
{
ArgumentNullException.ThrowIfNull(valueFactory);
ArgumentNullException.ThrowIfNull(apply);
_valueFactory = valueFactory;
_apply = apply;
}
/// <summary>
/// Executes the configured value computation. If another execution is running, this call will
/// cancel the current execution and restart the computation. The returned task completes when
/// (and only if) the computation associated with this invocation completes (or is canceled / superseded).
/// </summary>
/// <param name="cancellationToken">Optional external cancellation token.</param>
/// <returns>The computed value for this invocation.</returns>
public async Task<T> ExecuteAsync(CancellationToken cancellationToken = default)
{
TaskCompletionSource<T> tcs;
lock (_lock)
{
// Supersede any in-flight computation.
_currentCancellationSource?.Cancel();
_currentTcs?.TrySetException(new OperationCanceledException("Superseded by newer call"));
tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
_currentTcs = tcs;
_callId++;
if (_executingTask is null)
{
_executingTask = Task.Run(ExecuteLoop, CancellationToken.None);
}
}
using var ctr = cancellationToken.Register(state => ((TaskCompletionSource<T>)state!).TrySetCanceled(cancellationToken), tcs);
return await tcs.Task.ConfigureAwait(false);
}
private async Task ExecuteLoop()
{
try
{
while (true)
{
TaskCompletionSource<T>? currentTcs;
CancellationTokenSource? currentCts;
int currentCallId;
lock (_lock)
{
currentTcs = _currentTcs;
currentCallId = _callId;
if (currentTcs is null)
{
break; // Nothing pending.
}
_currentCancellationSource?.Dispose();
_currentCancellationSource = new();
currentCts = _currentCancellationSource;
}
try
{
var value = await _valueFactory(currentCts.Token).ConfigureAwait(false);
CompleteSuccessIfCurrent(currentTcs, currentCallId, value);
}
catch (OperationCanceledException)
{
CompleteIfCurrent(currentTcs, currentCallId, t => t.TrySetCanceled(currentCts.Token));
}
catch (Exception ex)
{
CompleteIfCurrent(currentTcs, currentCallId, t => t.TrySetException(ex));
}
}
}
finally
{
lock (_lock)
{
_currentTcs = null;
_currentCancellationSource?.Dispose();
_currentCancellationSource = null;
_executingTask = null;
}
}
}
private void CompleteSuccessIfCurrent(TaskCompletionSource<T> candidate, int id, T value)
{
var shouldApply = false;
lock (_lock)
{
if (_currentTcs == candidate && _callId == id)
{
// Mark as consumed so a new computation can start immediately.
_currentTcs = null;
shouldApply = true;
}
}
if (!shouldApply)
{
return; // Superseded meanwhile.
}
Exception? applyException = null;
try
{
lock (_applyLock)
{
_apply(value);
}
}
catch (Exception ex)
{
applyException = ex;
}
if (applyException is null)
{
candidate.TrySetResult(value);
}
else
{
candidate.TrySetException(applyException);
}
}
private void CompleteIfCurrent(
TaskCompletionSource<T> candidate,
int id,
Action<TaskCompletionSource<T>> complete)
{
lock (_lock)
{
if (_currentTcs == candidate && _callId == id)
{
complete(candidate);
_currentTcs = null;
}
}
}
public void Dispose()
{
lock (_lock)
{
_currentCancellationSource?.Cancel();
_currentCancellationSource?.Dispose();
_currentTcs?.TrySetException(new ObjectDisposedException(nameof(SupersedingAsyncValueGate<T>)));
_currentTcs = null;
}
GC.SuppressFinalize(this);
}
}

View File

@@ -12,8 +12,4 @@ MonitorFromWindow
SHOW_WINDOW_CMD
ShellExecuteEx
SEE_MASK_INVOKEIDLIST
GetFileAttributes
FILE_FLAGS_AND_ATTRIBUTES
INVALID_FILE_ATTRIBUTES
SEE_MASK_INVOKEIDLIST

View File

@@ -2,6 +2,8 @@
// 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;
namespace Microsoft.CmdPal.Core.Common.Services;
public interface IRunHistoryService
@@ -23,12 +25,3 @@ public interface IRunHistoryService
/// <param name="item">The run history item to add.</param>
void AddRunHistoryItem(string item);
}
public interface ITelemetryService
{
void LogRunQuery(string query, int resultCount, ulong durationMs);
void LogRunCommand(string command, bool asAdmin, bool success);
void LogOpenUri(string uri, bool isWeb, bool success);
}

View File

@@ -1,12 +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.Core.ViewModels;
/// <summary>
/// Encapsulates a navigation request within Command Palette view models.
/// </summary>
/// <param name="TargetViewModel">A view model that should be navigated to.</param>
/// <param name="NavigationToken"> A <see cref="CancellationToken"/> that can be used to cancel the pending navigation.</param>
public record AsyncNavigationRequest(object? TargetViewModel, CancellationToken NavigationToken);

View File

@@ -17,7 +17,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public ExtensionObject<ICommandItem> Model => _commandItemModel;
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
private CommandContextItemViewModel? _defaultCommandContextItemViewModel;
private CommandContextItemViewModel? _defaultCommandContextItem;
internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized;
@@ -43,9 +43,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public string Subtitle { get; private set; } = string.Empty;
private IconInfoViewModel _icon = new(null);
private IconInfoViewModel _listItemIcon = new(null);
public IconInfoViewModel Icon => _icon.IsSet ? _icon : Command.Icon;
public IconInfoViewModel Icon => _listItemIcon.IsSet ? _listItemIcon : Command.Icon;
public CommandViewModel Command { get; private set; }
@@ -69,9 +69,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
{
get
{
List<IContextItemViewModel> l = _defaultCommandContextItemViewModel is null ?
List<IContextItemViewModel> l = _defaultCommandContextItem is null ?
new() :
[_defaultCommandContextItemViewModel];
[_defaultCommandContextItem];
l.AddRange(MoreCommands);
return l;
@@ -136,11 +136,11 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
Command.InitializeProperties();
var icon = model.Icon;
if (icon is not null)
var listIcon = model.Icon;
if (listIcon is not null)
{
_icon = new(icon);
_icon.InitializeProperties();
_listItemIcon = new(listIcon);
_listItemIcon.InitializeProperties();
}
// TODO: Do these need to go into FastInit?
@@ -201,19 +201,21 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
if (!string.IsNullOrEmpty(model.Command?.Name))
{
_defaultCommandContextItemViewModel = new CommandContextItemViewModel(new CommandContextItem(model.Command!), PageContext)
_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
// Anything we set manually here must stay in sync with the corresponding properties on CommandItemViewModel.
};
// Only set the icon on the context item for us if our command didn't
// have its own icon
UpdateDefaultContextItemIcon();
if (!Command.HasIcon)
{
_defaultCommandContextItem._listItemIcon = _listItemIcon;
}
}
Initialized |= InitializedState.SelectionInitialized;
@@ -236,7 +238,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
_itemTitle = "Error";
Subtitle = "Item failed to load";
MoreCommands = [];
_icon = _errorIcon;
_listItemIcon = _errorIcon;
Initialized |= InitializedState.Error;
}
@@ -273,7 +275,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
_itemTitle = "Error";
Subtitle = "Item failed to load";
MoreCommands = [];
_icon = _errorIcon;
_listItemIcon = _errorIcon;
Initialized |= InitializedState.Error;
}
@@ -303,18 +305,17 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
switch (propertyName)
{
case nameof(Command):
Command.PropertyChanged -= Command_PropertyChanged;
if (Command is not null)
{
Command.PropertyChanged -= Command_PropertyChanged;
}
Command = new(model.Command, PageContext);
Command.InitializeProperties();
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
_itemTitle = model.Title;
_defaultCommandContextItemViewModel?.Command = Command;
_defaultCommandContextItemViewModel?.UpdateTitle(_itemTitle);
UpdateDefaultContextItemIcon();
UpdateProperty(nameof(Name));
UpdateProperty(nameof(Title));
UpdateProperty(nameof(Icon));
@@ -325,22 +326,12 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
break;
case nameof(Subtitle):
var modelSubtitle = model.Subtitle;
this.Subtitle = modelSubtitle;
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
this.Subtitle = model.Subtitle;
break;
case nameof(Icon):
var oldIcon = _icon;
_icon = new(model.Icon);
_icon.InitializeProperties();
if (oldIcon.IsSet || _icon.IsSet)
{
UpdateProperty(nameof(Icon));
}
UpdateDefaultContextItemIcon();
_listItemIcon = new(model.Icon);
_listItemIcon.InitializeProperties();
break;
case nameof(model.MoreCommands):
@@ -387,49 +378,26 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
private void Command_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var propertyName = e.PropertyName;
var model = _commandItemModel.Unsafe;
if (model is null)
{
return;
}
switch (propertyName)
{
case nameof(Command.Name):
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
_itemTitle = model.Title;
UpdateProperty(nameof(Title), nameof(Name));
var model = _commandItemModel.Unsafe;
if (model is not null)
{
_itemTitle = model.Title;
}
_defaultCommandContextItemViewModel?.UpdateTitle(model.Command.Name);
UpdateProperty(nameof(Title));
UpdateProperty(nameof(Name));
break;
case nameof(Command.Icon):
UpdateDefaultContextItemIcon();
UpdateProperty(nameof(Icon));
break;
}
}
private void UpdateDefaultContextItemIcon()
{
// Command icon takes precedence over our icon on the primary command
_defaultCommandContextItemViewModel?.UpdateIcon(Command.Icon.IsSet ? Command.Icon : _icon);
}
private void UpdateTitle(string? title)
{
_itemTitle = title ?? string.Empty;
UpdateProperty(nameof(Title));
}
private void UpdateIcon(IIconInfo? iconInfo)
{
_icon = new(iconInfo);
_icon.InitializeProperties();
UpdateProperty(nameof(Icon));
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();
@@ -443,10 +411,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
}
// _listItemIcon.SafeCleanup();
_icon = new(null); // necessary?
_listItemIcon = new(null); // necessary?
_defaultCommandContextItemViewModel?.SafeCleanup();
_defaultCommandContextItemViewModel = null;
_defaultCommandContextItem?.SafeCleanup();
_defaultCommandContextItem = null;
Command.PropertyChanged -= Command_PropertyChanged;
Command.SafeCleanup();

View File

@@ -2,10 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Input;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
@@ -13,13 +11,6 @@ public partial class DetailsLinkViewModel(
IDetailsElement _detailsElement,
WeakReference<IPageContext> context) : DetailsElementViewModel(_detailsElement, context)
{
private static readonly string[] _initProperties = [
nameof(Text),
nameof(Link),
nameof(IsLink),
nameof(IsText),
nameof(NavigateCommand)];
private readonly ExtensionObject<IDetailsLink> _dataModel =
new(_detailsElement.Data as IDetailsLink);
@@ -31,8 +22,6 @@ public partial class DetailsLinkViewModel(
public bool IsText => !IsLink;
public RelayCommand? NavigateCommand { get; private set; }
public override void InitializeProperties()
{
base.InitializeProperties();
@@ -49,18 +38,9 @@ public partial class DetailsLinkViewModel(
Text = Link.ToString();
}
if (Link is not null)
{
// Custom command to open a link in the default browser or app,
// depending on the link type.
// Binding Link to a Hyperlink(Button).NavigateUri works only for
// certain URI schemes (e.g., http, https) and cannot open file:
// scheme URIs or local files.
NavigateCommand = new RelayCommand(
() => ShellHelpers.OpenInShell(Link.ToString()),
() => Link is not null);
}
UpdateProperty(_initProperties);
UpdateProperty(nameof(Text));
UpdateProperty(nameof(Link));
UpdateProperty(nameof(IsLink));
UpdateProperty(nameof(IsText));
}
}

View File

@@ -4,4 +4,6 @@
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken);
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation)
{
}

View File

@@ -1,8 +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.Core.ViewModels;
internal sealed partial class NullPageViewModel(TaskScheduler scheduler, AppExtensionHost extensionHost)
: PageViewModel(null, scheduler, extensionHost);

View File

@@ -23,9 +23,6 @@ public partial class ShellViewModel : ObservableObject,
private readonly Lock _invokeLock = new();
private Task? _handleInvokeTask;
// Cancellation token source for page loading/navigation operations
private CancellationTokenSource? _navigationCts;
[ObservableProperty]
public partial bool IsLoaded { get; set; } = false;
@@ -69,8 +66,6 @@ public partial class ShellViewModel : ObservableObject,
public bool IsNested => _isNested;
public PageViewModel NullPage { get; private set; }
public ShellViewModel(
TaskScheduler scheduler,
IRootPageService rootPageService,
@@ -82,7 +77,6 @@ public partial class ShellViewModel : ObservableObject,
_rootPageService = rootPageService;
_appHostService = appHostService;
NullPage = new NullPageViewModel(_scheduler, appHostService.GetDefaultHost());
_currentPage = new LoadingPageViewModel(null, _scheduler, appHostService.GetDefaultHost());
// Register to receive messages
@@ -119,7 +113,7 @@ public partial class ShellViewModel : ObservableObject,
return true;
}
private async Task LoadPageViewModelAsync(PageViewModel viewModel, CancellationToken cancellationToken = default)
public async Task LoadPageViewModelAsync(PageViewModel viewModel)
{
// Note: We removed the general loading state, extensions sometimes use their `IsLoading`, but it's inconsistently implemented it seems.
// IsInitialized is our main indicator of the general overall state of loading props/items from a page we use for the progress bar
@@ -131,80 +125,44 @@ public partial class ShellViewModel : ObservableObject,
if (!viewModel.IsInitialized
&& viewModel.InitializeCommand is not null)
{
var outer = Task.Run(
async () =>
{
// You know, this creates the situation where we wait for
// both loading page properties, AND the items, before we
// display anything.
//
// We almost need to do an async await on initialize, then
// just a fire-and-forget on FetchItems.
// RE: We do set the CurrentPage in ShellPage.xaml.cs as well, so, we kind of are doing two different things here.
// Definitely some more clean-up to do, but at least its centralized to one spot now.
viewModel.InitializeCommand.Execute(null);
await viewModel.InitializeCommand.ExecutionTask!;
if (viewModel.InitializeCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
{
if (viewModel.InitializeCommand.ExecutionTask.Exception is AggregateException ex)
{
CoreLogger.LogError(ex.ToString());
}
}
else
{
var t = Task.Factory.StartNew(
() =>
{
if (cancellationToken.IsCancellationRequested)
{
if (viewModel is IDisposable disposable)
{
try
{
disposable.Dispose();
}
catch (Exception ex)
{
CoreLogger.LogError(ex.ToString());
}
}
return;
}
CurrentPage = viewModel;
},
cancellationToken,
TaskCreationOptions.None,
_scheduler);
await t;
}
},
cancellationToken);
await outer;
}
else
{
if (cancellationToken.IsCancellationRequested)
var outer = Task.Run(async () =>
{
if (viewModel is IDisposable disposable)
// You know, this creates the situation where we wait for
// both loading page properties, AND the items, before we
// display anything.
//
// We almost need to do an async await on initialize, then
// just a fire-and-forget on FetchItems.
// RE: We do set the CurrentPage in ShellPage.xaml.cs as well, so, we kind of are doing two different things here.
// Definitely some more clean-up to do, but at least its centralized to one spot now.
viewModel.InitializeCommand.Execute(null);
await viewModel.InitializeCommand.ExecutionTask!;
if (viewModel.InitializeCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
{
try
{
disposable.Dispose();
}
catch (Exception ex)
if (viewModel.InitializeCommand.ExecutionTask.Exception is AggregateException ex)
{
CoreLogger.LogError(ex.ToString());
}
}
return;
}
else
{
var t = Task.Factory.StartNew(
() =>
{
CurrentPage = viewModel;
},
CancellationToken.None,
TaskCreationOptions.None,
_scheduler);
await t;
}
});
await outer;
}
else
{
CurrentPage = viewModel;
}
}
@@ -216,28 +174,6 @@ public partial class ShellViewModel : ObservableObject,
private void PerformCommand(PerformCommandMessage message)
{
// Create/replace the navigation cancellation token.
// If one already exists, cancel and dispose it first.
var newCts = new CancellationTokenSource();
var oldCts = Interlocked.Exchange(ref _navigationCts, newCts);
if (oldCts is not null)
{
try
{
oldCts.Cancel();
}
catch (Exception ex)
{
CoreLogger.LogError(ex.ToString());
}
finally
{
oldCts.Dispose();
}
}
var navigationToken = newCts.Token;
var command = message.Command.Unsafe;
if (command is null)
{
@@ -265,27 +201,16 @@ public partial class ShellViewModel : ObservableObject,
throw new NotSupportedException();
}
// Clear command bar, ViewModel initialization can already set new commands if it wants to
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
// Kick off async loading of our ViewModel
LoadPageViewModelAsync(pageViewModel, navigationToken)
LoadPageViewModelAsync(pageViewModel)
.ContinueWith(
(Task t) =>
{
// clean up the navigation token if it's still ours
if (Interlocked.CompareExchange(ref _navigationCts, null, newCts) == newCts)
{
newCts.Dispose();
}
OnUIThread(() => { WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)); });
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation));
},
navigationToken,
TaskContinuationOptions.None,
_scheduler);
// While we're loading in the background, immediately move to the next page.
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation, navigationToken));
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
// See RootFrame_Navigated event handler.
}
@@ -443,9 +368,4 @@ public partial class ShellViewModel : ObservableObject,
TaskCreationOptions.None,
_scheduler);
}
public void CancelNavigation()
{
_navigationCts?.Cancel();
}
}

View File

@@ -10,8 +10,6 @@ using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Ext.Apps;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Properties;
using Microsoft.CommandPalette.Extensions;
@@ -38,7 +36,6 @@ public partial class MainListPage : DynamicListPage,
private List<Scored<IListItem>>? _filteredItems;
private List<Scored<IListItem>>? _filteredApps;
private List<Scored<IListItem>>? _fallbackItems;
private IEnumerable<Scored<IListItem>>? _scoredFallbackItems;
private bool _includeApps;
private bool _filteredItemsIncludesApps;
private int _appResultLimit = 10;
@@ -163,7 +160,7 @@ public partial class MainListPage : DynamicListPage,
{
lock (_tlcManager.TopLevelCommands)
{
var limitedApps = new List<Scored<IListItem>>();
List<Scored<IListItem>> limitedApps = new List<Scored<IListItem>>();
// Fuzzy matching can produce a lot of results, so we want to limit the
// number of apps we show at once if it's a large set.
@@ -174,7 +171,6 @@ public partial class MainListPage : DynamicListPage,
var items = Enumerable.Empty<Scored<IListItem>>()
.Concat(_filteredItems is not null ? _filteredItems : [])
.Concat(_scoredFallbackItems is not null ? _scoredFallbackItems : [])
.Concat(limitedApps)
.OrderByDescending(o => o.Score)
@@ -188,14 +184,6 @@ public partial class MainListPage : DynamicListPage,
}
}
private void ClearResults()
{
_filteredItems = null;
_filteredApps = null;
_fallbackItems = null;
_scoredFallbackItems = null;
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
var timer = new Stopwatch();
@@ -228,7 +216,8 @@ public partial class MainListPage : DynamicListPage,
lock (_tlcManager.TopLevelCommands)
{
_filteredItemsIncludesApps = _includeApps;
ClearResults();
_filteredItems = null;
_filteredApps = null;
}
}
@@ -244,36 +233,7 @@ public partial class MainListPage : DynamicListPage,
var commands = _tlcManager.TopLevelCommands;
lock (commands)
{
if (token.IsCancellationRequested)
{
return;
}
// prefilter fallbacks
var specialFallbacks = new List<TopLevelViewModel>(_specialFallbacks.Length);
var commonFallbacks = new List<TopLevelViewModel>();
foreach (var s in commands)
{
if (!s.IsFallback)
{
continue;
}
if (_specialFallbacks.Contains(s.CommandProviderId))
{
specialFallbacks.Add(s);
}
else
{
commonFallbacks.Add(s);
}
}
// start update of fallbacks; update special fallbacks separately,
// so they can finish faster
UpdateFallbacks(SearchText, specialFallbacks, token);
UpdateFallbacks(SearchText, commonFallbacks, token);
UpdateFallbacks(SearchText, commands.ToImmutableArray(), token);
if (token.IsCancellationRequested)
{
@@ -284,7 +244,9 @@ public partial class MainListPage : DynamicListPage,
if (string.IsNullOrEmpty(newSearch))
{
_filteredItemsIncludesApps = _includeApps;
ClearResults();
_filteredItems = null;
_filteredApps = null;
_fallbackItems = null;
RaiseItemsChanged(commands.Count);
return;
}
@@ -293,13 +255,17 @@ public partial class MainListPage : DynamicListPage,
// re-use previous results. Reset _filteredItems, and keep er moving.
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
{
ClearResults();
_filteredItems = null;
_filteredApps = null;
_fallbackItems = null;
}
// If the internal state has changed, reset _filteredItems to reset the list.
if (_filteredItemsIncludesApps != _includeApps)
{
ClearResults();
_filteredItems = null;
_filteredApps = null;
_fallbackItems = null;
}
if (token.IsCancellationRequested)
@@ -307,9 +273,9 @@ public partial class MainListPage : DynamicListPage,
return;
}
var newFilteredItems = Enumerable.Empty<IListItem>();
var newFallbacks = Enumerable.Empty<IListItem>();
var newApps = Enumerable.Empty<IListItem>();
IEnumerable<IListItem> newFilteredItems = Enumerable.Empty<IListItem>();
IEnumerable<IListItem> newFallbacks = Enumerable.Empty<IListItem>();
IEnumerable<IListItem> newApps = Enumerable.Empty<IListItem>();
if (_filteredItems is not null)
{
@@ -345,12 +311,15 @@ public partial class MainListPage : DynamicListPage,
// with a list of all our commands & apps.
if (!newFilteredItems.Any() && !newApps.Any())
{
newFilteredItems = commands.Where(s => !s.IsFallback);
// We're going to start over with our fallbacks
newFallbacks = Enumerable.Empty<IListItem>();
newFilteredItems = commands.Where(s => !s.IsFallback || _specialFallbacks.Contains(s.CommandProviderId));
// Fallbacks are always included in the list, even if they
// don't match the search text. But we don't want to
// consider them when filtering the list.
newFallbacks = commonFallbacks;
newFallbacks = commands.Where(s => s.IsFallback && !_specialFallbacks.Contains(s.CommandProviderId));
if (token.IsCancellationRequested)
{
@@ -361,20 +330,7 @@ public partial class MainListPage : DynamicListPage,
if (_includeApps)
{
var allNewApps = AllAppsCommandProvider.Page.GetItems().ToList();
// We need to remove pinned apps from allNewApps so they don't show twice.
var pinnedApps = PinnedAppsManager.Instance.GetPinnedAppIdentifiers();
if (pinnedApps.Length > 0)
{
newApps = allNewApps.Where(w =>
pinnedApps.IndexOf(((AppListItem)w).AppIdentifier) < 0);
}
else
{
newApps = allNewApps;
}
newApps = AllAppsCommandProvider.Page.GetItems().ToList();
}
if (token.IsCancellationRequested)
@@ -383,25 +339,8 @@ public partial class MainListPage : DynamicListPage,
}
}
var history = _serviceProvider.GetService<AppStateModel>()!.RecentCommands!;
Func<string, IListItem, int> scoreItem = (a, b) => { return ScoreTopLevelItem(a, b, history); };
// Produce a list of everything that matches the current filter.
_filteredItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, scoreItem)];
if (token.IsCancellationRequested)
{
return;
}
IEnumerable<IListItem> newFallbacksForScoring = commands.Where(s => s.IsFallback && _specialFallbacks.Contains(s.CommandProviderId));
if (token.IsCancellationRequested)
{
return;
}
_scoredFallbackItems = ListHelpers.FilterListWithScores<IListItem>(newFallbacksForScoring ?? [], SearchText, scoreItem);
_filteredItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, ScoreTopLevelItem)];
if (token.IsCancellationRequested)
{
@@ -419,7 +358,7 @@ public partial class MainListPage : DynamicListPage,
// Produce a list of filtered apps with the appropriate limit
if (newApps.Any())
{
var scoredApps = ListHelpers.FilterListWithScores<IListItem>(newApps, SearchText, scoreItem);
var scoredApps = ListHelpers.FilterListWithScores<IListItem>(newApps, SearchText, ScoreTopLevelItem);
if (token.IsCancellationRequested)
{
@@ -486,7 +425,7 @@ public partial class MainListPage : DynamicListPage,
// Almost verbatim ListHelpers.ScoreListItem, but also accounting for the
// fact that we want fallback handlers down-weighted, so that they don't
// _always_ show up first.
internal static int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem, IRecentCommandsManager history)
private int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem)
{
var title = topLevelOrAppItem.Title;
if (string.IsNullOrWhiteSpace(title))
@@ -562,9 +501,10 @@ public partial class MainListPage : DynamicListPage,
// here we add the recent command weight boost
//
// Otherwise something like `x` will still match everything you've run before
var finalScore = matchSomething * 10;
var finalScore = matchSomething;
if (matchSomething > 0)
{
var history = _serviceProvider.GetService<AppStateModel>()!.RecentCommands;
var recentWeightBoost = history.GetCommandHistoryWeight(id);
finalScore += recentWeightBoost;
}
@@ -581,7 +521,7 @@ public partial class MainListPage : DynamicListPage,
AppStateModel.SaveState(state);
}
private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
private string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
{
if (topLevelOrAppItem is TopLevelViewModel topLevel)
{

View File

@@ -1,7 +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.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CmdPal.UI.ViewModels.UnitTests")]

View File

@@ -7,7 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class RecentCommandsManager : ObservableObject, IRecentCommandsManager
public partial class RecentCommandsManager : ObservableObject
{
[JsonInclude]
internal List<HistoryItem> History { get; set; } = [];
@@ -80,10 +80,3 @@ public partial class RecentCommandsManager : ObservableObject, IRecentCommandsMa
}
}
}
public interface IRecentCommandsManager
{
int GetCommandHistoryWeight(string commandId);
void AddHistoryItem(string commandId);
}

View File

@@ -52,8 +52,6 @@ public partial class SettingsModel : ObservableObject
public MonitorBehavior SummonOn { get; set; } = MonitorBehavior.ToMouse;
public bool DisableAnimations { get; set; } = true;
// END SETTINGS
///////////////////////////////////////////////////////////////////////////

View File

@@ -128,16 +128,6 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
public bool DisableAnimations
{
get => _settings.DisableAnimations;
set
{
_settings.DisableAnimations = value;
Save();
}
}
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)

View File

@@ -114,7 +114,7 @@ public partial class App : Application
services.AddSingleton<ICommandProvider, ShellCommandsProvider>();
services.AddSingleton<ICommandProvider, CalculatorCommandProvider>();
services.AddSingleton<ICommandProvider>(files);
services.AddSingleton<ICommandProvider, BookmarksCommandProvider>(_ => BookmarksCommandProvider.CreateWithDefaultStore());
services.AddSingleton<ICommandProvider, BookmarksCommandProvider>();
services.AddSingleton<ICommandProvider, WindowWalkerCommandsProvider>();
services.AddSingleton<ICommandProvider, WebSearchCommandsProvider>();
@@ -160,7 +160,7 @@ public partial class App : Application
services.AddSingleton<IRootPageService, PowerToysRootPageService>();
services.AddSingleton<IAppHostService, PowerToysAppHostService>();
services.AddSingleton<ITelemetryService, TelemetryForwarder>();
services.AddSingleton(new TelemetryForwarder());
// ViewModels
services.AddSingleton<ShellViewModel>();

View File

@@ -14,6 +14,7 @@
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
Background="Transparent"
PreviewKeyDown="UserControl_PreviewKeyDown"
mc:Ignorable="d">
@@ -21,7 +22,7 @@
<ResourceDictionary>
<cmdpalUI:KeyChordToStringConverter x:Key="KeyChordToStringConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<Thickness x:Key="DefaultContextMenuItemPadding">12,8,12,8</Thickness>
<cmdpalUI:ContextItemTemplateSelector
x:Key="ContextItemTemplateSelector"
Critical="{StaticResource CriticalContextMenuViewModelTemplate}"
@@ -30,7 +31,7 @@
<!-- Template for context items in the context item menu -->
<DataTemplate x:Key="DefaultContextMenuViewModelTemplate" x:DataType="coreViewModels:CommandContextItemViewModel">
<Grid Padding="{StaticResource DefaultContextMenuItemPadding}" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
<Grid AutomationProperties.Name="{x:Bind Title}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32" />
<ColumnDefinition Width="*" />
@@ -41,7 +42,7 @@
Height="16"
Margin="4,0,0,0"
HorizontalAlignment="Left"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceKey="{x:Bind Icon}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock
x:Name="TitleTextBlock"
@@ -50,11 +51,11 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
MaxLines="1"
Text="{x:Bind Title, Mode=OneWay}"
Text="{x:Bind Title}"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap">
<ToolTipService.ToolTip>
<ToolTip Content="{x:Bind Title, Mode=OneWay}" Visibility="{Binding IsTextTrimmed, ElementName=TitleTextBlock, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<ToolTip Content="{x:Bind Title}" Visibility="{Binding IsTextTrimmed, ElementName=TitleTextBlock, Converter={StaticResource BoolToVisibilityConverter}}" />
</ToolTipService.ToolTip>
</TextBlock>
<TextBlock
@@ -64,13 +65,13 @@
VerticalAlignment="Center"
Foreground="{ThemeResource MenuFlyoutItemKeyboardAcceleratorTextForeground}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
Text="{x:Bind RequestedShortcut, Converter={StaticResource KeyChordToStringConverter}}" />
</Grid>
</DataTemplate>
<!-- Template for context items flagged as critical -->
<DataTemplate x:Key="CriticalContextMenuViewModelTemplate" x:DataType="coreViewModels:CommandContextItemViewModel">
<Grid Padding="{StaticResource DefaultContextMenuItemPadding}" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
<Grid AutomationProperties.Name="{x:Bind Title}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32" />
<ColumnDefinition Width="*" />
@@ -82,7 +83,7 @@
Margin="4,0,0,0"
HorizontalAlignment="Left"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceKey="{x:Bind Icon}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock
x:Name="TitleTextBlock"
@@ -92,11 +93,11 @@
VerticalAlignment="Center"
MaxLines="1"
Style="{StaticResource ContextItemTitleTextBlockCriticalStyle}"
Text="{x:Bind Title, Mode=OneWay}"
Text="{x:Bind Title}"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap">
<ToolTipService.ToolTip>
<ToolTip Content="{x:Bind Title, Mode=OneWay}" Visibility="{Binding IsTextTrimmed, ElementName=TitleTextBlock, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<ToolTip Content="{x:Bind Title}" Visibility="{Binding IsTextTrimmed, ElementName=TitleTextBlock, Converter={StaticResource BoolToVisibilityConverter}}" />
</ToolTipService.ToolTip>
</TextBlock>
<TextBlock
@@ -105,7 +106,7 @@
HorizontalAlignment="Right"
VerticalAlignment="Center"
Style="{StaticResource ContextItemCaptionTextBlockCriticalStyle}"
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
Text="{x:Bind RequestedShortcut, Converter={StaticResource KeyChordToStringConverter}}" />
</Grid>
</DataTemplate>
@@ -113,7 +114,7 @@
<DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="coreViewModels:SeparatorViewModel">
<Rectangle
Height="1"
Margin="0,2,0,2"
Margin="-16,-12,-12,-12"
Fill="{ThemeResource MenuFlyoutSeparatorBackground}" />
</DataTemplate>
</ResourceDictionary>
@@ -124,39 +125,35 @@
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<ListView
x:Name="CommandsDropdown"
MinWidth="248"
Margin="0,4,0,2"
IsItemClickEnabled="True"
ItemClick="CommandsDropdown_ItemClick"
ItemTemplateSelector="{StaticResource ContextItemTemplateSelector}"
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
PreviewKeyDown="CommandsDropdown_PreviewKeyDown"
SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="0" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
</ListView>
<Border BorderBrush="{ThemeResource MenuFlyoutSeparatorBackground}" BorderThickness="0,0,0,1" />
<StackPanel x:Name="CommandsPanel">
<ListView
x:Name="CommandsDropdown"
MinWidth="248"
IsItemClickEnabled="True"
ItemClick="CommandsDropdown_ItemClick"
ItemTemplateSelector="{StaticResource ContextItemTemplateSelector}"
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
PreviewKeyDown="CommandsDropdown_PreviewKeyDown"
SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="12,8" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
</ListView>
</StackPanel>
<TextBox
x:Name="ContextFilterBox"
x:Uid="ContextFilterBox"
Margin="0"
Padding="10,7,6,8"
Background="{ThemeResource AcrylicBackgroundFillColorBaseBrush}"
BorderThickness="0,0,0,2"
CornerRadius="8, 8, 0, 0"
Margin="4"
IsTextScaleFactorEnabled="True"
KeyDown="ContextFilterBox_KeyDown"
PreviewKeyDown="ContextFilterBox_PreviewKeyDown"
Style="{StaticResource SearchTextBoxStyle}"
TextChanged="ContextFilterBox_TextChanged" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ContextMenuOrder">
@@ -165,11 +162,9 @@
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.FilterOnTop, Mode=OneWay}" To="True" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="CommandsDropdown.(Grid.Row)" Value="1" />
<Setter Target="CommandsPanel.(Grid.Row)" Value="1" />
<Setter Target="ContextFilterBox.(Grid.Row)" Value="0" />
<Setter Target="CommandsDropdown.Margin" Value="0, 3, 0, 4" />
<Setter Target="ContextFilterBox.CornerRadius" Value="8, 8, 0, 0" />
<Setter Target="ContextFilterBox.Margin" Value="0,0,0,-1" />
<Setter Target="CommandsDropdown.Margin" Value="0, 0, 0, 4" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="FilterOnBottom">
@@ -177,11 +172,9 @@
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.FilterOnTop, Mode=OneWay}" To="False" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="CommandsDropdown.(Grid.Row)" Value="0" />
<Setter Target="CommandsPanel.(Grid.Row)" Value="0" />
<Setter Target="ContextFilterBox.(Grid.Row)" Value="1" />
<Setter Target="CommandsDropdown.Margin" Value="0, 4, 0, 4" />
<Setter Target="ContextFilterBox.CornerRadius" Value="0, 0, 8, 8" />
<Setter Target="ContextFilterBox.Margin" Value="0,0,0,-2" />
<Setter Target="CommandsDropdown.Margin" Value="0, 4, 0, 0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>

View File

@@ -1,80 +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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
// Just put all the run events in one file for simplicity.
#pragma warning disable SA1402 // File may only contain a single type
#pragma warning disable SA1649 // File name should match first type name
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class CmdPalRunQuery : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public string Query { get; set; }
public int ResultCount { get; set; }
public ulong DurationMs { get; set; }
public CmdPalRunQuery(string query, int resultCount, ulong durationMs)
{
EventName = "CmdPal_RunQuery";
Query = query;
ResultCount = resultCount;
DurationMs = durationMs;
}
}
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class CmdPalRunCommand : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public string Command { get; set; }
public bool AsAdmin { get; set; }
public bool Success { get; set; }
public CmdPalRunCommand(string command, bool asAdmin, bool success)
{
EventName = "CmdPal_RunCommand";
Command = command;
AsAdmin = asAdmin;
Success = success;
}
}
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class CmdPalOpenUri : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public string Uri { get; set; }
public bool IsWeb { get; set; }
public bool Success { get; set; }
public CmdPalOpenUri(string uri, bool isWeb, bool success)
{
EventName = "CmdPal_OpenUri";
Uri = uri;
IsWeb = isWeb;
Success = success;
}
}
#pragma warning restore SA1649 // File name should match first type name
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -46,18 +46,11 @@ public sealed partial class ContentPage : Page,
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.Parameter is not AsyncNavigationRequest navigationRequest)
if (e.Parameter is ContentPageViewModel vm)
{
throw new InvalidOperationException($"Invalid navigation parameter: {nameof(e.Parameter)} must be {nameof(AsyncNavigationRequest)}");
ViewModel = vm;
}
if (navigationRequest.TargetViewModel is not ContentPageViewModel contentPageViewModel)
{
throw new InvalidOperationException($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(ContentPageViewModel)}");
}
ViewModel = contentPageViewModel;
if (!WeakReferenceMessenger.Default.IsRegistered<ActivateSelectedListItemMessage>(this))
{
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
@@ -78,12 +71,6 @@ public sealed partial class ContentPage : Page,
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
// Clean-up event listeners
if (e.NavigationMode != NavigationMode.New)
{
ViewModel?.SafeCleanup();
CleanupHelper.Cleanup(this);
}
ViewModel = null;
}

View File

@@ -59,18 +59,11 @@ public sealed partial class ListPage : Page,
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.Parameter is not AsyncNavigationRequest navigationRequest)
if (e.Parameter is ListViewModel lvm)
{
throw new InvalidOperationException($"Invalid navigation parameter: {nameof(e.Parameter)} must be {nameof(AsyncNavigationRequest)}");
ViewModel = lvm;
}
if (navigationRequest.TargetViewModel is not ListViewModel listViewModel)
{
throw new InvalidOperationException($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(ListViewModel)}");
}
ViewModel = listViewModel;
if (e.NavigationMode == NavigationMode.Back
|| (e.NavigationMode == NavigationMode.New && ItemView.Items.Count > 0))
{

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Events;
using Microsoft.PowerToys.Telemetry;
@@ -20,7 +19,6 @@ namespace Microsoft.CmdPal.UI;
/// or something similar, but this works for now.
/// </summary>
internal sealed class TelemetryForwarder :
ITelemetryService,
IRecipient<BeginInvokeMessage>,
IRecipient<CmdPalInvokeResultMessage>
{
@@ -39,19 +37,4 @@ internal sealed class TelemetryForwarder :
{
PowerToysTelemetry.Log.WriteEvent(new BeginInvoke());
}
public void LogRunQuery(string query, int resultCount, ulong durationMs)
{
PowerToysTelemetry.Log.WriteEvent(new CmdPalRunQuery(query, resultCount, durationMs));
}
public void LogRunCommand(string command, bool asAdmin, bool success)
{
PowerToysTelemetry.Log.WriteEvent(new CmdPalRunCommand(command, asAdmin, success));
}
public void LogOpenUri(string uri, bool isWeb, bool success)
{
PowerToysTelemetry.Log.WriteEvent(new CmdPalOpenUri(uri, isWeb, success));
}
}

View File

@@ -360,51 +360,33 @@ public sealed partial class MainWindow : WindowEx,
private void HideWindow()
{
// Cloak our HWND to avoid all animations.
var cloaked = Cloak();
Cloak();
// Then hide our HWND, to make sure that the OS gives the FG / focus back to another app
// (there's no way for us to guess what the right hwnd might be, only the OS can do it right)
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
if (cloaked)
{
// TRICKY: show our HWND again. This will trick XAML into painting our
// HWND again, so that we avoid the "flicker" caused by a WinUI3 app
// window being first shown
// SW_SHOWNA will prevent us for trying to fight the focus back
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_SHOWNA);
// TRICKY: show our HWND again. This will trick XAML into painting our
// HWND again, so that we avoid the "flicker" caused by a WinUI3 app
// window being first shown
// SW_SHOWNA will prevent us for trying to fight the focus back
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_SHOWNA);
// Intentionally leave the window cloaked. So our window is "visible",
// but also cloaked, so you can't see it.
// If the window was not cloaked, then leave it hidden.
// Sure, it's not ideal, but at least it's not visible.
}
// Intentionally leave the window cloaked. So our window is "visible",
// but also cloaked, so you can't see it.
}
private bool Cloak()
private void Cloak()
{
bool wasCloaked;
unsafe
{
BOOL value = true;
var hr = PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
if (hr.Failed)
{
Logger.LogWarning($"DWM cloaking of the main window failed. HRESULT: {hr.Value}.");
}
wasCloaked = hr.Succeeded;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
}
if (wasCloaked)
{
// Because we're only cloaking the window, bury it at the bottom in case something can
// see it - e.g. some accessibility helper (note: this also removes the top-most status).
PInvoke.SetWindowPos(_hwnd, HWND.HWND_BOTTOM, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
}
return wasCloaked;
// Because we're only cloaking the window, bury it at the bottom in case something can
// see it - e.g. some accessibility helper (note: this also removes the top-most status).
PInvoke.SetWindowPos(_hwnd, HWND.HWND_BOTTOM, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
}
private void Uncloak()

View File

@@ -23,29 +23,23 @@ public sealed partial class LoadingPage : Page
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.Parameter is not AsyncNavigationRequest request)
if (e.Parameter is ShellViewModel shellVM
&& shellVM.LoadCommand is not null)
{
throw new InvalidOperationException($"Invalid navigation parameter: {nameof(e.Parameter)} must be {nameof(AsyncNavigationRequest)}");
}
// This will load the built-in commands, then navigate to the main page.
// Once the mainpage loads, we'll start loading extensions.
shellVM.LoadCommand.Execute(null);
if (request.TargetViewModel is not ShellViewModel shellVM)
{
throw new InvalidOperationException($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(ShellViewModel)}");
}
// This will load the built-in commands, then navigate to the main page.
// Once the mainpage loads, we'll start loading extensions.
shellVM.LoadCommand.Execute(null);
_ = Task.Run(async () =>
{
await shellVM.LoadCommand.ExecutionTask!;
if (shellVM.LoadCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
_ = Task.Run(async () =>
{
// TODO: Handle failure case
}
});
await shellVM.LoadCommand.ExecutionTask!;
if (shellVM.LoadCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
{
// TODO: Handle failure case
}
});
}
base.OnNavigatedTo(e);
}

View File

@@ -108,7 +108,6 @@
Visibility="{x:Bind IsText, Mode=OneWay}" />
<HyperlinkButton
Padding="0"
Command="{x:Bind NavigateCommand, Mode=OneWay}"
NavigateUri="{x:Bind Link, Mode=OneWay}"
Visibility="{x:Bind IsLink, Mode=OneWay}">
<TextBlock Text="{x:Bind Text, Mode=OneWay}" TextWrapping="Wrap" />

View File

@@ -95,24 +95,12 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true);
RootFrame.Navigate(typeof(LoadingPage), new AsyncNavigationRequest(ViewModel, CancellationToken.None));
RootFrame.Navigate(typeof(LoadingPage), ViewModel);
var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
_pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
}
/// <summary>
/// Gets the default page animation, depending on the settings
/// </summary>
private NavigationTransitionInfo DefaultPageAnimation
{
get
{
var settings = App.Current.Services.GetService<SettingsModel>()!;
return settings.DisableAnimations ? _noAnimation : _slideRightTransition;
}
}
public void Receive(NavigateBackMessage message)
{
var settings = App.Current.Services.GetService<SettingsModel>()!;
@@ -153,8 +141,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
ContentPageViewModel => typeof(ContentPage),
_ => throw new NotSupportedException(),
},
new AsyncNavigationRequest(message.Page, message.CancellationToken),
message.WithAnimation ? DefaultPageAnimation : _noAnimation);
message.Page,
message.WithAnimation ? _slideRightTransition : _noAnimation);
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth));
@@ -403,8 +391,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
{
HideDetails();
ViewModel.CancelNavigation();
// Note: That we restore the VM state below in RootFrame_Navigated call back after this occurs.
// In the future, we may want to manage the back stack ourselves vs. relying on Frame
// We could replace Frame with a ContentPresenter, but then have to manage transition animations ourselves.
@@ -458,32 +444,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
// This listens to the root frame to ensure that we also track the content's page VM as well that we passed as a parameter.
// This is currently used for both forward and backward navigation.
// As when we go back that we restore ourselves to the proper state within our VM
if (e.Parameter is AsyncNavigationRequest request)
if (e.Parameter is PageViewModel page)
{
if (request.NavigationToken.IsCancellationRequested && e.NavigationMode is not (Microsoft.UI.Xaml.Navigation.NavigationMode.Back or Microsoft.UI.Xaml.Navigation.NavigationMode.Forward))
{
return;
}
switch (request.TargetViewModel)
{
case PageViewModel pageViewModel:
ViewModel.CurrentPage = pageViewModel;
break;
case ShellViewModel:
// This one is an exception, for now (LoadingPage is tied to ShellViewModel,
// but ShellViewModel is not PageViewModel.
ViewModel.CurrentPage = ViewModel.NullPage;
break;
default:
ViewModel.CurrentPage = ViewModel.NullPage;
Logger.LogWarning($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(PageViewModel)}");
break;
}
}
else
{
Logger.LogWarning("Unrecognized target for shell navigation: " + e.Parameter);
// Note, this shortcuts and fights a bit with our LoadPageViewModel above, but we want to better fast display and incrementally load anyway
// We just need to reconcile our loading systems a bit more in the future.
ViewModel.CurrentPage = page;
}
if (e.Content is Page element)
@@ -584,25 +549,19 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
private static void ShellPage_OnPreviewKeyDown(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);
var onlyAlt = altPressed && !ctrlPressed && !shiftPressed && !winPressed;
if (e.Key == VirtualKey.Left && onlyAlt)
if (e.Key == VirtualKey.Left && e.KeyStatus.IsMenuKeyDown)
{
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
e.Handled = true;
}
else if (e.Key == VirtualKey.Home && onlyAlt)
{
WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(WithAnimation: false));
e.Handled = true;
}
else
{
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);
// The CommandBar is responsible for handling all the item keybindings,
// since the bound context item may need to then show another
// context menu

View File

@@ -88,10 +88,6 @@
<ToggleSwitch IsOn="{x:Bind viewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_DisableAnimations_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE945;}">
<ToggleSwitch IsOn="{x:Bind viewModel.DisableAnimations, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- 'For Developers' section -->
<TextBlock x:Uid="ForDevelopersSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />

View File

@@ -407,12 +407,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard.Description" xml:space="preserve">
<value>Choose if Command Palette is visible in the system tray</value>
</data>
<data name="Settings_GeneralPage_DisableAnimations_SettingsCard.Header" xml:space="preserve">
<value>Disable animations</value>
</data>
<data name="Settings_GeneralPage_DisableAnimations_SettingsCard.Description" xml:space="preserve">
<value>Disable animations when switching between pages</value>
</data>
<data name="BackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Back</value>
</data>

View File

@@ -0,0 +1,42 @@
// 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.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class BookmarkDataTests
{
[TestMethod]
public void BookmarkDataWebUrlDetection()
{
// Act
var webBookmark = new BookmarkData
{
Name = "Test Site",
Bookmark = "https://test.com",
};
var nonWebBookmark = new BookmarkData
{
Name = "Local File",
Bookmark = "C:\\temp\\file.txt",
};
var placeholderBookmark = new BookmarkData
{
Name = "Placeholder",
Bookmark = "{Placeholder}",
};
// Assert
Assert.IsTrue(webBookmark.IsWebUrl());
Assert.IsFalse(webBookmark.IsPlaceholder);
Assert.IsFalse(nonWebBookmark.IsWebUrl());
Assert.IsFalse(nonWebBookmark.IsPlaceholder);
Assert.IsTrue(placeholderBookmark.IsPlaceholder);
}
}

View File

@@ -3,8 +3,6 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
@@ -193,7 +191,7 @@ public class BookmarkJsonParserTests
public void SerializeBookmarks_ValidBookmarks_ReturnsJsonString()
{
// Arrange
var bookmarks = new BookmarksData
var bookmarks = new Bookmarks
{
Data = new List<BookmarkData>
{
@@ -218,7 +216,7 @@ public class BookmarkJsonParserTests
public void SerializeBookmarks_EmptyBookmarks_ReturnsValidJson()
{
// Arrange
var bookmarks = new BookmarksData();
var bookmarks = new Bookmarks();
// Act
var result = _parser.SerializeBookmarks(bookmarks);
@@ -243,7 +241,7 @@ public class BookmarkJsonParserTests
public void ParseBookmarks_RoundTripSerialization_PreservesData()
{
// Arrange
var originalBookmarks = new BookmarksData
var originalBookmarks = new Bookmarks
{
Data = new List<BookmarkData>
{
@@ -265,6 +263,7 @@ public class BookmarkJsonParserTests
{
Assert.AreEqual(originalBookmarks.Data[i].Name, parsedBookmarks.Data[i].Name);
Assert.AreEqual(originalBookmarks.Data[i].Bookmark, parsedBookmarks.Data[i].Bookmark);
Assert.AreEqual(originalBookmarks.Data[i].IsPlaceholder, parsedBookmarks.Data[i].IsPlaceholder);
}
}
@@ -297,6 +296,70 @@ public class BookmarkJsonParserTests
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Data.Count);
Assert.IsFalse(result.Data[0].IsPlaceholder);
Assert.IsTrue(result.Data[1].IsPlaceholder);
Assert.IsTrue(result.Data[2].IsPlaceholder);
}
[TestMethod]
public void ParseBookmarks_IsWebUrl_CorrectlyIdentifiesWebUrls()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "HTTPS Website",
"Bookmark": "https://www.google.com"
},
{
"Name": "HTTP Website",
"Bookmark": "http://example.com"
},
{
"Name": "Website without protocol",
"Bookmark": "www.github.com"
},
{
"Name": "Local File Path",
"Bookmark": "C:\\Users\\test\\Documents\\file.txt"
},
{
"Name": "Network Path",
"Bookmark": "\\\\server\\share\\file.txt"
},
{
"Name": "Executable",
"Bookmark": "notepad.exe"
},
{
"Name": "File URI",
"Bookmark": "file:///C:/temp/file.txt"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(7, result.Data.Count);
// Web URLs should return true
Assert.IsTrue(result.Data[0].IsWebUrl(), "HTTPS URL should be identified as web URL");
Assert.IsTrue(result.Data[1].IsWebUrl(), "HTTP URL should be identified as web URL");
// This case will fail. We need to consider if we need to support pure domain value in bookmark.
// Assert.IsTrue(result.Data[2].IsWebUrl(), "Domain without protocol should be identified as web URL");
// Non-web URLs should return false
Assert.IsFalse(result.Data[3].IsWebUrl(), "Local file path should not be identified as web URL");
Assert.IsFalse(result.Data[4].IsWebUrl(), "Network path should not be identified as web URL");
Assert.IsFalse(result.Data[5].IsWebUrl(), "Executable should not be identified as web URL");
Assert.IsFalse(result.Data[6].IsWebUrl(), "File URI should not be identified as web URL");
}
[TestMethod]
@@ -352,10 +415,23 @@ public class BookmarkJsonParserTests
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(9, result.Data.Count);
// Should be identified as placeholders
Assert.IsTrue(result.Data[0].IsPlaceholder, "Simple placeholder should be identified");
Assert.IsTrue(result.Data[1].IsPlaceholder, "Multiple placeholders should be identified");
Assert.IsTrue(result.Data[2].IsPlaceholder, "Web URL with placeholder should be identified");
Assert.IsTrue(result.Data[3].IsPlaceholder, "Complex placeholder should be identified");
Assert.IsTrue(result.Data[8].IsPlaceholder, "Empty placeholder should be identified");
// Should NOT be identified as placeholders
Assert.IsFalse(result.Data[4].IsPlaceholder, "Regular URL should not be placeholder");
Assert.IsFalse(result.Data[5].IsPlaceholder, "Local file should not be placeholder");
Assert.IsFalse(result.Data[6].IsPlaceholder, "Only opening brace should not be placeholder");
Assert.IsFalse(result.Data[7].IsPlaceholder, "Only closing brace should not be placeholder");
}
[TestMethod]
public void ParseBookmarks_MixedProperties_CorrectlyIdentifiesPlaceholder()
public void ParseBookmarks_MixedProperties_CorrectlyIdentifiesBothWebUrlAndPlaceholder()
{
// Arrange
var json = """
@@ -387,5 +463,73 @@ public class BookmarkJsonParserTests
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(4, result.Data.Count);
// Web URL with placeholder
Assert.IsTrue(result.Data[0].IsWebUrl(), "Web URL with placeholder should be identified as web URL");
Assert.IsTrue(result.Data[0].IsPlaceholder, "Web URL with placeholder should be identified as placeholder");
// Web URL without placeholder
Assert.IsTrue(result.Data[1].IsWebUrl(), "Web URL without placeholder should be identified as web URL");
Assert.IsFalse(result.Data[1].IsPlaceholder, "Web URL without placeholder should not be identified as placeholder");
// Local file with placeholder
Assert.IsFalse(result.Data[2].IsWebUrl(), "Local file with placeholder should not be identified as web URL");
Assert.IsTrue(result.Data[2].IsPlaceholder, "Local file with placeholder should be identified as placeholder");
// Local file without placeholder
Assert.IsFalse(result.Data[3].IsWebUrl(), "Local file without placeholder should not be identified as web URL");
Assert.IsFalse(result.Data[3].IsPlaceholder, "Local file without placeholder should not be identified as placeholder");
}
[TestMethod]
public void ParseBookmarks_EdgeCaseUrls_CorrectlyIdentifiesWebUrls()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "FTP URL",
"Bookmark": "ftp://files.example.com"
},
{
"Name": "HTTPS with port",
"Bookmark": "https://localhost:8080"
},
{
"Name": "IP Address",
"Bookmark": "http://192.168.1.1"
},
{
"Name": "Subdomain",
"Bookmark": "https://api.github.com"
},
{
"Name": "Domain only",
"Bookmark": "example.com"
},
{
"Name": "Not a URL - no dots",
"Bookmark": "localhost"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(6, result.Data.Count);
Assert.IsFalse(result.Data[0].IsWebUrl(), "FTP URL should not be identified as web URL");
Assert.IsTrue(result.Data[1].IsWebUrl(), "HTTPS with port should be identified as web URL");
Assert.IsTrue(result.Data[2].IsWebUrl(), "IP Address with HTTP should be identified as web URL");
Assert.IsTrue(result.Data[3].IsWebUrl(), "Subdomain should be identified as web URL");
// This case will fail. We need to consider if we need to support pure domain value in bookmark.
// Assert.IsTrue(result.Data[4].IsWebUrl(), "Domain only should be identified as web URL");
Assert.IsFalse(result.Data[5].IsWebUrl(), "Single word without dots should not be identified as web URL");
}
}

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.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class BookmarkManagerTests
{
[TestMethod]
public void BookmarkManager_CanBeInstantiated()
{
// Arrange & Act
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource());
// Assert
Assert.IsNotNull(bookmarkManager);
}
[TestMethod]
public void BookmarkManager_InitialBookmarksEmpty()
{
// Arrange
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource());
// Act
var bookmarks = bookmarkManager.Bookmarks;
// Assert
Assert.IsNotNull(bookmarks);
Assert.AreEqual(0, bookmarks.Count);
}
[TestMethod]
public void BookmarkManager_InitialBookmarksCorruptedData()
{
// Arrange
var json = "@*>$ß Corrupted data. Hey, this is not JSON!";
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource(json));
// Act
var bookmarks = bookmarkManager.Bookmarks;
// Assert
Assert.IsNotNull(bookmarks);
Assert.AreEqual(0, bookmarks.Count);
}
[TestMethod]
public void BookmarkManager_InitializeWithExistingData()
{
// Arrange
const string json = """
{
"Data":[
{"Id":"d290f1ee-6c54-4b01-90e6-d701748f0851","Name":"Bookmark1","Bookmark":"C:\\Path1"},
{"Id":"c4a760a4-5b63-4c9e-b8b3-2c3f5f3e6f7a","Name":"Bookmark2","Bookmark":"D:\\Path2"}
]
}
""";
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource(json));
// Act
var bookmarks = bookmarkManager.Bookmarks?.ToList();
// Assert
Assert.IsNotNull(bookmarks);
Assert.AreEqual(2, bookmarks.Count);
Assert.AreEqual("Bookmark1", bookmarks[0].Name);
Assert.AreEqual("C:\\Path1", bookmarks[0].Bookmark);
Assert.AreEqual(Guid.Parse("d290f1ee-6c54-4b01-90e6-d701748f0851"), bookmarks[0].Id);
Assert.AreEqual("Bookmark2", bookmarks[1].Name);
Assert.AreEqual("D:\\Path2", bookmarks[1].Bookmark);
Assert.AreEqual(Guid.Parse("c4a760a4-5b63-4c9e-b8b3-2c3f5f3e6f7a"), bookmarks[1].Id);
}
[TestMethod]
public void BookmarkManager_InitializeWithLegacyData_GeneratesIds()
{
// Arrange
const string json = """
{
"Data":
[
{ "Name":"Bookmark1", "Bookmark":"C:\\Path1" },
{ "Name":"Bookmark2", "Bookmark":"D:\\Path2" }
]
}
""";
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource(json));
// Act
var bookmarks = bookmarkManager.Bookmarks?.ToList();
// Assert
Assert.IsNotNull(bookmarks);
Assert.AreEqual(2, bookmarks.Count);
Assert.AreEqual("Bookmark1", bookmarks[0].Name);
Assert.AreEqual("C:\\Path1", bookmarks[0].Bookmark);
Assert.AreNotEqual(Guid.Empty, bookmarks[0].Id);
Assert.AreEqual("Bookmark2", bookmarks[1].Name);
Assert.AreEqual("D:\\Path2", bookmarks[1].Bookmark);
Assert.AreNotEqual(Guid.Empty, bookmarks[1].Id);
Assert.AreNotEqual(bookmarks[0].Id, bookmarks[1].Id);
}
[TestMethod]
public void BookmarkManager_AddBookmark_WorksCorrectly()
{
// Arrange
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource());
var bookmarkAddedEventFired = false;
bookmarkManager.BookmarkAdded += (bookmark) =>
{
bookmarkAddedEventFired = true;
Assert.AreEqual("TestBookmark", bookmark.Name);
Assert.AreEqual("C:\\TestPath", bookmark.Bookmark);
};
// Act
var addedBookmark = bookmarkManager.Add("TestBookmark", "C:\\TestPath");
// Assert
var bookmarks = bookmarkManager.Bookmarks;
Assert.AreEqual(1, bookmarks.Count);
Assert.AreEqual(addedBookmark, bookmarks.First());
Assert.IsTrue(bookmarkAddedEventFired);
}
[TestMethod]
public void BookmarkManager_RemoveBookmark_WorksCorrectly()
{
// Arrange
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource());
var addedBookmark = bookmarkManager.Add("TestBookmark", "C:\\TestPath");
var bookmarkRemovedEventFired = false;
bookmarkManager.BookmarkRemoved += (bookmark) =>
{
bookmarkRemovedEventFired = true;
Assert.AreEqual(addedBookmark, bookmark);
};
// Act
var removeResult = bookmarkManager.Remove(addedBookmark.Id);
// Assert
var bookmarks = bookmarkManager.Bookmarks;
Assert.IsTrue(removeResult);
Assert.AreEqual(0, bookmarks.Count);
Assert.IsTrue(bookmarkRemovedEventFired);
}
[TestMethod]
public void BookmarkManager_UpdateBookmark_WorksCorrectly()
{
// Arrange
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource());
var addedBookmark = bookmarkManager.Add("TestBookmark", "C:\\TestPath");
var bookmarkUpdatedEventFired = false;
bookmarkManager.BookmarkUpdated += (data, bookmarkData) =>
{
bookmarkUpdatedEventFired = true;
Assert.AreEqual(addedBookmark, data);
Assert.AreEqual("UpdatedBookmark", bookmarkData.Name);
Assert.AreEqual("D:\\UpdatedPath", bookmarkData.Bookmark);
};
// Act
var updatedBookmark = bookmarkManager.Update(addedBookmark.Id, "UpdatedBookmark", "D:\\UpdatedPath");
// Assert
var bookmarks = bookmarkManager.Bookmarks;
Assert.IsNotNull(updatedBookmark);
Assert.AreEqual(1, bookmarks.Count);
Assert.AreEqual(updatedBookmark, bookmarks.First());
Assert.AreEqual("UpdatedBookmark", updatedBookmark.Name);
Assert.AreEqual("D:\\UpdatedPath", updatedBookmark.Bookmark);
Assert.IsTrue(bookmarkUpdatedEventFired);
}
}

View File

@@ -1,303 +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.Ext.Bookmarks.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public partial class BookmarkResolverTests
{
[DataTestMethod]
[DynamicData(nameof(CommonClassificationData.CommonCases), typeof(CommonClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Common_ValidateCommonClassification(PlaceholderClassificationCase c) => await RunShared(c);
[DataTestMethod]
[DynamicData(nameof(CommonClassificationData.UwpAumidCases), typeof(CommonClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Common_ValidateUwpAumidClassification(PlaceholderClassificationCase c) => await RunShared(c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(CommonClassificationData.UnquotedRelativePaths), dynamicDataDeclaringType: typeof(CommonClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Common_ValidateUnquotedRelativePathScenarios(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(CommonClassificationData.UnquotedShellProtocol), dynamicDataDeclaringType: typeof(CommonClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Common_ValidateUnquotedShellProtocolScenarios(PlaceholderClassificationCase c) => await RunShared(c: c);
private static class CommonClassificationData
{
public static IEnumerable<object[]> CommonCases()
{
return
[
[
new PlaceholderClassificationCase(
Name: "HTTPS URL",
Input: "https://microsoft.com",
ExpectSuccess: true,
ExpectedKind: CommandKind.WebUrl,
ExpectedTarget: "https://microsoft.com",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "WWW URL without scheme",
Input: "www.example.com",
ExpectSuccess: true,
ExpectedKind: CommandKind.WebUrl,
ExpectedTarget: "https://www.example.com",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "HTTP URL with query",
Input: "http://yahoo.com?p=search",
ExpectSuccess: true,
ExpectedKind: CommandKind.WebUrl,
ExpectedTarget: "http://yahoo.com?p=search",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "Mailto protocol",
Input: "mailto:user@example.com",
ExpectSuccess: true,
ExpectedKind: CommandKind.Protocol,
ExpectedTarget: "mailto:user@example.com",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "MS-Settings protocol",
Input: "ms-settings:display",
ExpectSuccess: true,
ExpectedKind: CommandKind.Protocol,
ExpectedTarget: "ms-settings:display",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "Custom protocol",
Input: "myapp:doit",
ExpectSuccess: true,
ExpectedKind: CommandKind.Protocol,
ExpectedTarget: "myapp:doit",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "Not really a valid protocol",
Input: "this is not really a protocol myapp: doit",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "this",
ExpectedArguments: "is not really a protocol myapp: doit",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "Drive",
Input: "C:",
ExpectSuccess: true,
ExpectedKind: CommandKind.Directory,
ExpectedTarget: "C:\\",
ExpectedLaunch: LaunchMethod.ExplorerOpen,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "Non-existing path with extension",
Input: "C:\\this-folder-should-not-exist-12345\\file.txt",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: "C:\\this-folder-should-not-exist-12345\\file.txt",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "Unknown fallback",
Input: "some_unlikely_command_name_12345",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "some_unlikely_command_name_12345",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false),
],
[new PlaceholderClassificationCase(
Name: "Simple unquoted executable path",
Input: "C:\\Windows\\System32\\notepad.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\Windows\\System32\\notepad.exe",
ExpectedArguments: string.Empty,
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "Unquoted document path (non existed file)",
Input: "C:\\Users\\John\\Documents\\file.txt",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: "C:\\Users\\John\\Documents\\file.txt",
ExpectedArguments: string.Empty,
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false),
]
];
}
public static IEnumerable<object[]> UwpAumidCases() =>
[
[
new PlaceholderClassificationCase(
Name: "UWP AUMID with AppsFolder prefix",
Input: "shell:AppsFolder\\Microsoft.WindowsCalculator_8wekyb3d8bbwe!App",
ExpectSuccess: true,
ExpectedKind: CommandKind.Aumid,
ExpectedTarget: "shell:AppsFolder\\Microsoft.WindowsCalculator_8wekyb3d8bbwe!App",
ExpectedLaunch: LaunchMethod.ActivateAppId,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "UWP AUMID with AppsFolder prefix and argument (Trap)",
Input: "shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App --maximized",
ExpectSuccess: true,
ExpectedKind: CommandKind.Aumid,
ExpectedTarget: "shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App --maximized",
ExpectedArguments: string.Empty,
ExpectedLaunch: LaunchMethod.ActivateAppId,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "UWP AUMID via AppsFolder",
Input: "shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App",
ExpectSuccess: true,
ExpectedKind: CommandKind.Aumid,
ExpectedTarget: "shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App",
ExpectedLaunch: LaunchMethod.ActivateAppId,
ExpectedIsPlaceholder: false),
],
];
public static IEnumerable<object[]> UnquotedShellProtocol() =>
[
[
new PlaceholderClassificationCase(
Name: "Shell for This PC (shell:::{20D04FE0-3AEA-1069-A2D8-08002B30309D})",
Input: "shell:::{20D04FE0-3AEA-1069-A2D8-08002B30309D}",
ExpectSuccess: true,
ExpectedKind: CommandKind.VirtualShellItem,
ExpectedTarget: "shell:::{20D04FE0-3AEA-1069-A2D8-08002B30309D}",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "Shell for This PC (::{20D04FE0-3AEA-1069-A2D8-08002B30309D})",
Input: "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}",
ExpectSuccess: true,
ExpectedKind: CommandKind.VirtualShellItem,
ExpectedTarget: "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "Shell protocol for My Documents (shell:::{450D8FBA-AD25-11D0-98A8-0800361B1103}",
Input: "shell:::{450D8FBA-AD25-11D0-98A8-0800361B1103}",
ExpectSuccess: true,
ExpectedKind: CommandKind.Directory,
ExpectedTarget: Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
ExpectedLaunch: LaunchMethod.ExplorerOpen,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "Shell protocol for My Documents (::{450D8FBA-AD25-11D0-98A8-0800361B1103}",
Input: "::{450D8FBA-AD25-11D0-98A8-0800361B1103}",
ExpectSuccess: true,
ExpectedKind: CommandKind.Directory,
ExpectedTarget: Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
ExpectedLaunch: LaunchMethod.ExplorerOpen,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "Shell protocol for AppData (shell:appdata)",
Input: "shell:appdata",
ExpectSuccess: true,
ExpectedKind: CommandKind.Directory,
ExpectedTarget: Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
ExpectedLaunch: LaunchMethod.ExplorerOpen,
ExpectedIsPlaceholder: false),
],
[
// let's pray this works on all systems
new PlaceholderClassificationCase(
Name: "Shell protocol for AppData + subpath (shell:appdata\\microsoft)",
Input: "shell:appdata\\microsoft",
ExpectSuccess: true,
ExpectedKind: CommandKind.Directory,
ExpectedTarget: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft"),
ExpectedLaunch: LaunchMethod.ExplorerOpen,
ExpectedIsPlaceholder: false),
],
];
public static IEnumerable<object[]> UnquotedRelativePaths() =>
[
[
new PlaceholderClassificationCase(
Name: "Unquoted relative current path",
Input: ".\\file.txt",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "file.txt")),
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
#if CMDPAL_ENABLE_UNSAFE_TESTS
It's not really a good idea blindly write to directory out of user profile
[
new PlaceholderClassificationCase(
Name: "Unquoted relative parent path",
Input: "..\\parent folder\\file.txt",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "..", "parent folder", "file.txt")),
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
#endif // CMDPAL_ENABLE_UNSAFE_TESTS
[
new PlaceholderClassificationCase(
Name: "Unquoted relative home folder",
Input: $"~\\{_testDirName}\\app.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: Path.Combine(_testDirPath, "app.exe"),
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
]
];
}
}

View File

@@ -1,369 +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;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
using Microsoft.CmdPal.Ext.Bookmarks.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
public partial class BookmarkResolverTests
{
[DataTestMethod]
[DynamicData(nameof(PlaceholderClassificationData.PlaceholderCases), typeof(PlaceholderClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Placeholders_ValidatePlaceholderClassification(PlaceholderClassificationCase c) => await RunShared(c);
[DataTestMethod]
[DynamicData(nameof(PlaceholderClassificationData.EdgeCases), typeof(PlaceholderClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Placeholders_ValidatePlaceholderEdgeCases(PlaceholderClassificationCase c)
{
// Arrange
IBookmarkResolver resolver = new BookmarkResolver(new PlaceholderParser());
// Act & Assert - Should not throw exceptions
var classification = await resolver.TryClassifyAsync(c.Input, CancellationToken.None);
Assert.IsNotNull(classification);
Assert.AreEqual(c.ExpectSuccess, classification.Success);
if (c.ExpectSuccess && classification.Result != null)
{
Assert.AreEqual(c.ExpectedIsPlaceholder, classification.Result.IsPlaceholder);
Assert.AreEqual(c.Input, classification.Result.Input, "OriginalInput should be preserved");
}
}
private static class PlaceholderClassificationData
{
public static IEnumerable<object[]> PlaceholderCases()
{
// UWP/AUMID with placeholders
yield return
[
new PlaceholderClassificationCase(
Name: "UWP AUMID with package placeholder",
Input: "shell:AppsFolder\\{packageFamily}!{appId}",
ExpectSuccess: true,
ExpectedKind: CommandKind.Aumid,
ExpectedTarget: "shell:AppsFolder\\{packageFamily}!{appId}",
ExpectedLaunch: LaunchMethod.ActivateAppId,
ExpectedIsPlaceholder: true)
];
yield return
[
// Expects no special handling
new PlaceholderClassificationCase(
Name: "Bare UWP AUMID with placeholders",
Input: "{packageFamily}!{appId}",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "{packageFamily}!{appId}",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
// Web URLs with placeholders
yield return
[
new PlaceholderClassificationCase(
Name: "HTTPS URL with domain placeholder",
Input: "https://{domain}/path",
ExpectSuccess: true,
ExpectedKind: CommandKind.WebUrl,
ExpectedTarget: "https://{domain}/path",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
yield return
[
new PlaceholderClassificationCase(
Name: "WWW URL with site placeholder",
Input: "www.{site}.com",
ExpectSuccess: true,
ExpectedKind: CommandKind.WebUrl,
ExpectedTarget: "https://www.{site}.com",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
yield return
[
new PlaceholderClassificationCase(
Name: "WWW URL - Yahoo with Search",
Input: "http://yahoo.com?p={search}",
ExpectSuccess: true,
ExpectedKind: CommandKind.WebUrl,
ExpectedTarget: "http://yahoo.com?p={search}",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
// Protocol URLs with placeholders
yield return
[
new PlaceholderClassificationCase(
Name: "Mailto protocol with email placeholder",
Input: "mailto:{email}",
ExpectSuccess: true,
ExpectedKind: CommandKind.Protocol,
ExpectedTarget: "mailto:{email}",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
yield return
[
new PlaceholderClassificationCase(
Name: "MS-Settings protocol with category placeholder",
Input: "ms-settings:{category}",
ExpectSuccess: true,
ExpectedKind: CommandKind.Protocol,
ExpectedTarget: "ms-settings:{category}",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
// File executables with placeholders - These might classify as Unknown currently
// due to nonexistent paths, but should preserve placeholder flag
yield return
[
new PlaceholderClassificationCase(
Name: "Executable with profile path placeholder",
Input: "{userProfile}\\Documents\\app.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable, // May be Unknown if path doesn't exist
ExpectedTarget: "{userProfile}\\Documents\\app.exe",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
yield return
[
new PlaceholderClassificationCase(
Name: "Executable with program files placeholder",
Input: "{programFiles}\\MyApp\\tool.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable, // May be Unknown if path doesn't exist
ExpectedTarget: "{programFiles}\\MyApp\\tool.exe",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
// Commands with placeholders
yield return
[
new PlaceholderClassificationCase(
Name: "Command with placeholder and arguments",
Input: "{editor} {filename}",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown, // Likely Unknown since command won't be found in PATH
ExpectedTarget: "{editor}",
ExpectedArguments: "{filename}",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
// Directory paths with placeholders
yield return
[
new PlaceholderClassificationCase(
Name: "Directory with user profile placeholder",
Input: "{userProfile}\\Documents",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown, // May be Unknown if path doesn't exist during classification
ExpectedTarget: "{userProfile}\\Documents",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
// Complex quoted paths with placeholders
yield return
[
new PlaceholderClassificationCase(
Name: "Quoted executable path with placeholders and args",
Input: "\"{programFiles}\\{appName}\\{executable}.exe\" --verbose",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable, // Likely Unknown due to nonexistent path
ExpectedTarget: "{programFiles}\\{appName}\\{executable}.exe",
ExpectedArguments: "--verbose",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
// Shell paths with placeholders
yield return
[
new PlaceholderClassificationCase(
Name: "Shell folder with placeholder",
Input: "shell:{folder}\\{filename}",
ExpectSuccess: true,
ExpectedKind: CommandKind.VirtualShellItem,
ExpectedTarget: "shell:{folder}\\{filename}",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
// Shell paths with placeholders
yield return
[
new PlaceholderClassificationCase(
Name: "Shell folder with placeholder",
Input: "shell:knownFolder\\{filename}",
ExpectSuccess: true,
ExpectedKind: CommandKind.VirtualShellItem,
ExpectedTarget: "shell:knownFolder\\{filename}",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
yield return
[
// cmd /K {param1}
new PlaceholderClassificationCase(
Name: "Command with braces in arguments",
Input: "cmd /K {param1}",
ExpectSuccess: true,
ExpectedKind: CommandKind.PathCommand,
ExpectedTarget: "C:\\Windows\\system32\\cmd.EXE",
ExpectedArguments: "/K {param1}",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
// Mixed literal and placeholder paths
yield return
[
new PlaceholderClassificationCase(
Name: "Mixed literal and placeholder path",
Input: "C:\\{folder}\\app.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable, // Behavior depends on partial path resolution
ExpectedTarget: "C:\\{folder}\\app.exe",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
// Multiple placeholders
yield return
[
new PlaceholderClassificationCase(
Name: "Multiple placeholders in path",
Input: "{drive}\\{folder}\\{subfolder}\\{file}.{ext}",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "{drive}\\{folder}\\{subfolder}\\{file}.{ext}",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
}
public static IEnumerable<object[]> EdgeCases()
{
// Empty and malformed placeholders
yield return
[
new PlaceholderClassificationCase(
Name: "Empty placeholder",
Input: "{} file.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "{} file.exe",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
];
yield return
[
new PlaceholderClassificationCase(
Name: "Unclosed placeholder",
Input: "{unclosed file.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "{unclosed file.exe",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
];
yield return
[
new PlaceholderClassificationCase(
Name: "Placeholder with spaces",
Input: "{with spaces}\\file.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "{with spaces}\\file.exe",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
];
yield return
[
new PlaceholderClassificationCase(
Name: "Nested placeholders",
Input: "{outer{inner}}\\file.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "{outer{inner}}\\file.exe",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
];
yield return
[
new PlaceholderClassificationCase(
Name: "Only closing brace",
Input: "file} something",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "file} something",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
];
// Very long placeholder names
yield return
[
new PlaceholderClassificationCase(
Name: "Very long placeholder name",
Input: "{thisIsVeryLongPlaceholderNameThatShouldStillWorkProperly}\\file.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "{thisIsVeryLongPlaceholderNameThatShouldStillWorkProperly}\\file.exe",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
// Special characters in placeholders
yield return
[
new PlaceholderClassificationCase(
Name: "Placeholder with underscores",
Input: "{user_profile}\\file.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "{user_profile}\\file.exe",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
yield return
[
new PlaceholderClassificationCase(
Name: "Placeholder with numbers",
Input: "{path123}\\file.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "{path123}\\file.exe",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: true)
];
}
}
}

View File

@@ -1,669 +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.Ext.Bookmarks.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
public partial class BookmarkResolverTests
{
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.MixedQuotesScenarios), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateMixedQuotesScenarios(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.EscapedQuotes), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateEscapedQuotes(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.PartialMalformedQuotes), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidatePartialMalformedQuotes(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.EnvironmentVariablesWithQuotes), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateEnvironmentVariablesWithQuotes(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.ShellProtocolPathsWithQuotes), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateShellProtocolPathsWithQuotes(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.CommandFlagsAndOptions), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateCommandFlagsAndOptions(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.NetworkPathsUnc), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateNetworkPathsUnc(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.RelativePathsWithQuotes), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateRelativePathsWithQuotes(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.EmptyAndWhitespaceCases), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateEmptyAndWhitespaceCases(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.RealWorldCommandScenarios), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateRealWorldCommandScenarios(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.SpecialCharactersInPaths), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateSpecialCharactersInPaths(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.QuotedPathsCurrentlyBroken), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateQuotedPathsCurrentlyBroken(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.QuotedPathsInCommands), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateQuotedPathsInCommands(PlaceholderClassificationCase c) => await RunShared(c: c);
[DataTestMethod]
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.QuotedAumid), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
public async Task Quoted_ValidateQuotedUwpAppAumidCommands(PlaceholderClassificationCase c) => await RunShared(c: c);
public static class QuotedClassificationData
{
public static IEnumerable<object[]> MixedQuotesScenarios() =>
[
[
new PlaceholderClassificationCase(
Name: "Executable with quoted argument",
Input: "C:\\Windows\\notepad.exe \"C:\\my file.txt\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\Windows\\notepad.exe",
ExpectedArguments: "\"C:\\my file.txt\"",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "App with quoted argument containing spaces",
Input: "app.exe \"argument with spaces\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "app.exe",
ExpectedArguments: "\"argument with spaces\"",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Tool with input flag and quoted file",
Input: "C:\\tool.exe -input \"data file.txt\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\tool.exe",
ExpectedArguments: "-input \"data file.txt\"",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Multiple quoted arguments after path",
Input: "\"C:\\Program Files\\app.exe\" -file \"C:\\data\\input.txt\" -output \"C:\\results\\output.txt\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\Program Files\\app.exe",
ExpectedArguments: "-file \"C:\\data\\input.txt\" -output \"C:\\results\\output.txt\"",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Command with two quoted paths",
Input: "cmd /c \"C:\\First Path\\tool.exe\" \"C:\\Second Path\\file.txt\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.PathCommand,
ExpectedTarget: "C:\\Windows\\system32\\cmd.EXE",
ExpectedArguments: "/c \"C:\\First Path\\tool.exe\" \"C:\\Second Path\\file.txt\"",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
]
];
public static IEnumerable<object[]> EscapedQuotes() =>
[
[
new PlaceholderClassificationCase(
Name: "Path with escaped quotes in folder name",
Input: "C:\\Windows\\\\\\\"System32\\\\\\\"CatRoot\\\\",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "C:\\Windows\\\\\\\"System32\\\\\\\"CatRoot\\\\",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted path with trailing escaped quote",
Input: "\"C:\\Windows\\\\\\\"\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.Directory,
ExpectedTarget: "C:\\Windows\\",
ExpectedLaunch: LaunchMethod.ExplorerOpen,
ExpectedIsPlaceholder: false)
]
];
public static IEnumerable<object[]> PartialMalformedQuotes() =>
[
[
new PlaceholderClassificationCase(
Name: "Unclosed quote at start",
Input: "\"C:\\Program Files\\app.exe",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\Program Files\\app.exe",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quote in middle of unquoted path",
Input: "C:\\Some\\\"Path\\file.txt",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: "C:\\Some\\\"Path\\file.txt",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Unclosed quote - never ends",
Input: "\"Starts quoted but never ends",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "Starts quoted but never ends",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
]
];
public static IEnumerable<object[]> EnvironmentVariablesWithQuotes() =>
[
[
new PlaceholderClassificationCase(
Name: "Quoted environment variable path with spaces",
Input: "\"%ProgramFiles%\\MyApp\\app.exe\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "MyApp", "app.exe"),
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted USERPROFILE with document path",
Input: "\"%USERPROFILE%\\Documents\\file with spaces.txt\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents", "file with spaces.txt"),
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Environment variable with trailing args",
Input: "\"%ProgramFiles%\\App\" with args",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "App"),
ExpectedArguments: "with args",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Environment variable with trailing args",
Input: "%ProgramFiles%\\App with args",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "App"),
ExpectedArguments: "with args",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
];
public static IEnumerable<object[]> ShellProtocolPathsWithQuotes() =>
[
[
new PlaceholderClassificationCase(
Name: "Quoted shell:Downloads",
Input: "\"shell:Downloads\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.Directory,
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"),
ExpectedLaunch: LaunchMethod.ExplorerOpen,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted shell:Downloads with subpath",
Input: "\"shell:Downloads\\file.txt\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads", "file.txt"),
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Shell Desktop with subpath",
Input: "shell:Desktop\\file.txt",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "file.txt"),
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted shell path with trailing text",
Input: "\"shell:Programs\" extra",
ExpectSuccess: true,
ExpectedKind: CommandKind.Directory,
ExpectedTarget: Path.Combine(paths: Environment.GetFolderPath(Environment.SpecialFolder.Programs)),
ExpectedLaunch: LaunchMethod.ExplorerOpen,
ExpectedIsPlaceholder: false)
]
];
public static IEnumerable<object[]> CommandFlagsAndOptions() =>
[
[
new PlaceholderClassificationCase(
Name: "Path followed by flag with quoted value",
Input: "C:\\app.exe -flag \"value\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\app.exe",
ExpectedArguments: "-flag \"value\"",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted tool with equals-style flag",
Input: "\"C:\\Program Files\\tool.exe\" --input=file.txt",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\Program Files\\tool.exe",
ExpectedArguments: "--input=file.txt",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Path with slash option and quoted value",
Input: "C:\\tool.exe /option \"quoted value\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\tool.exe",
ExpectedArguments: "/option \"quoted value\"",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Flag before quoted path",
Input: "--path \"C:\\Program Files\\app.exe\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "--path",
ExpectedArguments: "\"C:\\Program Files\\app.exe\"",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
]
];
public static IEnumerable<object[]> NetworkPathsUnc() =>
[
[
new PlaceholderClassificationCase(
Name: "UNC path unquoted",
Input: "\\\\server\\share\\folder\\file.txt",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: "\\\\server\\share\\folder\\file.txt",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted UNC path with spaces",
Input: "\"\\\\server\\share with spaces\\file.txt\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: "\\\\server\\share with spaces\\file.txt",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "UNC path with trailing args",
Input: "\"\\\\server\\share\\\" with args",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: "\\\\server\\share\\",
ExpectedArguments: "with args",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted UNC app with flag",
Input: "\"\\\\server\\My Share\\app.exe\" --flag",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "\\\\server\\My Share\\app.exe",
ExpectedArguments: "--flag",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
]
];
public static IEnumerable<object[]> RelativePathsWithQuotes() =>
[
[
new PlaceholderClassificationCase(
Name: "Quoted relative current path",
Input: "\".\\file.txt\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "file.txt")),
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted relative parent path",
Input: "\"..\\parent folder\\file.txt\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "..", "parent folder", "file.txt")),
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted relative home folder",
Input: "\"~\\current folder\\app.exe\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "current folder\\app.exe"),
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
]
];
public static IEnumerable<object[]> EmptyAndWhitespaceCases() =>
[
[
new PlaceholderClassificationCase(
Name: "Empty string",
Input: string.Empty,
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: string.Empty,
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Only whitespace",
Input: " ",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: " ",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Just empty quotes",
Input: "\"\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: string.Empty,
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted single space",
Input: "\" \"",
ExpectSuccess: true,
ExpectedKind: CommandKind.Unknown,
ExpectedTarget: " ",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
]
];
public static IEnumerable<object[]> RealWorldCommandScenarios() =>
[
#if CMDPAL_ENABLE_UNSAFE_TESTS
[
new PlaceholderClassificationCase(
Name: "Git clone command with full exe path with quoted path",
Input: "\"C:\\Program Files\\Git\\bin\\git.exe\" clone repo",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\Program Files\\Git\\bin\\git.exe",
ExpectedArguments: "clone repo",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Git clone command with quoted path",
Input: "git clone repo",
ExpectSuccess: true,
ExpectedKind: CommandKind.PathCommand,
ExpectedTarget: "C:\\Program Files\\Git\\cmd\\git.EXE",
ExpectedArguments: "clone repo",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Visual Studio devenv with solution",
Input: "\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Preview\\Common7\\IDE\\devenv.exe\" solution.sln",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\Program Files\\Microsoft Visual Studio\\2022\\Preview\\Common7\\IDE\\devenv.exe",
ExpectedArguments: "solution.sln",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Double-quoted Windows cmd pattern",
Input: "cmd /c \"\"C:\\Program Files\\app.exe\" arg1 arg2\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.PathCommand,
ExpectedTarget: "C:\\Windows\\system32\\cmd.EXE",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedArguments: "/c \"\"C:\\Program Files\\app.exe\" arg1 arg2\"",
ExpectedIsPlaceholder: false)
],
#endif
[
new PlaceholderClassificationCase(
Name: "PowerShell script with execution policy",
Input: "powershell -ExecutionPolicy Bypass -File \"C:\\Scripts\\My Script.ps1\" -param \"value\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.PathCommand,
ExpectedTarget: "C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\PowerShell.exe",
ExpectedArguments: "-ExecutionPolicy Bypass -File \"C:\\Scripts\\My Script.ps1\" -param \"value\"",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
];
public static IEnumerable<object[]> SpecialCharactersInPaths() =>
[
[
new PlaceholderClassificationCase(
Name: "Quoted path with square brackets",
Input: "\"C:\\Path\\file[1].txt\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: "C:\\Path\\file[1].txt",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted path with parentheses",
Input: "\"C:\\Folder (2)\\app.exe\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\Folder (2)\\app.exe",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted path with hyphens and underscores",
Input: "\"C:\\Path\\file_name-123.txt\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: "C:\\Path\\file_name-123.txt",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
]
];
public static IEnumerable<object[]> QuotedPathsCurrentlyBroken() =>
[
[
new PlaceholderClassificationCase(
Name: "Quoted path with spaces - complete path",
Input: "\"C:\\Program Files\\MyApp\\app.exe\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\Program Files\\MyApp\\app.exe",
ExpectedArguments: string.Empty,
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted path with spaces in user folder",
Input: "\"C:\\Users\\John Doe\\Documents\\file.txt\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: "C:\\Users\\John Doe\\Documents\\file.txt",
ExpectedArguments: string.Empty,
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted path with trailing arguments",
Input: "\"C:\\Program Files\\app.exe\" --flag",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\Program Files\\app.exe",
ExpectedArguments: "--flag",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted path with multiple arguments",
Input: "\"C:\\My Documents\\file.txt\" -output result.txt",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileDocument,
ExpectedTarget: "C:\\My Documents\\file.txt",
ExpectedArguments: "-output result.txt",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "Quoted path with trailing flag and value",
Input: "\"C:\\Tools\\converter.exe\" input.txt output.txt",
ExpectSuccess: true,
ExpectedKind: CommandKind.FileExecutable,
ExpectedTarget: "C:\\Tools\\converter.exe",
ExpectedArguments: "input.txt output.txt",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
]
];
public static IEnumerable<object[]> QuotedPathsInCommands() =>
[
[
new PlaceholderClassificationCase(
Name: "cmd /c with quoted path",
Input: "cmd /c \"C:\\Program Files\\tool.exe\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.PathCommand,
ExpectedTarget: "C:\\Windows\\system32\\cmd.exe",
ExpectedArguments: "/c \"C:\\Program Files\\tool.exe\"",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "PowerShell with quoted script path",
Input: "powershell -File \"C:\\Scripts\\my script.ps1\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.PathCommand,
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.System), "WindowsPowerShell", "v1.0", path4: "powershell.exe"),
ExpectedArguments: "-File \"C:\\Scripts\\my script.ps1\"",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
],
[
new PlaceholderClassificationCase(
Name: "runas with quoted executable",
Input: "runas /user:admin \"C:\\Windows\\System32\\cmd.exe\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.PathCommand,
ExpectedTarget: "C:\\Windows\\system32\\runas.exe",
ExpectedArguments: "/user:admin \"C:\\Windows\\System32\\cmd.exe\"",
ExpectedLaunch: LaunchMethod.ShellExecute,
ExpectedIsPlaceholder: false)
]
];
public static IEnumerable<object[]> QuotedAumid() =>
[
[
new PlaceholderClassificationCase(
Name: "Quoted UWP AUMID via AppsFolder",
Input: "\"shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App\"",
ExpectSuccess: true,
ExpectedKind: CommandKind.Aumid,
ExpectedTarget: "shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App",
ExpectedLaunch: LaunchMethod.ActivateAppId,
ExpectedIsPlaceholder: false),
],
[
new PlaceholderClassificationCase(
Name: "Quoted UWP AUMID with AppsFolder prefix and argument",
Input: "\"shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App\" --maximized",
ExpectSuccess: true,
ExpectedKind: CommandKind.Aumid,
ExpectedTarget: "shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App",
ExpectedArguments: "--maximized",
ExpectedLaunch: LaunchMethod.ActivateAppId,
ExpectedIsPlaceholder: false),
],
];
}
}

View File

@@ -1,102 +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.
#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
using Microsoft.CmdPal.Ext.Bookmarks.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
public partial class BookmarkResolverTests
{
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
private static string _testDirPath;
private static string _userHomeDirPath;
private static string _testDirName;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
[ClassInitialize]
public static void ClassSetup(TestContext context)
{
_userHomeDirPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
_testDirName = "CmdPalBookmarkTests" + Guid.NewGuid().ToString("N");
_testDirPath = Path.Combine(_userHomeDirPath, _testDirName);
Directory.CreateDirectory(_testDirPath);
// test files in user home
File.WriteAllText(Path.Combine(_userHomeDirPath, "file.txt"), "This is a test text file.");
// test files in test dir
File.WriteAllText(Path.Combine(_testDirPath, "file.txt"), "This is a test text file.");
File.WriteAllText(Path.Combine(_testDirPath, "app.exe"), "This is a test text file.");
}
[ClassCleanup]
public static void ClassCleanup()
{
if (Directory.Exists(_testDirPath))
{
Directory.Delete(_testDirPath, true);
}
if (File.Exists(Path.Combine(_userHomeDirPath, "file.txt")))
{
File.Delete(Path.Combine(_userHomeDirPath, "file.txt"));
}
}
// must be public static to be used as DataTestMethod data source
public static string FromCase(MethodInfo method, object[] data)
=> data is [PlaceholderClassificationCase c]
? c.Name
: $"{method.Name}({string.Join(", ", data.Select(row => row.ToString()))})";
private static async Task RunShared(PlaceholderClassificationCase c)
{
// Arrange
IBookmarkResolver resolver = new BookmarkResolver(new PlaceholderParser());
// Act
var classification = await resolver.TryClassifyAsync(c.Input, CancellationToken.None);
// Assert
Assert.IsNotNull(classification);
Assert.AreEqual(c.ExpectSuccess, classification.Success, "Success flag mismatch.");
if (c.ExpectSuccess)
{
Assert.IsNotNull(classification.Result, "Result should not be null for successful classification.");
Assert.AreEqual(c.ExpectedKind, classification.Result.Kind, $"CommandKind mismatch for input: {c.Input}");
Assert.AreEqual(c.ExpectedTarget, classification.Result.Target, StringComparer.OrdinalIgnoreCase, $"Target mismatch for input: {c.Input}");
Assert.AreEqual(c.ExpectedLaunch, classification.Result.Launch, $"LaunchMethod mismatch for input: {c.Input}");
Assert.AreEqual(c.ExpectedArguments, classification.Result.Arguments, $"Arguments mismatch for input: {c.Input}");
Assert.AreEqual(c.ExpectedIsPlaceholder, classification.Result.IsPlaceholder, $"IsPlaceholder mismatch for input: {c.Input}");
if (c.ExpectedDisplayName != null)
{
Assert.AreEqual(c.ExpectedDisplayName, classification.Result.DisplayName, $"DisplayName mismatch for input: {c.Input}");
}
}
}
public sealed record PlaceholderClassificationCase(
string Name, // Friendly name for Test Explorer
string Input, // Input string passed to classifier
bool ExpectSuccess, // Expected Success flag
CommandKind ExpectedKind, // Expected Result.Kind
string ExpectedTarget, // Expected Result.Target (normalized)
LaunchMethod ExpectedLaunch, // Expected Result.Launch
bool ExpectedIsPlaceholder, // Expected Result.IsPlaceholder
string ExpectedArguments = "", // Expected Result.Arguments
string? ExpectedDisplayName = null // Expected Result.DisplayName
);
}

View File

@@ -2,9 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
using Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
@@ -16,8 +16,8 @@ public class BookmarksCommandProviderTests
public void ProviderHasCorrectId()
{
// Setup
var mockBookmarkManager = new MockBookmarkManager();
var provider = new BookmarksCommandProvider(mockBookmarkManager);
var mockDataSource = new MockBookmarkDataSource();
var provider = new BookmarksCommandProvider(mockDataSource);
// Assert
Assert.AreEqual("Bookmarks", provider.Id);
@@ -27,8 +27,8 @@ public class BookmarksCommandProviderTests
public void ProviderHasDisplayName()
{
// Setup
var mockBookmarkManager = new MockBookmarkManager();
var provider = new BookmarksCommandProvider(mockBookmarkManager);
var mockDataSource = new MockBookmarkDataSource();
var provider = new BookmarksCommandProvider(mockDataSource);
// Assert
Assert.IsNotNull(provider.DisplayName);
@@ -39,8 +39,7 @@ public class BookmarksCommandProviderTests
public void ProviderHasIcon()
{
// Setup
var mockBookmarkManager = new MockBookmarkManager();
var provider = new BookmarksCommandProvider(mockBookmarkManager);
var provider = new BookmarksCommandProvider();
// Assert
Assert.IsNotNull(provider.Icon);
@@ -50,8 +49,7 @@ public class BookmarksCommandProviderTests
public void TopLevelCommandsNotEmpty()
{
// Setup
var mockBookmarkManager = new MockBookmarkManager();
var provider = new BookmarksCommandProvider(mockBookmarkManager);
var provider = new BookmarksCommandProvider();
// Act
var commands = provider.TopLevelCommands();
@@ -62,40 +60,47 @@ public class BookmarksCommandProviderTests
}
[TestMethod]
[Timeout(5000)]
public async Task ProviderWithMockData_LoadsBookmarksCorrectly()
public void ProviderWithMockData_LoadsBookmarksCorrectly()
{
// Arrange
var mockBookmarkManager = new MockBookmarkManager(
new BookmarkData("Test Bookmark", "http://test.com"),
new BookmarkData("Another Bookmark", "http://another.com"));
var provider = new BookmarksCommandProvider(mockBookmarkManager);
var jsonData = @"{
""Data"": [
{
""Name"": ""Test Bookmark"",
""Bookmark"": ""https://test.com""
},
{
""Name"": ""Another Bookmark"",
""Bookmark"": ""https://another.com""
}
]
}";
var dataSource = new MockBookmarkDataSource(jsonData);
var provider = new BookmarksCommandProvider(dataSource);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands, "commands != null");
Assert.IsNotNull(commands);
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
var testBookmark = commands.Where(c => c.Title.Contains("Test Bookmark")).FirstOrDefault();
// Should have three commandsAdd + two custom bookmarks
Assert.AreEqual(3, commands.Length);
// Wait until all BookmarkListItem commands are initialized
await Task.WhenAll(commands.OfType<Pages.BookmarkListItem>().Select(t => t.IsInitialized));
var addCommand = commands.FirstOrDefault(c => c.Title.Contains("Add bookmark"));
var testBookmark = commands.FirstOrDefault(c => c.Title.Contains("Test Bookmark"));
Assert.IsNotNull(addCommand, "addCommand != null");
Assert.IsNotNull(testBookmark, "testBookmark != null");
Assert.IsNotNull(addCommand);
Assert.IsNotNull(testBookmark);
}
[TestMethod]
public void ProviderWithEmptyData_HasOnlyAddCommand()
{
// Arrange
var mockBookmarkManager = new MockBookmarkManager();
var provider = new BookmarksCommandProvider(mockBookmarkManager);
var dataSource = new MockBookmarkDataSource(@"{ ""Data"": [] }");
var provider = new BookmarksCommandProvider(dataSource);
// Act
var commands = provider.TopLevelCommands();
@@ -106,7 +111,7 @@ public class BookmarksCommandProviderTests
// Only have Add command
Assert.AreEqual(1, commands.Length);
var addCommand = commands.FirstOrDefault(c => c.Title.Contains("Add bookmark"));
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
Assert.IsNotNull(addCommand);
}
@@ -115,7 +120,7 @@ public class BookmarksCommandProviderTests
{
// Arrange
var dataSource = new MockBookmarkDataSource("invalid json");
var provider = new BookmarksCommandProvider(new MockBookmarkManager());
var provider = new BookmarksCommandProvider(dataSource);
// Act
var commands = provider.TopLevelCommands();
@@ -126,7 +131,7 @@ public class BookmarksCommandProviderTests
// Only have one command. Will ignore json parse error.
Assert.AreEqual(1, commands.Length);
var addCommand = commands.FirstOrDefault(c => c.Title.Contains("Add bookmark"));
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
Assert.IsNotNull(addCommand);
}
}

View File

@@ -1,268 +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.
#nullable enable
using System;
using System.IO;
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class CommandLineHelperTests
{
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
private static string _tempTestDir;
private static string _tempTestFile;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
[ClassInitialize]
public static void ClassSetup(TestContext context)
{
// Create temporary test directory and file
_tempTestDir = Path.Combine(Path.GetTempPath(), "CommandLineHelperTests_" + Guid.NewGuid().ToString());
Directory.CreateDirectory(_tempTestDir);
_tempTestFile = Path.Combine(_tempTestDir, "testfile.txt");
File.WriteAllText(_tempTestFile, "test");
}
[ClassCleanup]
public static void ClassCleanup()
{
// Clean up test directory
if (Directory.Exists(_tempTestDir))
{
Directory.Delete(_tempTestDir, true);
}
}
[TestMethod]
[DataRow("%TEMP%", false, true, DisplayName = "Expands TEMP environment variable")]
[DataRow("%USERPROFILE%", false, true, DisplayName = "Expands USERPROFILE environment variable")]
[DataRow("%SystemRoot%", false, true, DisplayName = "Expands SystemRoot environment variable")]
public void Expand_WithEnvironmentVariables_ExpandsCorrectly(string input, bool expandShell, bool shouldExist)
{
// Act
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
// Assert
Assert.AreEqual(shouldExist, result, $"Expected result {shouldExist} for input '{input}'");
if (shouldExist)
{
Assert.IsFalse(full.Contains('%'), "Output should not contain % symbols after expansion");
Assert.IsTrue(Path.Exists(full), $"Expanded path '{full}' should exist");
}
}
[TestMethod]
[DataRow("shell:Downloads", true, DisplayName = "Expands shell:Downloads when expandShell is true")]
[DataRow("shell:Desktop", true, DisplayName = "Expands shell:Desktop when expandShell is true")]
[DataRow("shell:Documents", true, DisplayName = "Expands shell:Documents when expandShell is true")]
public void Expand_WithShellPaths_ExpandsWhenFlagIsTrue(string input, bool expandShell)
{
// Act
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
// Assert
if (result)
{
Assert.IsFalse(full.StartsWith("shell:", StringComparison.OrdinalIgnoreCase), "Shell prefix should be resolved");
Assert.IsTrue(Path.Exists(full), $"Expanded shell path '{full}' should exist");
}
// Note: Result may be false if ShellNames.TryGetFileSystemPath fails
}
[TestMethod]
[DataRow("shell:Personal", false, DisplayName = "Does not expand shell: when expandShell is false")]
public void Expand_WithShellPaths_DoesNotExpandWhenFlagIsFalse(string input, bool expandShell)
{
// Act
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
// Assert - shell: paths won't exist as literal paths
Assert.IsFalse(result, "Should return false for unexpanded shell path");
Assert.AreEqual(input, full, "Output should match input when not expanding shell paths");
}
[TestMethod]
[DataRow("shell:Personal\\subfolder", true, "\\subfolder", DisplayName = "Expands shell path with subfolder")]
[DataRow("shell:Desktop\\test.txt", true, "\\test.txt", DisplayName = "Expands shell path with file")]
public void Expand_WithShellPathsAndSubpaths_CombinesCorrectly(string input, bool expandShell, string expectedEnding)
{
// Act
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
// Note: Result depends on whether the combined path exists
if (result)
{
Assert.IsFalse(full.StartsWith("shell:", StringComparison.OrdinalIgnoreCase), "Shell prefix should be resolved");
Assert.IsTrue(full.EndsWith(expectedEnding, StringComparison.OrdinalIgnoreCase), "Output should end with the subpath");
}
}
[TestMethod]
public void Expand_WithExistingDirectory_ReturnsFullPath()
{
// Arrange
var input = _tempTestDir;
// Act
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, false, out var full);
// Assert
Assert.IsTrue(result, "Should return true for existing directory");
Assert.AreEqual(Path.GetFullPath(_tempTestDir), full, "Should return full path");
}
[TestMethod]
public void Expand_WithExistingFile_ReturnsFullPath()
{
// Arrange
var input = _tempTestFile;
// Act
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, false, out var full);
// Assert
Assert.IsTrue(result, "Should return true for existing file");
Assert.AreEqual(Path.GetFullPath(_tempTestFile), full, "Should return full path");
}
[TestMethod]
[DataRow("C:\\NonExistent\\Path\\That\\Does\\Not\\Exist", false, "C:\\NonExistent\\Path\\That\\Does\\Not\\Exist", DisplayName = "Nonexistent absolute path")]
[DataRow("NonExistentFile.txt", false, "NonExistentFile.txt", DisplayName = "Nonexistent relative path")]
public void Expand_WithNonExistentPath_ReturnsFalse(string input, bool expandShell, string expectedFull)
{
// Act
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
// Assert
Assert.IsFalse(result, "Should return false for nonexistent path");
Assert.AreEqual(expectedFull, full, "Output should be empty string");
}
[TestMethod]
[DataRow("", false, DisplayName = "Empty string")]
[DataRow(" ", false, DisplayName = "Whitespace only")]
public void Expand_WithEmptyOrWhitespace_ReturnsFalse(string input, bool expandShell)
{
// Act
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
// Assert
Assert.IsFalse(result, "Should return false for empty/whitespace input");
}
[TestMethod]
[DataRow("%TEMP%\\testsubdir", false, DisplayName = "Env var with subdirectory")]
[DataRow("%USERPROFILE%\\Desktop", false, DisplayName = "USERPROFILE with Desktop")]
public void Expand_WithEnvironmentVariableAndSubpath_ExpandsCorrectly(string input, bool expandShell)
{
// Act
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
// Result depends on whether the path exists
if (result)
{
Assert.IsFalse(full.Contains('%'), "Should expand environment variables");
Assert.IsTrue(Path.Exists(full), "Expanded path should exist");
}
}
[TestMethod]
public void Expand_WithRelativePath_ConvertsToAbsoluteWhenExists()
{
// Arrange
var relativePath = Path.GetRelativePath(Environment.CurrentDirectory, _tempTestDir);
// Act
var result = CommandLineHelper.ExpandPathToPhysicalFile(relativePath, false, out var full);
// Assert
if (result)
{
Assert.IsTrue(Path.IsPathRooted(full), "Output should be absolute path");
Assert.IsTrue(Path.Exists(full), "Expanded path should exist");
}
}
[TestMethod]
[DataRow("InvalidShell:Path", true, DisplayName = "Invalid shell path format")]
public void Expand_WithInvalidShellPath_ReturnsFalse(string input, bool expandShell)
{
// Act
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
// Assert
// If ShellNames.TryGetFileSystemPath returns false, method returns false
Assert.IsFalse(result || Path.Exists(full), "Should return false or path should not exist");
}
[DataTestMethod]
// basic
[DataRow("cmd ping", "cmd", "ping")]
[DataRow("cmd ping pong", "cmd", "ping pong")]
[DataRow("cmd \"ping pong\"", "cmd", "\"ping pong\"")]
// no tail / trailing whitespace after head
[DataRow("cmd", "cmd", "")]
[DataRow("cmd ", "cmd", "")]
// spacing & tabs between args should be preserved in tail
[DataRow("cmd ping pong", "cmd", "ping pong")]
[DataRow("cmd\tping\tpong", "cmd", "ping\tpong")]
// leading whitespace before head
[DataRow(" cmd ping", "", "cmd ping")]
[DataRow("\t cmd ping", "", "cmd ping")]
// quoted tail variants
[DataRow("cmd \"\"", "cmd", "\"\"")]
[DataRow("cmd \"a \\\"quoted\\\" arg\" b", "cmd", "\"a \\\"quoted\\\" arg\" b")]
// quoted head (spaces in path)
[DataRow(@"""C:\Program Files\nodejs\node.exe"" -v", @"C:\Program Files\nodejs\node.exe", "-v")]
[DataRow(@"""C:\Program Files\Git\bin\bash.exe""", @"C:\Program Files\Git\bin\bash.exe", "")]
[DataRow(@" ""C:\Program Files\Git\bin\bash.exe"" -lc ""hi""", @"", @"""C:\Program Files\Git\bin\bash.exe"" -lc ""hi""")]
[DataRow(@"""C:\Program Files (x86)\MSBuild\Current\Bin\MSBuild.exe"" Test.sln", @"C:\Program Files (x86)\MSBuild\Current\Bin\MSBuild.exe", "Test.sln")]
// quoted simple head (still should strip quotes for head)
[DataRow(@"""cmd"" ping", "cmd", "ping")]
// common CLI shapes
[DataRow("git --version", "git", "--version")]
[DataRow("dotnet build -c Release", "dotnet", "build -c Release")]
// UNC paths
[DataRow("\"\\\\server\\share\\\" with args", "\\\\server\\share\\", "with args")]
public void SplitHeadAndArgs(string input, string expectedHead, string expectedTail)
{
// Act
var result = CommandLineHelper.SplitHeadAndArgs(input);
// Assert
// If ShellNames.TryGetFileSystemPath returns false, method returns false
Assert.AreEqual(expectedHead, result.Head);
Assert.AreEqual(expectedTail, result.Tail);
}
[DataTestMethod]
[DataRow(@"C:\program files\myapp\app.exe -param ""1"" -param 2", @"C:\program files\myapp\app.exe -param", @"""1"" -param 2")]
[DataRow(@"git commit -m test", "git commit -m test", "")]
[DataRow(@"""C:\Program Files\App\app.exe"" -v", "", @"""C:\Program Files\App\app.exe"" -v")]
[DataRow(@"tool a\\\""b c ""d e"" f", @"tool a\\\""b c", @"""d e"" f")] // escaped quote before first real one
[DataRow("C:\\Some\\\"Path\\file.txt", "C:\\Some\\\"Path\\file.txt", "")]
[DataRow(@" ""C:\p\app.exe"" -v", "", @"""C:\p\app.exe"" -v")] // first token is quoted
public void SplitLongestHeadBeforeQuotedArg_Tests(string input, string expectedHead, string expectedTail)
{
var (head, tail) = CommandLineHelper.SplitLongestHeadBeforeQuotedArg(input);
Assert.AreEqual(expectedHead, head);
Assert.AreEqual(expectedTail, tail);
}
}

View File

@@ -1,9 +1,6 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
internal sealed class MockBookmarkDataSource : IBookmarkDataSource

View File

@@ -1,35 +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 Microsoft.CmdPal.Ext.Bookmarks.Persistence;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
#pragma warning disable CS0067
internal sealed class MockBookmarkManager : IBookmarksManager
{
private readonly List<BookmarkData> _bookmarks;
public event Action<BookmarkData> BookmarkAdded;
public event Action<BookmarkData, BookmarkData> BookmarkUpdated;
public event Action<BookmarkData> BookmarkRemoved;
public IReadOnlyCollection<BookmarkData> Bookmarks => _bookmarks;
public BookmarkData Add(string name, string bookmark) => throw new NotImplementedException();
public bool Remove(Guid id) => throw new NotImplementedException();
public BookmarkData Update(Guid id, string name, string bookmark) => throw new NotImplementedException();
public MockBookmarkManager(params IEnumerable<BookmarkData> bookmarks)
{
_bookmarks = [.. bookmarks];
}
}

View File

@@ -1,108 +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 Microsoft.CmdPal.Ext.Bookmarks.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class PlaceholderInfoNameEqualityComparerTests
{
[TestMethod]
public void Equals_BothNull_ReturnsTrue()
{
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
var result = comparer.Equals(null, null);
Assert.IsTrue(result);
}
[TestMethod]
public void Equals_OneNull_ReturnsFalse()
{
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
var p = new PlaceholderInfo("name", 0);
Assert.IsFalse(comparer.Equals(p, null));
Assert.IsFalse(comparer.Equals(null, p));
}
[TestMethod]
public void Equals_SameNameDifferentIndex_ReturnsTrue()
{
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
var p1 = new PlaceholderInfo("name", 0);
var p2 = new PlaceholderInfo("name", 10);
Assert.IsTrue(comparer.Equals(p1, p2));
}
[TestMethod]
public void Equals_DifferentNameSameIndex_ReturnsFalse()
{
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
var p1 = new PlaceholderInfo("first", 3);
var p2 = new PlaceholderInfo("second", 3);
Assert.IsFalse(comparer.Equals(p1, p2));
}
[TestMethod]
public void Equals_CaseInsensitive_ReturnsTrue()
{
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
var p1 = new PlaceholderInfo("Name", 0);
var p2 = new PlaceholderInfo("name", 5);
Assert.IsTrue(comparer.Equals(p1, p2));
Assert.AreEqual(comparer.GetHashCode(p1), comparer.GetHashCode(p2));
}
[TestMethod]
public void GetHashCode_SameNameDifferentIndex_SameHash()
{
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
var p1 = new PlaceholderInfo("same", 1);
var p2 = new PlaceholderInfo("same", 99);
Assert.AreEqual(comparer.GetHashCode(p1), comparer.GetHashCode(p2));
}
[TestMethod]
public void GetHashCode_Null_ThrowsArgumentNullException()
{
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
Assert.ThrowsException<ArgumentNullException>(() => comparer.GetHashCode(null!));
}
[TestMethod]
public void Instance_ReturnsSingleton()
{
var a = PlaceholderInfoNameEqualityComparer.Instance;
var b = PlaceholderInfoNameEqualityComparer.Instance;
Assert.IsNotNull(a);
Assert.AreSame(a, b);
}
[TestMethod]
public void HashSet_UsesNameEquality_IgnoresIndex()
{
var set = new HashSet<PlaceholderInfo>(PlaceholderInfoNameEqualityComparer.Instance)
{
new("dup", 0),
new("DUP", 10),
new("unique", 0),
};
Assert.AreEqual(2, set.Count);
Assert.IsTrue(set.Contains(new PlaceholderInfo("dup", 123)));
Assert.IsTrue(set.Contains(new PlaceholderInfo("UNIQUE", 999)));
Assert.IsFalse(set.Contains(new PlaceholderInfo("missing", 0)));
}
}

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.Linq;
using Microsoft.CmdPal.Ext.Bookmarks.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class PlaceholderParserTests
{
private IPlaceholderParser _parser;
[TestInitialize]
public void Setup()
{
_parser = new PlaceholderParser();
}
public static IEnumerable<object[]> ValidPlaceholderTestData =>
[
[
"Hello {name}!",
true,
"Hello ",
new[] { "name" },
new[] { 6 }
],
[
"User {user_name} has {count} items",
true,
"User ",
new[] { "user_name", "count" },
new[] { 5, 21 }
],
[
"Order {order-id} for {name} by {name}",
true,
"Order ",
new[] { "order-id", "name", "name" },
new[] { 6, 21, 31 }
],
[
"{start} and {end}",
true,
string.Empty,
new[] { "start", "end" },
new[] { 0, 12 }
],
[
"Number {123} and text {abc}",
true,
"Number ",
new[] { "123", "abc" },
new[] { 7, 22 }
]
];
public static IEnumerable<object[]> InvalidPlaceholderTestData =>
[
[string.Empty, false, string.Empty, Array.Empty<string>()],
["No placeholders here", false, "No placeholders here", Array.Empty<string>()],
["GUID: {550e8400-e29b-41d4-a716-446655440000}", false, "GUID: {550e8400-e29b-41d4-a716-446655440000}", Array.Empty<string>()],
["Invalid {user.name} placeholder", false, "Invalid {user.name} placeholder", Array.Empty<string>()],
["Empty {} placeholder", false, "Empty {} placeholder", Array.Empty<string>()],
["Unclosed {placeholder", false, "Unclosed {placeholder", Array.Empty<string>()],
["No opening brace placeholder}", false, "No opening brace placeholder}", Array.Empty<string>()],
["Invalid chars {user@domain}", false, "Invalid chars {user@domain}", Array.Empty<string>()],
["Spaces { name }", false, "Spaces { name }", Array.Empty<string>()]
];
[TestMethod]
[DynamicData(nameof(ValidPlaceholderTestData))]
public void ParsePlaceholders_ValidInput_ReturnsExpectedResults(
string input,
bool expectedResult,
string expectedHead,
string[] expectedPlaceholderNames,
int[] expectedIndexes)
{
// Act
var result = _parser.ParsePlaceholders(input, out var head, out var placeholders);
// Assert
Assert.AreEqual(expectedResult, result);
Assert.AreEqual(expectedHead, head);
Assert.AreEqual(expectedPlaceholderNames.Length, placeholders.Count);
var actualNames = placeholders.Select(p => p.Name).ToArray();
var actualIndexes = placeholders.Select(p => p.Index).ToArray();
// Validate names and indexes (allow duplicates, ignore order)
CollectionAssert.AreEquivalent(expectedPlaceholderNames, actualNames);
CollectionAssert.AreEquivalent(expectedIndexes, actualIndexes);
// Validate name-index pairing exists for each expected placeholder occurrence
for (var i = 0; i < expectedPlaceholderNames.Length; i++)
{
var expectedName = expectedPlaceholderNames[i];
var expectedIndex = expectedIndexes[i];
Assert.IsTrue(
placeholders.Any(p => p.Name == expectedName && p.Index == expectedIndex),
$"Expected placeholder '{{{expectedName}}}' at index {expectedIndex} was not found.");
}
}
[TestMethod]
[DynamicData(nameof(InvalidPlaceholderTestData))]
public void ParsePlaceholders_InvalidInput_ReturnsExpectedResults(
string input,
bool expectedResult,
string expectedHead,
string[] expectedPlaceholderNames)
{
// Act
var result = _parser.ParsePlaceholders(input, out var head, out var placeholders);
// Assert
Assert.AreEqual(expectedResult, result);
Assert.AreEqual(expectedHead, head);
Assert.AreEqual(expectedPlaceholderNames.Length, placeholders.Count);
var actualNames = placeholders.Select(p => p.Name).ToArray();
CollectionAssert.AreEquivalent(expectedPlaceholderNames, actualNames);
}
[TestMethod]
public void ParsePlaceholders_NullInput_ThrowsArgumentNullException()
{
Assert.ThrowsException<ArgumentNullException>(() => _parser.ParsePlaceholders(null!, out _, out _));
}
[TestMethod]
public void Placeholder_Equality_WorksCorrectly()
{
// Arrange
var placeholder1 = new PlaceholderInfo("name", 0);
var placeholder2 = new PlaceholderInfo("name", 0);
var placeholder3 = new PlaceholderInfo("other", 0);
var placeholder4 = new PlaceholderInfo("name", 1);
// Assert
Assert.AreEqual(placeholder1, placeholder2);
Assert.AreNotEqual(placeholder1, placeholder3);
Assert.AreEqual(placeholder1.GetHashCode(), placeholder2.GetHashCode());
Assert.AreNotEqual(placeholder1, placeholder4);
Assert.AreNotEqual(placeholder1.GetHashCode(), placeholder4.GetHashCode());
}
[TestMethod]
public void Placeholder_ToString_ReturnsName()
{
// Arrange
var placeholder = new PlaceholderInfo("userName", 0);
// Assert
Assert.AreEqual("userName", placeholder.ToString());
}
[TestMethod]
public void Placeholder_Constructor_ThrowsOnNull()
{
// Assert
Assert.ThrowsException<ArgumentNullException>(() => new PlaceholderInfo(null!, 0));
}
[TestMethod]
public void Placeholder_Constructor_ThrowsArgumentOutOfRange()
{
// Assert
Assert.ThrowsException<ArgumentOutOfRangeException>(() => new PlaceholderInfo("Name", -1));
}
}

View File

@@ -40,4 +40,16 @@ public class QueryTests : CommandPaletteUnitTestBase
Assert.IsNotNull(githubBookmark);
Assert.AreEqual("https://github.com", githubBookmark.Bookmark);
}
[TestMethod]
public void ValidateWebUrlDetection()
{
// Setup
var bookmarks = Settings.CreateDefaultBookmarks();
var microsoftBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "Microsoft");
// Assert
Assert.IsNotNull(microsoftBookmark);
Assert.IsTrue(microsoftBookmark.IsWebUrl());
}
}

View File

@@ -2,15 +2,13 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
public static class Settings
{
public static BookmarksData CreateDefaultBookmarks()
public static Bookmarks CreateDefaultBookmarks()
{
var bookmarks = new BookmarksData();
var bookmarks = new Bookmarks();
// Add some test bookmarks
bookmarks.Data.Add(new BookmarkData

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 Microsoft.CmdPal.Ext.Bookmarks.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class UriHelperTests
{
private static bool TryGetScheme(ReadOnlySpan<char> input, out string scheme, out string remainder)
{
return UriHelper.TryGetScheme(input, out scheme, out remainder);
}
[DataTestMethod]
[DataRow("http://example.com", "http", "//example.com")]
[DataRow("ftp:", "ftp", "")]
[DataRow("my-app:payload", "my-app", "payload")]
[DataRow("x-cmdpal://settings/", "x-cmdpal", "//settings/")]
[DataRow("custom+ext.-scheme:xyz", "custom+ext.-scheme", "xyz")]
[DataRow("MAILTO:foo@bar", "MAILTO", "foo@bar")]
[DataRow("a:b", "a", "b")]
public void TryGetScheme_ValidSchemes_ReturnsTrueAndSplits(string input, string expectedScheme, string expectedRemainder)
{
var ok = TryGetScheme(input.AsSpan(), out var scheme, out var remainder);
Assert.IsTrue(ok, "Expected valid scheme.");
Assert.AreEqual(expectedScheme, scheme);
Assert.AreEqual(expectedRemainder, remainder);
}
[TestMethod]
public void TryGetScheme_OnlySchemeAndColon_ReturnsEmptyRemainder()
{
var ok = TryGetScheme("http:".AsSpan(), out var scheme, out var remainder);
Assert.IsTrue(ok);
Assert.AreEqual("http", scheme);
Assert.AreEqual(string.Empty, remainder);
}
[DataTestMethod]
[DataRow("123:http")] // starts with digit
[DataRow(":nope")] // colon at start
[DataRow("noColon")] // no colon at all
[DataRow("bad_scheme:")] // underscore not allowed
[DataRow("bad*scheme:")] // asterisk not allowed
[DataRow(":")] // syntactically invalid literal just for completeness; won't compile, example only
public void TryGetScheme_InvalidInputs_ReturnsFalse(string input)
{
var ok = TryGetScheme(input.AsSpan(), out var scheme, out var remainder);
Assert.IsFalse(ok);
Assert.AreEqual(string.Empty, scheme);
Assert.AreEqual(string.Empty, remainder);
}
[TestMethod]
public void TryGetScheme_MultipleColons_SplitsOnFirst()
{
const string input = "shell:::{645FF040-5081-101B-9F08-00AA002F954E}";
var ok = TryGetScheme(input.AsSpan(), out var scheme, out var remainder);
Assert.IsTrue(ok);
Assert.AreEqual("shell", scheme);
Assert.AreEqual("::{645FF040-5081-101B-9F08-00AA002F954E}", remainder);
}
[TestMethod]
public void TryGetScheme_MinimumLength_OneLetterAndColon()
{
var ok = TryGetScheme("a:".AsSpan(), out var scheme, out var remainder);
Assert.IsTrue(ok);
Assert.AreEqual("a", scheme);
Assert.AreEqual(string.Empty, remainder);
}
[TestMethod]
public void TryGetScheme_TooShort_ReturnsFalse()
{
Assert.IsFalse(TryGetScheme("a".AsSpan(), out _, out _), "No colon.");
Assert.IsFalse(TryGetScheme(":".AsSpan(), out _, out _), "Colon at start; no scheme.");
}
[DataTestMethod]
[DataRow("HTTP://x", "HTTP", "//x")]
[DataRow("hTtP:rest", "hTtP", "rest")]
public void TryGetScheme_CaseIsPreserved(string input, string expectedScheme, string expectedRemainder)
{
var ok = TryGetScheme(input.AsSpan(), out var scheme, out var remainder);
Assert.IsTrue(ok);
Assert.AreEqual(expectedScheme, scheme);
Assert.AreEqual(expectedRemainder, remainder);
}
[TestMethod]
public void TryGetScheme_WhitespaceInsideScheme_Fails()
{
Assert.IsFalse(TryGetScheme("ht tp:rest".AsSpan(), out _, out _));
}
[TestMethod]
public void TryGetScheme_PlusMinusDot_AllowedInMiddleOnly()
{
Assert.IsTrue(TryGetScheme("a+b.c-d:rest".AsSpan(), out var s1, out var r1));
Assert.AreEqual("a+b.c-d", s1);
Assert.AreEqual("rest", r1);
// The first character must be a letter; plus is not allowed as first char
Assert.IsFalse(TryGetScheme("+abc:rest".AsSpan(), out _, out _));
Assert.IsFalse(TryGetScheme(".abc:rest".AsSpan(), out _, out _));
Assert.IsFalse(TryGetScheme("-abc:rest".AsSpan(), out _, out _));
}
}

View File

@@ -2,14 +2,13 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Pages;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.CommandPalette.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@@ -84,7 +83,7 @@ public class QueryTests : CommandPaletteUnitTestBase
var settings = Settings.CreateDefaultSettings();
var mockHistory = CreateMockHistoryService();
var pages = new ShellListPage(settings, mockHistory.Object, telemetryService: null);
var pages = new ShellListPage(settings, mockHistory.Object);
await UpdatePageAndWaitForItems(pages, () =>
{
@@ -116,7 +115,7 @@ public class QueryTests : CommandPaletteUnitTestBase
var settings = Settings.CreateDefaultSettings();
var mockHistoryService = CreateMockHistoryServiceWithCommonCommands();
var pages = new ShellListPage(settings, mockHistoryService.Object, telemetryService: null);
var pages = new ShellListPage(settings, mockHistoryService.Object);
await UpdatePageAndWaitForItems(pages, () =>
{
@@ -142,7 +141,7 @@ public class QueryTests : CommandPaletteUnitTestBase
var settings = Settings.CreateDefaultSettings();
var mockHistoryService = CreateMockHistoryServiceWithCommonCommands();
var pages = new ShellListPage(settings, mockHistoryService.Object, telemetryService: null);
var pages = new ShellListPage(settings, mockHistoryService.Object);
await UpdatePageAndWaitForItems(pages, () =>
{
@@ -155,131 +154,4 @@ public class QueryTests : CommandPaletteUnitTestBase
// Should find at least the ping command from history
Assert.IsTrue(commandList.Length > 1);
}
[TestMethod]
public async Task TestCacheBackToSameDirectory()
{
// Setup
var settings = Settings.CreateDefaultSettings();
var mockHistoryService = CreateMockHistoryService();
var page = new ShellListPage(settings, mockHistoryService.Object, telemetryService: null);
// Load up everything in c:\, for the sake of comparing:
var filesInC = Directory.EnumerateFileSystemEntries("C:\\");
await UpdatePageAndWaitForItems(page, () => { page.SearchText = "c:\\"; });
var commandList = page.GetItems();
// Should find only items for what's in c:\
Assert.IsTrue(commandList.Length == filesInC.Count());
await UpdatePageAndWaitForItems(page, () => { page.SearchText = "c:\\Win"; });
await UpdatePageAndWaitForItems(page, () => { page.SearchText = "c:\\Windows"; });
await UpdatePageAndWaitForItems(page, () => { page.SearchText = "c:\\"; });
commandList = page.GetItems();
// Should still find everything
Assert.IsTrue(commandList.Length == filesInC.Count());
await TypeStringIntoPage(page, "c:\\Windows\\Pro");
await BackspaceSearchText(page, "c:\\Windows\\Pro", 3); // 3 characters for c:\
commandList = page.GetItems();
// Should still find everything
Assert.IsTrue(commandList.Length == filesInC.Count());
}
private async Task TypeStringIntoPage(IDynamicListPage page, string searchText)
{
// type the string one character at a time
for (var i = 0; i < searchText.Length; i++)
{
var substr = searchText[..i];
await UpdatePageAndWaitForItems(page, () => { page.SearchText = substr; });
}
}
private async Task BackspaceSearchText(IDynamicListPage page, string originalSearchText, int finalStringLength)
{
var originalLength = originalSearchText.Length;
for (var i = originalLength; i >= finalStringLength; i--)
{
var substr = originalSearchText[..i];
await UpdatePageAndWaitForItems(page, () => { page.SearchText = substr; });
}
}
[TestMethod]
public async Task TestCacheSameDirectorySlashy()
{
// Setup
var settings = Settings.CreateDefaultSettings();
var mockHistoryService = CreateMockHistoryService();
var page = new ShellListPage(settings, mockHistoryService.Object, telemetryService: null);
// Load up everything in c:\, for the sake of comparing:
var filesInC = Directory.EnumerateFileSystemEntries("C:\\");
var filesInWindows = Directory.EnumerateFileSystemEntries("C:\\Windows");
await UpdatePageAndWaitForItems(page, () => { page.SearchText = "c:\\"; });
var commandList = page.GetItems();
Assert.IsTrue(commandList.Length == filesInC.Count());
// First navigate to c:\Windows. This should match everything that matches "windows" inside of C:\
await UpdatePageAndWaitForItems(page, () => { page.SearchText = "c:\\Windows"; });
var cWindowsCommandsPre = page.GetItems();
// Then go into c:\windows\. This will only have the results in c:\windows\
await UpdatePageAndWaitForItems(page, () => { page.SearchText = "c:\\Windows\\"; });
var windowsCommands = page.GetItems();
Assert.IsTrue(windowsCommands.Length != cWindowsCommandsPre.Length);
// now go back to c:\windows. This should match the results from the last time we entered this string
await UpdatePageAndWaitForItems(page, () => { page.SearchText = "c:\\Windows"; });
var cWindowsCommandsPost = page.GetItems();
Assert.IsTrue(cWindowsCommandsPre.Length == cWindowsCommandsPost.Length);
}
[TestMethod]
public async Task TestPathWithSpaces()
{
// Setup
var settings = Settings.CreateDefaultSettings();
var mockHistoryService = CreateMockHistoryService();
var page = new ShellListPage(settings, mockHistoryService.Object, telemetryService: null);
// Load up everything in c:\, for the sake of comparing:
var filesInC = Directory.EnumerateFileSystemEntries("C:\\");
var filesInProgramFiles = Directory.EnumerateFileSystemEntries("C:\\Program Files");
await UpdatePageAndWaitForItems(page, () => { page.SearchText = "c:\\Program Files\\"; });
var commandList = page.GetItems();
Assert.IsTrue(commandList.Length == filesInProgramFiles.Count());
}
[TestMethod]
public async Task TestNoWrapSuggestionsWithSpaces()
{
// Setup
var settings = Settings.CreateDefaultSettings();
var mockHistoryService = CreateMockHistoryService();
var page = new ShellListPage(settings, mockHistoryService.Object, telemetryService: null);
await UpdatePageAndWaitForItems(page, () => { page.SearchText = "c:\\Program Files\\"; });
var commandList = page.GetItems();
foreach (var item in commandList)
{
Assert.IsTrue(!string.IsNullOrEmpty(item.TextToSuggest));
Assert.IsFalse(item.TextToSuggest.StartsWith('"'));
}
}
}

View File

@@ -16,7 +16,7 @@ public class ShellCommandProviderTests
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object, telemetryService: null);
var provider = new ShellCommandsProvider(mockHistoryService.Object);
// Assert
Assert.IsNotNull(provider.DisplayName);
@@ -28,7 +28,7 @@ public class ShellCommandProviderTests
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object, telemetryService: null);
var provider = new ShellCommandsProvider(mockHistoryService.Object);
// Assert
Assert.IsNotNull(provider.Icon);
@@ -39,7 +39,7 @@ public class ShellCommandProviderTests
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object, telemetryService: null);
var provider = new ShellCommandsProvider(mockHistoryService.Object);
// Act
var commands = provider.TopLevelCommands();

View File

@@ -7,7 +7,6 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.UnitTestBase;
@@ -33,14 +32,9 @@ public class CommandPaletteUnitTestBase
// and wait for the event to be raised.
var tcs = new TaskCompletionSource<object>();
TypedEventHandler<object, IItemsChangedEventArgs> handleItemsChanged = (object s, IItemsChangedEventArgs e) =>
{
tcs.TrySetResult(e);
};
page.ItemsChanged += (sender, args) => tcs.SetResult(null);
page.ItemsChanged += handleItemsChanged;
modification();
await tcs.Task;
}
}

View File

@@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.UI.ViewModels.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
<PackageReference Include="WyHash" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,444 +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.Linq;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.CmdPal.UI.ViewModels.MainPage;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Foundation;
using WyHash;
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
[TestClass]
public partial class RecentCommandsTests : CommandPaletteUnitTestBase
{
private static RecentCommandsManager CreateHistory(IList<string>? commandIds = null)
{
var history = new RecentCommandsManager();
if (commandIds != null)
{
foreach (var item in commandIds)
{
history.AddHistoryItem(item);
}
}
return history;
}
private static RecentCommandsManager CreateBasicHistoryService()
{
var commonCommands = new List<string>
{
"com.microsoft.cmdpal.shell",
"com.microsoft.cmdpal.windowwalker",
"Visual Studio 2022 Preview_6533433915015224980",
"com.microsoft.cmdpal.reload",
"com.microsoft.cmdpal.shell",
};
return CreateHistory(commonCommands);
}
[TestMethod]
public void ValidateHistoryFunctionality()
{
// Setup
var history = CreateHistory();
// Act
history.AddHistoryItem("com.microsoft.cmdpal.shell");
// Assert
Assert.IsTrue(history.GetCommandHistoryWeight("com.microsoft.cmdpal.shell") > 0);
}
[TestMethod]
public void ValidateHistoryWeighting()
{
// Setup
var history = CreateBasicHistoryService();
// Act
var shellWeight = history.GetCommandHistoryWeight("com.microsoft.cmdpal.shell");
var windowWalkerWeight = history.GetCommandHistoryWeight("com.microsoft.cmdpal.windowwalker");
var vsWeight = history.GetCommandHistoryWeight("Visual Studio 2022 Preview_6533433915015224980");
var reloadWeight = history.GetCommandHistoryWeight("com.microsoft.cmdpal.reload");
var nonExistentWeight = history.GetCommandHistoryWeight("non.existent.command");
// Assert
Assert.IsTrue(shellWeight > windowWalkerWeight, "Shell should be weighted higher than Window Walker, more uses");
Assert.IsTrue(vsWeight > windowWalkerWeight, "Visual Studio should be weighted higher than Window Walker, because recency");
Assert.AreEqual(reloadWeight, vsWeight, "both reload and VS were used in the last three commands, same weight");
Assert.IsTrue(shellWeight > vsWeight, "VS and run were both used in the last 3, but shell has 2 more frequency");
Assert.AreEqual(0, nonExistentWeight, "Nonexistent command should have zero weight");
}
private sealed partial record ListItemMock(
string Title,
string? Subtitle = "",
string? GivenId = "",
string? ProviderId = "") : IListItem
{
public string Id => string.IsNullOrEmpty(GivenId) ? GenerateId() : GivenId;
public IDetails Details => throw new System.NotImplementedException();
public string Section => throw new System.NotImplementedException();
public ITag[] Tags => throw new System.NotImplementedException();
public string TextToSuggest => throw new System.NotImplementedException();
public ICommand Command => new NoOpCommand() { Id = Id };
public IIconInfo Icon => throw new System.NotImplementedException();
public IContextItem[] MoreCommands => throw new System.NotImplementedException();
#pragma warning disable CS0067
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged;
#pragma warning restore CS0067
private string GenerateId()
{
// Use WyHash64 to generate stable ID hashes.
// manually seeding with 0, so that the hash is stable across launches
var result = WyHash64.ComputeHash64(ProviderId + Title + Subtitle, seed: 0);
return $"{ProviderId}{result}";
}
}
private static RecentCommandsManager CreateHistory(IList<ListItemMock> items)
{
var history = new RecentCommandsManager();
foreach (var item in items)
{
history.AddHistoryItem(item.Id);
}
return history;
}
[TestMethod]
public void ValidateMocksWork()
{
// Setup
var items = new List<ListItemMock>
{
new("Command A", "Subtitle A", "idA", "providerA"),
new("Command B", "Subtitle B", GivenId: "idB"),
new("Command C", "Subtitle C", ProviderId: "providerC"),
new("Command A", "Subtitle A", "idA", "providerA"), // Duplicate to test incrementing uses
};
// Act
var history = CreateHistory(items);
// Assert
foreach (var item in items)
{
var weight = history.GetCommandHistoryWeight(item.Id);
Assert.IsTrue(weight > 0, $"Item {item.Title} should have a weight greater than zero.");
}
// Check that the duplicate item has a higher weight due to increased uses
var weightA = history.GetCommandHistoryWeight("idA");
var weightB = history.GetCommandHistoryWeight("idB");
var weightC = history.GetCommandHistoryWeight(items[2].Id); // providerC generated ID
Assert.IsTrue(weightA > weightB, "Item A should have a higher weight than Item B due to more uses.");
Assert.IsTrue(weightA > weightC, "Item A should have a higher weight than Item C due to more uses.");
Assert.AreEqual(weightC, weightB, "Item C and Item B were used in the last 3 commands");
}
[TestMethod]
public void ValidateHistoryBuckets()
{
// Setup
// (these will be checked in reverse order, so that A is the most recent)
var items = new List<ListItemMock>
{
new("Command A", "Subtitle A", GivenId: "idA"), // #0 -> bucket 0
new("Command B", "Subtitle B", GivenId: "idB"), // #1 -> bucket 0
new("Command C", "Subtitle C", GivenId: "idC"), // #2 -> bucket 0
new("Command D", "Subtitle D", GivenId: "idD"), // #3 -> bucket 1
new("Command E", "Subtitle E", GivenId: "idE"), // #4 -> bucket 1
new("Command F", "Subtitle F", GivenId: "idF"), // #5 -> bucket 1
new("Command G", "Subtitle G", GivenId: "idG"), // #6 -> bucket 1
new("Command H", "Subtitle H", GivenId: "idH"), // #7 -> bucket 1
new("Command I", "Subtitle I", GivenId: "idI"), // #8 -> bucket 1
new("Command J", "Subtitle J", GivenId: "idJ"), // #9 -> bucket 1
new("Command K", "Subtitle K", GivenId: "idK"), // #10 -> bucket 1
new("Command L", "Subtitle L", GivenId: "idL"), // #11 -> bucket 2
new("Command M", "Subtitle M", GivenId: "idM"), // #12 -> bucket 2
new("Command N", "Subtitle N", GivenId: "idN"), // #13 -> bucket 2
new("Command O", "Subtitle O", GivenId: "idO"), // #14 -> bucket 2
};
for (var i = items.Count; i <= 50; i++)
{
items.Add(new ListItemMock($"Command #{i}", GivenId: $"id{i}"));
}
// Act
var history = CreateHistory(items.Reverse<ListItemMock>().ToList());
// Assert
// First three items should be in the top bucket
var weightA = history.GetCommandHistoryWeight("idA");
var weightB = history.GetCommandHistoryWeight("idB");
var weightC = history.GetCommandHistoryWeight("idC");
Assert.AreEqual(weightA, weightB, "Items A and B were used in the last 3 commands");
Assert.AreEqual(weightB, weightC, "Items B and C were used in the last 3 commands");
// Next eight items (3-10 inclusive) should be in the second bucket
var weightD = history.GetCommandHistoryWeight("idD");
var weightE = history.GetCommandHistoryWeight("idE");
var weightF = history.GetCommandHistoryWeight("idF");
var weightG = history.GetCommandHistoryWeight("idG");
var weightH = history.GetCommandHistoryWeight("idH");
var weightI = history.GetCommandHistoryWeight("idI");
var weightJ = history.GetCommandHistoryWeight("idJ");
var weightK = history.GetCommandHistoryWeight("idK");
Assert.AreEqual(weightD, weightE, "Items D and E were used in the last 10 commands");
Assert.AreEqual(weightE, weightF, "Items E and F were used in the last 10 commands");
Assert.AreEqual(weightF, weightG, "Items F and G were used in the last 10 commands");
Assert.AreEqual(weightG, weightH, "Items G and H were used in the last 10 commands");
Assert.AreEqual(weightH, weightI, "Items H and I were used in the last 10 commands");
Assert.AreEqual(weightI, weightJ, "Items I and J were used in the last 10 commands");
Assert.AreEqual(weightJ, weightK, "Items J and K were used in the last 10 commands");
// Items up to the 15th should be in the third bucket
var weightL = history.GetCommandHistoryWeight("idL");
var weightM = history.GetCommandHistoryWeight("idM");
var weightN = history.GetCommandHistoryWeight("idN");
var weightO = history.GetCommandHistoryWeight("idO");
var weight15 = history.GetCommandHistoryWeight("id15");
Assert.AreEqual(weightL, weightM, "Items L and M were used in the last 15 commands");
Assert.AreEqual(weightM, weightN, "Items M and N were used in the last 15 commands");
Assert.AreEqual(weightN, weightO, "Items N and O were used in the last 15 commands");
Assert.AreEqual(weightO, weight15, "Items O and 15 were used in the last 15 commands");
// Items after that should be in the lowest buckets
var weight0 = history.GetCommandHistoryWeight(items[0].Id);
var weight3 = history.GetCommandHistoryWeight(items[3].Id);
var weight11 = history.GetCommandHistoryWeight(items[11].Id);
var weight16 = history.GetCommandHistoryWeight("id16");
var weight20 = history.GetCommandHistoryWeight("id20");
var weight30 = history.GetCommandHistoryWeight("id30");
var weight40 = history.GetCommandHistoryWeight("id40");
var weight49 = history.GetCommandHistoryWeight("id49");
Assert.IsTrue(weight0 > weight3);
Assert.IsTrue(weight3 > weight11);
Assert.IsTrue(weight11 > weight16);
Assert.AreEqual(weight16, weight20);
Assert.AreEqual(weight20, weight30);
Assert.IsTrue(weight30 > weight40);
Assert.AreEqual(weight40, weight49);
// The 50th item has fallen out of the list now
var weight50 = history.GetCommandHistoryWeight("id50");
Assert.AreEqual(0, weight50, "Item 50 should have fallen out of the history list");
}
[TestMethod]
public void ValidateSimpleScoring()
{
// Setup
var items = new List<ListItemMock>
{
new("Command A", "Subtitle A", GivenId: "idA"), // #0 -> bucket 0
new("Command B", "Subtitle B", GivenId: "idB"), // #1 -> bucket 0
new("Command C", "Subtitle C", GivenId: "idC"), // #2 -> bucket 0
};
var history = CreateHistory(items.Reverse<ListItemMock>().ToList());
var scoreA = MainListPage.ScoreTopLevelItem("C", items[0], history);
var scoreB = MainListPage.ScoreTopLevelItem("C", items[1], history);
var scoreC = MainListPage.ScoreTopLevelItem("C", items[2], history);
// Assert
// All of these equally match the query, and they're all in the same bucket,
// so they should all have the same score.
Assert.AreEqual(scoreA, scoreB, "Items A and B should have the same score");
Assert.AreEqual(scoreB, scoreC, "Items B and C should have the same score");
}
private static List<ListItemMock> CreateMockHistoryItems()
{
var items = new List<ListItemMock>
{
new("Visual Studio 2022"), // #0 -> bucket 0
new("Visual Studio Code"), // #1 -> bucket 0
new("Explore Mastodon", GivenId: "social.mastodon.explore"), // #2 -> bucket 0
new("Run commands", Subtitle: "Executes commands (e.g. ping, cmd)", GivenId: "com.microsoft.cmdpal.run"), // #3 -> bucket 1
new("Windows Settings"), // #4 -> bucket 1
new("Command Prompt"), // #5 -> bucket 1
new("Terminal Canary"), // #6 -> bucket 1
};
return items;
}
private static RecentCommandsManager CreateMockHistoryService(List<ListItemMock>? items = null)
{
var history = CreateHistory((items ?? CreateMockHistoryItems()).Reverse<ListItemMock>().ToList());
return history;
}
private sealed record ScoredItem(ListItemMock Item, int Score)
{
public string Title => Item.Title;
public override string ToString() => $"[{Score}]{Title}";
}
private static IEnumerable<ScoredItem> TieScoresToMatches(List<ListItemMock> items, List<int> scores)
{
if (items.Count != scores.Count)
{
throw new ArgumentException("Items and scores must have the same number of elements");
}
for (var i = 0; i < items.Count; i++)
{
yield return new ScoredItem(items[i], scores[i]);
}
}
private static IEnumerable<ScoredItem> GetMatches(IEnumerable<ScoredItem> scoredItems)
{
var matches = scoredItems
.Where(x => x.Score > 0)
.OrderByDescending(x => x.Score)
.ToList();
return matches;
}
private static IEnumerable<ScoredItem> GetMatches(List<ListItemMock> items, List<int> scores)
{
return GetMatches(TieScoresToMatches(items, scores));
}
[TestMethod]
public void ValidateScoredWeightingSimple()
{
var items = CreateMockHistoryItems();
var emptyHistory = CreateMockHistoryService(new());
var history = CreateMockHistoryService(items);
var unweightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, emptyHistory)).ToList();
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, history)).ToList();
Assert.AreEqual(unweightedScores.Count, weightedScores.Count, "Both score lists should have the same number of items");
for (var i = 0; i < unweightedScores.Count; i++)
{
var unweighted = unweightedScores[i];
var weighted = weightedScores[i];
var item = items[i];
if (item.Title.Contains('C', System.StringComparison.CurrentCultureIgnoreCase))
{
Assert.IsTrue(unweighted >= 0, $"Item {item.Title} didn't match the query, so should have a weighted score of zero");
Assert.IsTrue(weighted > unweighted, $"Item {item.Title} should have a higher weighted ({weighted}) score than unweighted ({unweighted})");
}
else
{
Assert.AreEqual(unweighted, 0, $"Item {item.Title} didn't match the query, so should have a weighted score of zero");
Assert.AreEqual(unweighted, weighted);
}
}
var unweightedMatches = GetMatches(items, unweightedScores).ToList();
Assert.AreEqual(4, unweightedMatches.Count);
Assert.AreEqual("Command Prompt", unweightedMatches[0].Title, "Command Prompt should be the top match");
Assert.AreEqual("Visual Studio Code", unweightedMatches[1].Title, "Visual Studio Code should be the second match");
Assert.AreEqual("Terminal Canary", unweightedMatches[2].Title);
Assert.AreEqual("Run commands", unweightedMatches[3].Title);
// Even after weighting for 1 use, Command Prompt should still be the top match.
var weightedMatches = GetMatches(items, weightedScores).ToList();
Assert.AreEqual(4, weightedMatches.Count);
Assert.AreEqual("Command Prompt", weightedMatches[0].Title);
Assert.AreEqual("Visual Studio Code", weightedMatches[1].Title);
Assert.AreEqual("Terminal Canary", weightedMatches[2].Title);
Assert.AreEqual("Run commands", weightedMatches[3].Title);
}
[TestMethod]
public void ValidateTitlesAreMoreImportantThanHistory()
{
var items = CreateMockHistoryItems();
var emptyHistory = CreateMockHistoryService(new());
var history = CreateMockHistoryService(items);
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("te", item, history)).ToList();
var weightedMatches = GetMatches(items, weightedScores).ToList();
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
// Terminal is in bucket 1, VS Code is in bucket 0, but Terminal matches
// the title better
Assert.AreEqual("Terminal Canary", weightedMatches[0].Title, "Terminal should be the top match, title match");
Assert.AreEqual("Visual Studio Code", weightedMatches[1].Title, "VsCode does fuzzy match, but is less relevant than Terminal");
Assert.AreEqual("Run commands", weightedMatches[2].Title, "run only matches on the subtitle");
}
[TestMethod]
public void ValidateTitlesAreMoreImportantThanUsage()
{
var items = CreateMockHistoryItems();
var emptyHistory = CreateMockHistoryService(new());
var history = CreateMockHistoryService(items);
// Add extra uses of VS Code to try and push it above Terminal
for (var i = 0; i < 10; i++)
{
history.AddHistoryItem(items[1].Id);
}
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("te", item, history)).ToList();
var weightedMatches = GetMatches(items, weightedScores).ToList();
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
// Terminal is in bucket 1, VS Code is in bucket 0, but Terminal matches
// the title better
Assert.AreEqual("Terminal Canary", weightedMatches[0].Title, "Terminal should be the top match, title match");
Assert.AreEqual("Visual Studio Code", weightedMatches[1].Title, "VsCode does fuzzy match, but is less relevant than Terminal");
Assert.AreEqual("Run commands", weightedMatches[2].Title, "run only matches on the subtitle");
}
[TestMethod]
public void ValidateUsageEventuallyHelps()
{
var items = CreateMockHistoryItems();
var emptyHistory = CreateMockHistoryService(new());
var history = CreateMockHistoryService(items);
// We're gonna run this test and keep adding more uses of VS Code till
// it breaks past Command Prompt
var vsCodeId = items[1].Id;
for (var i = 0; i < 10; i++)
{
history.AddHistoryItem(vsCodeId);
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, history)).ToList();
var weightedMatches = GetMatches(items, weightedScores).ToList();
Assert.AreEqual(4, weightedMatches.Count);
var expectedCmdIndex = i < 5 ? 0 : 1;
var expectedCodeIndex = i < 5 ? 1 : 0;
Assert.AreEqual("Command Prompt", weightedMatches[expectedCmdIndex].Title);
Assert.AreEqual("Visual Studio Code", weightedMatches[expectedCodeIndex].Title);
}
}
}

View File

@@ -16,19 +16,24 @@ using WyHash;
namespace Microsoft.CmdPal.Ext.Apps;
internal sealed partial class AppCommand : InvokableCommand
public sealed partial class AppCommand : InvokableCommand
{
private readonly AppItem _app;
public AppCommand(AppItem app)
{
_app = app;
Name = Resources.run_command_action!;
Name = Resources.run_command_action;
Id = GenerateId();
Icon = Icons.GenericAppIcon;
if (!string.IsNullOrEmpty(app.IcoPath))
{
Icon = new(app.IcoPath);
}
}
private static async Task StartApp(string aumid)
internal static async Task StartApp(string aumid)
{
await Task.Run(() =>
{
@@ -53,7 +58,7 @@ internal sealed partial class AppCommand : InvokableCommand
}).ConfigureAwait(false);
}
private static async Task StartExe(string path)
internal static async Task StartExe(string path)
{
await Task.Run(() =>
{
@@ -68,7 +73,7 @@ internal sealed partial class AppCommand : InvokableCommand
});
}
private async Task Launch()
internal async Task Launch()
{
if (_app.IsPackaged)
{

View File

@@ -5,51 +5,34 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
public sealed partial class AppListItem : ListItem
internal sealed partial class AppListItem : ListItem
{
private readonly AppItem _app;
private static readonly Tag _appTag = new("App");
private readonly AppCommand _appCommand;
private readonly AppItem _app;
private readonly Lazy<Details> _details;
private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
private InterlockedBoolean _isLoadingIcon;
private readonly Lazy<IconInfo> _icon;
public override IDetails? Details { get => _details.Value; set => base.Details = value; }
public override IIconInfo? Icon
{
get
{
if (_isLoadingIcon.Set())
{
_ = LoadIconAsync();
}
return base.Icon;
}
set => base.Icon = value;
}
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
public string AppIdentifier => _app.AppIdentifier;
public AppListItem(AppItem app, bool useThumbnails, bool isPinned)
: base(new AppCommand(app))
{
Command = _appCommand = new AppCommand(app);
_app = app;
Title = app.Name;
Subtitle = app.Subtitle;
Tags = [_appTag];
Icon = Icons.GenericAppIcon;
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
@@ -60,19 +43,12 @@ public sealed partial class AppListItem : ListItem
return t.Result;
});
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails));
}
private async Task LoadIconAsync()
{
try
_icon = new Lazy<IconInfo>(() =>
{
Icon = _appCommand.Icon = await _iconLoadTask.Value ?? Icons.GenericAppIcon;
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to load icon for {AppIdentifier}\n{ex}");
}
var t = FetchIcon(useThumbnails);
t.Wait();
return t.Result;
});
}
private async Task<Details> BuildDetails()
@@ -111,12 +87,12 @@ public sealed partial class AppListItem : ListItem
return new Details()
{
Title = this.Title,
HeroImage = heroImage ?? this.Icon ?? Icons.GenericAppIcon,
HeroImage = heroImage ?? this.Icon ?? new IconInfo(string.Empty),
Metadata = metadata.ToArray(),
};
}
private async Task<IconInfo> FetchIcon(bool useThumbnails)
public async Task<IconInfo> FetchIcon(bool useThumbnails)
{
IconInfo? icon = null;
if (_app.IsPackaged)
@@ -132,12 +108,12 @@ public sealed partial class AppListItem : ListItem
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath);
if (stream is not null)
{
icon = IconInfo.FromStream(stream);
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
icon = new IconInfo(data, data);
}
}
catch (Exception ex)
catch
{
Logger.LogDebug($"Failed to load icon for {AppIdentifier}:\n{ex}");
}
icon = icon ?? new IconInfo(_app.IcoPath);

View File

@@ -6,23 +6,21 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps;
internal static class Icons
internal sealed class Icons
{
internal static IconInfo AllAppsIcon { get; } = IconHelpers.FromRelativePath("Assets\\AllApps.svg");
internal static IconInfo AllAppsIcon => IconHelpers.FromRelativePath("Assets\\AllApps.svg");
internal static IconInfo RunAsUserIcon { get; } = new("\uE7EE"); // OtherUser icon
internal static IconInfo RunAsUserIcon => new("\uE7EE"); // OtherUser icon
internal static IconInfo RunAsAdminIcon { get; } = new("\uE7EF"); // Admin icon
internal static IconInfo RunAsAdminIcon => new("\uE7EF"); // Admin icon
internal static IconInfo OpenPathIcon { get; } = new("\ue838"); // Folder Open icon
internal static IconInfo OpenPathIcon => new("\ue838"); // Folder Open icon
internal static IconInfo CopyIcon { get; } = new("\ue8c8"); // Copy icon
internal static IconInfo CopyIcon => new("\ue8c8"); // Copy icon
public static IconInfo UnpinIcon { get; } = new("\uE77A"); // Unpin icon
public static IconInfo PinIcon { get; } = new("\uE840"); // Pin icon
public static IconInfo UninstallApplicationIcon { get; } = new("\uE74D"); // Uninstall icon
public static IconInfo GenericAppIcon { get; } = new("\uE737"); // Favicon
}

View File

@@ -103,8 +103,7 @@ public class UWPApplication : IUWPApplication
new CommandContextItem(
new OpenFileCommand(Location)
{
Icon = new("\uE838"),
Name = Resources.open_location,
Name = Resources.open_containing_folder,
})
{
RequestedShortcut = KeyChords.OpenFileLocation,

View File

@@ -207,10 +207,7 @@ public class Win32Program : IProgram
});
commands.Add(new CommandContextItem(
new ShowFileInFolderCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath)
{
Name = Resources.open_location,
})
new OpenFileCommand(ParentDirectory))
{
RequestedShortcut = KeyChords.OpenFileLocation,
});

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
@@ -241,7 +241,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
/// <summary>
/// Looks up a localized string similar to Open file location.
/// Looks up a localized string similar to Open location.
/// </summary>
internal static string open_location {
get {

View File

@@ -161,7 +161,7 @@
<value>File</value>
</data>
<data name="open_location" xml:space="preserve">
<value>Open file location</value>
<value>Open location</value>
</data>
<data name="copy_path" xml:space="preserve">
<value>Copy path</value>
@@ -237,4 +237,4 @@
<data name="limit_none" xml:space="preserve">
<value>Unlimited</value>
</data>
</root>
</root>

View File

@@ -4,28 +4,38 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Bookmarks.Pages;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed partial class AddBookmarkForm : FormContent
{
private readonly BookmarkData? _bookmark;
internal event TypedEventHandler<object, BookmarkData>? AddedCommand;
private readonly BookmarkData? _bookmark;
public AddBookmarkForm(BookmarkData? bookmark)
{
_bookmark = bookmark;
var name = bookmark?.Name ?? string.Empty;
var url = bookmark?.Bookmark ?? string.Empty;
var name = _bookmark?.Name ?? string.Empty;
var url = _bookmark?.Bookmark ?? string.Empty;
TemplateJson = $$"""
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "Input.Text",
"style": "text",
"id": "name",
"label": "{{Resources.bookmarks_form_name_label}}",
"value": {{JsonSerializer.Serialize(name, BookmarkSerializationContext.Default.String)}},
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_name_required}}"
},
{
"type": "Input.Text",
"style": "text",
@@ -34,15 +44,6 @@ internal sealed partial class AddBookmarkForm : FormContent
"label": "{{Resources.bookmarks_form_bookmark_label}}",
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_bookmark_required}}"
},
{
"type": "Input.Text",
"style": "text",
"id": "name",
"label": "{{Resources.bookmarks_form_name_label}}",
"value": {{JsonSerializer.Serialize(name, BookmarkSerializationContext.Default.String)}},
"isRequired": false,
"errorMessage": "{{Resources.bookmarks_form_name_required}}"
}
],
"actions": [
@@ -70,7 +71,13 @@ internal sealed partial class AddBookmarkForm : FormContent
// get the name and url out of the values
var formName = formInput["name"] ?? string.Empty;
var formBookmark = formInput["bookmark"] ?? string.Empty;
AddedCommand?.Invoke(this, new BookmarkData(formName.ToString(), formBookmark.ToString()) { Id = _bookmark?.Id ?? Guid.Empty });
var hasPlaceholder = formBookmark.ToString().Contains('{') && formBookmark.ToString().Contains('}');
var updated = _bookmark ?? new BookmarkData();
updated.Name = formName.ToString();
updated.Bookmark = formBookmark.ToString();
AddedCommand?.Invoke(this, updated);
return CommandResult.GoHome();
}
}

View File

@@ -2,33 +2,33 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Bookmarks.Pages;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed partial class AddBookmarkPage : ContentPage
{
private readonly AddBookmarkForm _addBookmark;
internal event TypedEventHandler<object, BookmarkData>? AddedCommand
{
add => _addBookmarkForm.AddedCommand += value;
remove => _addBookmarkForm.AddedCommand -= value;
add => _addBookmark.AddedCommand += value;
remove => _addBookmark.AddedCommand -= value;
}
private readonly AddBookmarkForm _addBookmarkForm;
public override IContent[] GetContent() => [_addBookmark];
public AddBookmarkPage(BookmarkData? bookmark)
{
var name = bookmark?.Name ?? string.Empty;
var url = bookmark?.Bookmark ?? string.Empty;
Icon = Icons.BookmarkIcon;
var isAdd = string.IsNullOrEmpty(name) && string.IsNullOrEmpty(url);
Title = isAdd ? Resources.bookmarks_add_title : Resources.bookmarks_edit_name;
Name = isAdd ? Resources.bookmarks_add_name : Resources.bookmarks_edit_name;
_addBookmarkForm = new AddBookmarkForm(bookmark);
_addBookmark = new(bookmark);
}
public override IContent[] GetContent() => [_addBookmarkForm];
}

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.Text.Json.Serialization;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public class BookmarkData
{
public string Name { get; set; } = string.Empty;
public string Bookmark { get; set; } = string.Empty;
// public string Type { get; set; } = string.Empty;
[JsonIgnore]
public bool IsPlaceholder => Bookmark.Contains('{') && Bookmark.Contains('}');
internal void GetExeAndArgs(out string exe, out string args)
{
ShellHelpers.ParseExecutableAndArgs(Bookmark, out exe, out args);
}
internal bool IsWebUrl()
{
GetExeAndArgs(out var exe, out var args);
if (string.IsNullOrEmpty(exe))
{
return false;
}
if (Uri.TryCreate(exe, UriKind.Absolute, out var uri))
{
if (uri.Scheme == Uri.UriSchemeFile)
{
return false;
}
// return true if the scheme is http or https, or if there's no scheme (e.g., "www.example.com") but there is a dot in the host
return
uri.Scheme == Uri.UriSchemeHttp ||
uri.Scheme == Uri.UriSchemeHttps ||
(string.IsNullOrEmpty(uri.Scheme) && uri.Host.Contains('.'));
}
// If we can't parse it as a URI, we assume it's not a web URL
return false;
}
}

View File

@@ -2,9 +2,11 @@
// 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.Text.Json;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks.Persistence;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public class BookmarkJsonParser
{
@@ -12,32 +14,32 @@ public class BookmarkJsonParser
{
}
public BookmarksData ParseBookmarks(string json)
public Bookmarks ParseBookmarks(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
return new BookmarksData();
return new Bookmarks();
}
try
{
var bookmarks = JsonSerializer.Deserialize<BookmarksData>(json, BookmarkSerializationContext.Default.BookmarksData);
return bookmarks ?? new BookmarksData();
var bookmarks = JsonSerializer.Deserialize<Bookmarks>(json, BookmarkSerializationContext.Default.Bookmarks);
return bookmarks ?? new Bookmarks();
}
catch (JsonException ex)
{
ExtensionHost.LogMessage($"parse bookmark data failed. ex: {ex.Message}");
return new BookmarksData();
return new Bookmarks();
}
}
public string SerializeBookmarks(BookmarksData? bookmarks)
public string SerializeBookmarks(Bookmarks? bookmarks)
{
if (bookmarks == null)
{
return string.Empty;
}
return JsonSerializer.Serialize(bookmarks, BookmarkSerializationContext.Default.BookmarksData);
return JsonSerializer.Serialize(bookmarks, BookmarkSerializationContext.Default.Bookmarks);
}
}

View File

@@ -0,0 +1,92 @@
// 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.Globalization;
using System.Linq;
using System.Text;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed partial class BookmarkPlaceholderForm : FormContent
{
private static readonly CompositeFormat ErrorMessage = System.Text.CompositeFormat.Parse(Resources.bookmarks_required_placeholder);
private readonly List<string> _placeholderNames;
private readonly string _bookmark = string.Empty;
// TODO pass in an array of placeholders
public BookmarkPlaceholderForm(string name, string url)
{
_bookmark = url;
var r = new Regex(Regex.Escape("{") + "(.*?)" + Regex.Escape("}"));
var matches = r.Matches(url);
_placeholderNames = matches.Select(m => m.Groups[1].Value).ToList();
var inputs = _placeholderNames.Select(p =>
{
var errorMessage = string.Format(CultureInfo.CurrentCulture, ErrorMessage, p);
return $$"""
{
"type": "Input.Text",
"style": "text",
"id": "{{p}}",
"label": "{{p}}",
"isRequired": true,
"errorMessage": "{{errorMessage}}"
}
""";
}).ToList();
var allInputs = string.Join(",", inputs);
TemplateJson = $$"""
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
""" + allInputs + $$"""
],
"actions": [
{
"type": "Action.Submit",
"title": "{{Resources.bookmarks_form_open}}",
"data": {
"placeholder": "placeholder"
}
}
]
}
""";
}
public override CommandResult SubmitForm(string payload)
{
var target = _bookmark;
// parse the submitted JSON and then open the link
var formInput = JsonNode.Parse(payload);
var formObject = formInput?.AsObject();
if (formObject is null)
{
return CommandResult.GoHome();
}
foreach (var (key, value) in formObject)
{
var placeholderString = $"{{{key}}}";
var placeholderData = value?.ToString();
target = target.Replace(placeholderString, placeholderData);
}
var success = UrlCommand.LaunchCommand(target);
return success ? CommandResult.Dismiss() : CommandResult.KeepOpen();
}
}

View File

@@ -0,0 +1,39 @@
// 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.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed partial class BookmarkPlaceholderPage : ContentPage
{
private readonly Lazy<IconInfo> _icon;
private readonly FormContent _bookmarkPlaceholder;
public override IContent[] GetContent() => [_bookmarkPlaceholder];
public override IconInfo Icon { get => _icon.Value; set => base.Icon = value; }
public BookmarkPlaceholderPage(BookmarkData data)
: this(data.Name, data.Bookmark)
{
}
public BookmarkPlaceholderPage(string name, string url)
{
Name = Properties.Resources.bookmarks_command_name_open;
_bookmarkPlaceholder = new BookmarkPlaceholderForm(name, url);
_icon = new Lazy<IconInfo>(() =>
{
ShellHelpers.ParseExecutableAndArgs(url, out var exe, out var args);
var t = UrlCommand.GetIconForPath(exe);
t.Wait();
return t.Result;
});
}
}

View File

@@ -2,16 +2,19 @@
// 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.Text.Json.Serialization;
namespace Microsoft.CmdPal.Ext.Bookmarks.Persistence;
namespace Microsoft.CmdPal.Ext.Bookmarks;
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(BookmarkData))]
[JsonSerializable(typeof(BookmarksData))]
[JsonSerializable(typeof(Bookmarks))]
[JsonSerializable(typeof(List<BookmarkData>), TypeInfoPropertyName = "BookmarkList")]
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
internal sealed partial class BookmarkSerializationContext : JsonSerializerContext;
internal sealed partial class BookmarkSerializationContext : JsonSerializerContext
{
}

View File

@@ -2,9 +2,13 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.Bookmarks.Persistence;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
public sealed class BookmarksData
namespace Microsoft.CmdPal.Ext.Bookmarks;
public sealed class Bookmarks
{
public List<BookmarkData> Data { get; set; } = [];
}

View File

@@ -2,129 +2,186 @@
// 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.Diagnostics.Contracts;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.CmdPal.Ext.Bookmarks.Pages;
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
using Microsoft.CmdPal.Ext.Bookmarks.Services;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
using Microsoft.CmdPal.Ext.Indexer;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public sealed partial class BookmarksCommandProvider : CommandProvider
public partial class BookmarksCommandProvider : CommandProvider
{
private const int LoadStateNotLoaded = 0;
private const int LoadStateLoading = 1;
private const int LoadStateLoaded = 2;
private readonly List<CommandItem> _commands = [];
private readonly IPlaceholderParser _placeholderParser = new PlaceholderParser();
private readonly IBookmarksManager _bookmarksManager;
private readonly IBookmarkResolver _commandResolver;
private readonly IBookmarkIconLocator _iconLocator = new IconLocator();
private readonly AddBookmarkPage _addNewCommand = new(null);
private readonly ListItem _addNewItem;
private readonly Lock _bookmarksLock = new();
private readonly IBookmarkDataSource _dataSource;
private readonly BookmarkJsonParser _parser;
private Bookmarks? _bookmarks;
private ICommandItem[] _commands = [];
private List<BookmarkListItem> _bookmarks = [];
private int _loadState;
private static string StateJsonPath()
public BookmarksCommandProvider()
: this(new FileBookmarkDataSource(StateJsonPath()))
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, "bookmarks.json");
}
public static BookmarksCommandProvider CreateWithDefaultStore()
internal BookmarksCommandProvider(IBookmarkDataSource dataSource)
{
return new BookmarksCommandProvider(new BookmarksManager(new FileBookmarkDataSource(StateJsonPath())));
}
internal BookmarksCommandProvider(IBookmarksManager bookmarksManager)
{
ArgumentNullException.ThrowIfNull(bookmarksManager);
_bookmarksManager = bookmarksManager;
_bookmarksManager.BookmarkAdded += OnBookmarkAdded;
_bookmarksManager.BookmarkRemoved += OnBookmarkRemoved;
_commandResolver = new BookmarkResolver(_placeholderParser);
_dataSource = dataSource;
_parser = new BookmarkJsonParser();
Id = "Bookmarks";
DisplayName = Resources.bookmarks_display_name;
Icon = Icons.PinIcon;
var addBookmarkPage = new AddBookmarkPage(null);
addBookmarkPage.AddedCommand += (_, e) => _bookmarksManager.Add(e.Name, e.Bookmark);
_addNewItem = new ListItem(addBookmarkPage);
_addNewCommand.AddedCommand += AddNewCommand_AddedCommand;
}
private void OnBookmarkAdded(BookmarkData bookmarkData)
private void AddNewCommand_AddedCommand(object sender, BookmarkData args)
{
var newItem = new BookmarkListItem(bookmarkData, _bookmarksManager, _commandResolver, _iconLocator, _placeholderParser);
lock (_bookmarksLock)
{
_bookmarks.Add(newItem);
}
ExtensionHost.LogMessage($"Adding bookmark ({args.Name},{args.Bookmark})");
_bookmarks?.Data.Add(args);
NotifyChange();
SaveAndUpdateCommands();
}
private void OnBookmarkRemoved(BookmarkData bookmarkData)
// In the edit path, `args` was already in _bookmarks, we just updated it
private void Edit_AddedCommand(object sender, BookmarkData args)
{
lock (_bookmarksLock)
ExtensionHost.LogMessage($"Edited bookmark ({args.Name},{args.Bookmark})");
SaveAndUpdateCommands();
}
private void SaveAndUpdateCommands()
{
try
{
_bookmarks.RemoveAll(t => t.BookmarkId == bookmarkData.Id);
var jsonData = _parser.SerializeBookmarks(_bookmarks);
_dataSource.SaveBookmarkData(jsonData);
}
catch (Exception ex)
{
Logger.LogError($"Failed to save bookmarks: {ex.Message}");
}
NotifyChange();
LoadCommands();
RaiseItemsChanged(0);
}
private void LoadCommands()
{
List<CommandItem> collected = [];
collected.Add(new CommandItem(_addNewCommand));
if (_bookmarks is null)
{
LoadBookmarksFromFile();
}
if (_bookmarks is not null)
{
collected.AddRange(_bookmarks.Data.Select(BookmarkToCommandItem));
}
_commands.Clear();
_commands.AddRange(collected);
}
private void LoadBookmarksFromFile()
{
try
{
var jsonData = _dataSource.GetBookmarkData();
_bookmarks = _parser.ParseBookmarks(jsonData);
}
catch (Exception ex)
{
Logger.LogError(ex.Message);
}
if (_bookmarks is null)
{
_bookmarks = new();
}
}
private CommandItem BookmarkToCommandItem(BookmarkData bookmark)
{
ICommand command = bookmark.IsPlaceholder ?
new BookmarkPlaceholderPage(bookmark) :
new UrlCommand(bookmark);
var listItem = new CommandItem(command) { Icon = command.Icon };
List<CommandContextItem> contextMenu = [];
// Add commands for folder types
if (command is UrlCommand urlCommand)
{
if (!bookmark.IsWebUrl())
{
contextMenu.Add(
new CommandContextItem(new DirectoryPage(urlCommand.Url)));
contextMenu.Add(
new CommandContextItem(new OpenInTerminalCommand(urlCommand.Url)));
}
}
listItem.Title = bookmark.Name;
listItem.Subtitle = bookmark.Bookmark;
var edit = new AddBookmarkPage(bookmark) { Icon = Icons.EditIcon };
edit.AddedCommand += Edit_AddedCommand;
contextMenu.Add(new CommandContextItem(edit));
var delete = new CommandContextItem(
title: Resources.bookmarks_delete_title,
name: Resources.bookmarks_delete_name,
action: () =>
{
if (_bookmarks is not null)
{
ExtensionHost.LogMessage($"Deleting bookmark ({bookmark.Name},{bookmark.Bookmark})");
_bookmarks.Data.Remove(bookmark);
SaveAndUpdateCommands();
}
},
result: CommandResult.KeepOpen())
{
IsCritical = true,
Icon = Icons.DeleteIcon,
};
contextMenu.Add(delete);
listItem.MoreCommands = contextMenu.ToArray();
return listItem;
}
public override ICommandItem[] TopLevelCommands()
{
if (Volatile.Read(ref _loadState) != LoadStateLoaded)
if (_commands.Count == 0)
{
if (Interlocked.CompareExchange(ref _loadState, LoadStateLoading, LoadStateNotLoaded) == LoadStateNotLoaded)
{
try
{
lock (_bookmarksLock)
{
_bookmarks = [.. _bookmarksManager.Bookmarks.Select(bookmark => new BookmarkListItem(bookmark, _bookmarksManager, _commandResolver, _iconLocator, _placeholderParser))];
_commands = BuildTopLevelCommandsUnsafe();
}
Volatile.Write(ref _loadState, LoadStateLoaded);
RaiseItemsChanged();
}
catch
{
Volatile.Write(ref _loadState, LoadStateNotLoaded);
throw;
}
}
LoadCommands();
}
return _commands;
return _commands.ToArray();
}
private void NotifyChange()
internal static string StateJsonPath()
{
if (Volatile.Read(ref _loadState) != LoadStateLoaded)
{
return;
}
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
lock (_bookmarksLock)
{
_commands = BuildTopLevelCommandsUnsafe();
}
RaiseItemsChanged();
// now, the state is just next to the exe
return System.IO.Path.Combine(directory, "bookmarks.json");
}
[Pure]
private ICommandItem[] BuildTopLevelCommandsUnsafe() => [_addNewItem, .. _bookmarks];
}

View File

@@ -1,141 +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.Linq;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
namespace Microsoft.CmdPal.Ext.Bookmarks;
internal sealed partial class BookmarksManager : IDisposable, IBookmarksManager
{
private readonly IBookmarkDataSource _dataSource;
private readonly BookmarkJsonParser _parser = new();
private readonly SupersedingAsyncGate _savingGate;
private readonly Lock _lock = new();
private BookmarksData _bookmarksData = new();
public event Action<BookmarkData>? BookmarkAdded;
public event Action<BookmarkData, BookmarkData>? BookmarkUpdated; // old, new
public event Action<BookmarkData>? BookmarkRemoved;
public IReadOnlyCollection<BookmarkData> Bookmarks
{
get
{
lock (_lock)
{
return _bookmarksData.Data.ToList().AsReadOnly();
}
}
}
public BookmarksManager(IBookmarkDataSource dataSource)
{
ArgumentNullException.ThrowIfNull(dataSource);
_dataSource = dataSource;
_savingGate = new SupersedingAsyncGate(WriteData);
LoadBookmarksFromFile();
}
public BookmarkData Add(string name, string bookmark)
{
var newBookmark = new BookmarkData(name, bookmark);
lock (_lock)
{
_bookmarksData.Data.Add(newBookmark);
_ = SaveChangesAsync();
BookmarkAdded?.Invoke(newBookmark);
return newBookmark;
}
}
public bool Remove(Guid id)
{
lock (_lock)
{
var bookmark = _bookmarksData.Data.FirstOrDefault(b => b.Id == id);
if (bookmark != null && _bookmarksData.Data.Remove(bookmark))
{
_ = SaveChangesAsync();
BookmarkRemoved?.Invoke(bookmark);
return true;
}
return false;
}
}
public BookmarkData? Update(Guid id, string name, string bookmark)
{
lock (_lock)
{
var existingBookmark = _bookmarksData.Data.FirstOrDefault(b => b.Id == id);
if (existingBookmark != null)
{
var updatedBookmark = existingBookmark with
{
Name = name,
Bookmark = bookmark,
};
var index = _bookmarksData.Data.IndexOf(existingBookmark);
_bookmarksData.Data[index] = updatedBookmark;
_ = SaveChangesAsync();
BookmarkUpdated?.Invoke(existingBookmark, updatedBookmark);
return updatedBookmark;
}
return null;
}
}
private void LoadBookmarksFromFile()
{
try
{
var jsonData = _dataSource.GetBookmarkData();
_bookmarksData = _parser.ParseBookmarks(jsonData);
}
catch (Exception ex)
{
Logger.LogError(ex.Message);
}
}
private Task WriteData(CancellationToken arg)
{
List<BookmarkData> dataToSave;
lock (_lock)
{
dataToSave = _bookmarksData.Data.ToList();
}
try
{
var jsonData = _parser.SerializeBookmarks(new BookmarksData { Data = dataToSave });
_dataSource.SaveBookmarkData(jsonData);
}
catch (Exception ex)
{
Logger.LogError($"Failed to save bookmarks: {ex.Message}");
}
return Task.CompletedTask;
}
private async Task SaveChangesAsync()
{
await _savingGate.ExecuteAsync(CancellationToken.None);
}
public void Dispose() => _savingGate.Dispose();
}

View File

@@ -1,30 +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.Ext.Bookmarks.Persistence;
namespace Microsoft.CmdPal.Ext.Bookmarks.Commands;
internal sealed partial class DeleteBookmarkCommand : InvokableCommand
{
private readonly BookmarkData _bookmark;
private readonly IBookmarksManager _bookmarksManager;
public DeleteBookmarkCommand(BookmarkData bookmark, IBookmarksManager bookmarksManager)
{
ArgumentNullException.ThrowIfNull(bookmark);
ArgumentNullException.ThrowIfNull(bookmarksManager);
_bookmark = bookmark;
_bookmarksManager = bookmarksManager;
Name = Resources.bookmarks_delete_name;
Icon = Icons.DeleteIcon;
}
public override CommandResult Invoke()
{
_bookmarksManager.Remove(_bookmark.Id);
return CommandResult.GoHome();
}
}

View File

@@ -1,109 +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.Globalization;
using System.Text;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
using Microsoft.CmdPal.Ext.Bookmarks.Services;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Ext.Bookmarks.Commands;
internal sealed partial class LaunchBookmarkCommand : BaseObservable, IInvokableCommand, IDisposable
{
private static readonly CompositeFormat FailedToOpenMessageFormat = CompositeFormat.Parse(Resources.bookmark_toast_failed_open_text!);
private readonly BookmarkData _bookmarkData;
private readonly Dictionary<string, string>? _placeholders;
private readonly IBookmarkResolver _bookmarkResolver;
private readonly SupersedingAsyncValueGate<IIconInfo?> _iconReloadGate;
private readonly Classification _classification;
private IIconInfo? _icon;
public IIconInfo Icon => _icon ?? Icons.Reloading;
public string Name { get; }
public string Id { get; }
public LaunchBookmarkCommand(BookmarkData bookmarkData, Classification classification, IBookmarkIconLocator iconLocator, IBookmarkResolver bookmarkResolver, Dictionary<string, string>? placeholders = null)
{
ArgumentNullException.ThrowIfNull(bookmarkData);
ArgumentNullException.ThrowIfNull(classification);
_bookmarkData = bookmarkData;
_classification = classification;
_placeholders = placeholders;
_bookmarkResolver = bookmarkResolver;
Id = CommandIds.GetLaunchBookmarkItemId(bookmarkData.Id);
Name = Resources.bookmarks_command_name_open;
_iconReloadGate = new(
async ct => await iconLocator.GetIconForPath(_classification, ct),
icon =>
{
_icon = icon;
OnPropertyChanged(nameof(Icon));
});
RequestIconReloadAsync();
}
private void RequestIconReloadAsync()
{
_icon = null;
OnPropertyChanged(nameof(Icon));
_ = _iconReloadGate.ExecuteAsync();
}
public ICommandResult Invoke(object sender)
{
var bookmarkAddress = ReplacePlaceholders(_bookmarkData.Bookmark);
var classification = _bookmarkResolver.ClassifyOrUnknown(bookmarkAddress);
var success = CommandLauncher.Launch(classification);
return success
? CommandResult.Dismiss()
: CommandResult.ShowToast(new ToastArgs
{
Message = !string.IsNullOrWhiteSpace(_bookmarkData.Name)
? string.Format(CultureInfo.CurrentCulture, FailedToOpenMessageFormat, _bookmarkData.Name + ": " + bookmarkAddress)
: string.Format(CultureInfo.CurrentCulture, FailedToOpenMessageFormat, bookmarkAddress),
Result = CommandResult.KeepOpen(),
});
}
private string ReplacePlaceholders(string input)
{
var result = input;
if (_placeholders?.Count > 0)
{
foreach (var (key, value) in _placeholders)
{
var placeholderString = $"{{{key}}}";
var encodedValue = value;
if (_classification.Kind is CommandKind.Protocol or CommandKind.WebUrl)
{
encodedValue = Uri.EscapeDataString(value);
}
result = result.Replace(placeholderString, encodedValue, StringComparison.OrdinalIgnoreCase);
}
}
return result;
}
public void Dispose()
{
_iconReloadGate.Dispose();
GC.SuppressFinalize(this);
}
}

View File

@@ -2,11 +2,13 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks.Persistence;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public sealed partial class FileBookmarkDataSource : IBookmarkDataSource
public class FileBookmarkDataSource : IBookmarkDataSource
{
private readonly string _filePath;

View File

@@ -1,8 +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.
global using System;
global using System.Collections.Generic;
global using Microsoft.CmdPal.Ext.Bookmarks.Properties;
global using Microsoft.CommandPalette.Extensions.Toolkit;

View File

@@ -1,20 +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.Ext.Bookmarks.Helpers;
public sealed record Classification(
CommandKind Kind,
string Input,
string Target,
string Arguments,
LaunchMethod Launch,
string? WorkingDirectory,
bool IsPlaceholder,
string? FileSystemTarget = null,
string? DisplayName = null)
{
public static Classification Unknown(string rawInput) =>
new(CommandKind.Unknown, rawInput, rawInput, string.Empty, LaunchMethod.ShellExecute, string.Empty, false, null, null);
}

View File

@@ -1,15 +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.Ext.Bookmarks.Helpers;
internal static class CommandIds
{
/// <summary>
/// Returns id of a command associated with a bookmark item. This id is for a command that launches the bookmark - regardless of whether
/// the bookmark type of if it is a placeholder bookmark or not.
/// </summary>
/// <param name="id">Bookmark ID</param>
public static string GetLaunchBookmarkItemId(Guid id) => "Bookmarks.Launch." + id;
}

View File

@@ -1,66 +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.Ext.Bookmarks.Helpers;
/// <summary>
/// Classifies a command or bookmark target type.
/// </summary>
public enum CommandKind
{
/// <summary>
/// Unknown or unsupported target.
/// </summary>
Unknown = 0,
/// <summary>
/// HTTP/HTTPS URL.
/// </summary>
WebUrl,
/// <summary>
/// Any non-file URI scheme (e.g., mailto:, ms-settings:, wt:, myapp:).
/// </summary>
Protocol,
/// <summary>
/// Application User Model ID (e.g., shell:AppsFolder\AUMID or pkgfamily!app).
/// </summary>
Aumid,
/// <summary>
/// Existing folder path.
/// </summary>
Directory,
/// <summary>
/// Existing executable file (e.g., .exe, .bat, .cmd).
/// </summary>
FileExecutable,
/// <summary>
/// Existing document file.
/// </summary>
FileDocument,
/// <summary>
/// Windows shortcut file (*.lnk).
/// </summary>
Shortcut,
/// <summary>
/// Internet shortcut file (*.url).
/// </summary>
InternetShortcut,
/// <summary>
/// Bare command resolved via PATH/PATHEXT (e.g., "wt", "git").
/// </summary>
PathCommand,
/// <summary>
/// Shell item not matching other types (e.g., Control Panel item, purely virtual directory).
/// </summary>
VirtualShellItem,
}

View File

@@ -1,98 +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.ComponentModel;
using System.Runtime.InteropServices;
using ManagedCommon;
namespace Microsoft.CmdPal.Ext.Bookmarks.Helpers;
internal static class CommandLauncher
{
/// <summary>
/// Launches the classified item.
/// </summary>
/// <param name="classification">Classification produced by CommandClassifier.</param>
/// <param name="runAsAdmin">Optional: force elevation if possible.</param>
public static bool Launch(Classification classification, bool runAsAdmin = false)
{
switch (classification.Launch)
{
case LaunchMethod.ExplorerOpen:
// Folders and shell: URIs are best handled by explorer.exe
// You can notice the difference with Recycle Bin for example:
// - "explorer ::{645FF040-5081-101B-9F08-00AA002F954E}"
// - "::{645FF040-5081-101B-9F08-00AA002F954E}"
return ShellHelpers.OpenInShell("explorer.exe", classification.Target);
case LaunchMethod.ActivateAppId:
return ActivateAppId(classification.Target, classification.Arguments);
case LaunchMethod.ShellExecute:
default:
return ShellHelpers.OpenInShell(classification.Target, classification.Arguments, classification.WorkingDirectory, runAsAdmin ? ShellHelpers.ShellRunAsType.Administrator : ShellHelpers.ShellRunAsType.None);
}
}
private static bool ActivateAppId(string aumidOrAppsFolder, string? arguments)
{
const string shellAppsFolder = "shell:AppsFolder\\";
try
{
if (aumidOrAppsFolder.StartsWith(shellAppsFolder, StringComparison.OrdinalIgnoreCase))
{
aumidOrAppsFolder = aumidOrAppsFolder[shellAppsFolder.Length..];
}
ApplicationActivationManager.ActivateApplication(aumidOrAppsFolder, arguments, 0, out _);
return true;
}
catch (Exception ex)
{
Logger.LogError($"Can't activate AUMID using app store '{aumidOrAppsFolder}'", ex);
}
try
{
ShellHelpers.OpenInShell(shellAppsFolder + aumidOrAppsFolder, arguments);
return true;
}
catch (Exception ex)
{
Logger.LogError($"Can't activate AUMID using shell '{aumidOrAppsFolder}'", ex);
}
return false;
}
private static class ApplicationActivationManager
{
public static void ActivateApplication(string aumid, string? args, int options, out uint pid)
{
var mgr = (IApplicationActivationManager)new _ApplicationActivationManager();
var hr = mgr.ActivateApplication(aumid, args ?? string.Empty, options, out pid);
if (hr < 0)
{
throw new Win32Exception(hr);
}
}
[ComImport]
[Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Private class")]
private class _ApplicationActivationManager;
[ComImport]
[Guid("2E941141-7F97-4756-BA1D-9DECDE894A3D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IApplicationActivationManager
{
int ActivateApplication(
[MarshalAs(UnmanagedType.LPWStr)] string appUserModelId,
[MarshalAs(UnmanagedType.LPWStr)] string arguments,
int options,
out uint processId);
}
}
}

View File

@@ -1,294 +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.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Bookmarks.Helpers;
/// <summary>
/// Provides helper methods for parsing command lines and expanding paths.
/// </summary>
/// <remarks>
/// Warning: This code handles parsing specifically for Bookmarks, and is NOT a general-purpose command line parser.
/// In some cases it mimics system rules (e.g. CreateProcess, CommandLineToArgvW) but in other cases it uses, but it can also
/// bend the rules to be more forgiving.
/// </remarks>
internal static partial class CommandLineHelper
{
private static readonly char[] PathSeparators = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar];
public static string[] SplitCommandLine(string commandLine)
{
ArgumentNullException.ThrowIfNull(commandLine);
var argv = NativeMethods.CommandLineToArgvW(commandLine, out var argc);
if (argv == IntPtr.Zero)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
try
{
var result = new string[argc];
for (var i = 0; i < argc; i++)
{
var p = Marshal.ReadIntPtr(argv, i * IntPtr.Size);
result[i] = Marshal.PtrToStringUni(p)!;
}
return result;
}
finally
{
NativeMethods.LocalFree(argv);
}
}
/// <summary>
/// Splits the raw command line into the first argument (Head) and the remainder (Tail). This method follows the rules
/// of CommandLineToArgvW.
/// </summary>
/// <remarks>
/// This is a mental support for SplitLongestHeadBeforeQuotedArg.
///
/// Rules:
/// - If the input starts with any whitespace, Head is an empty string (per CommandLineToArgvW behavior for first segment, handles by CreateProcess rules).
/// - Otherwise, Head uses the CreateProcess "program name" rule:
/// - If the first char is a quote, Head is everything up to the next quote (backslashes do NOT escape it).
/// - Else, Head is the run up to the first whitespace.
/// - Tail starts at the first non-whitespace character after Head (or is empty if nothing remains).
/// No normalization is performed; returned slices preserve the original text (no un/escaping).
/// </remarks>
public static (string Head, string Tail) SplitHeadAndArgs(string input)
{
ArgumentNullException.ThrowIfNull(input);
if (input.Length == 0)
{
return (string.Empty, string.Empty);
}
var s = input.AsSpan();
var n = s.Length;
var i = 0;
// Leading whitespace -> empty argv[0]
if (char.IsWhiteSpace(s[0]))
{
while (i < n && char.IsWhiteSpace(s[i]))
{
i++;
}
var tailAfterWs = i < n ? input[i..] : string.Empty;
return (string.Empty, tailAfterWs);
}
string head;
if (s[i] == '"')
{
// Quoted program name: everything up to the next unescaped quote (CreateProcess rule: slashes don't escape here)
i++;
var start = i;
while (i < n && s[i] != '"')
{
i++;
}
head = input.Substring(start, i - start);
if (i < n && s[i] == '"')
{
i++; // consume closing quote
}
}
else
{
// Unquoted program name: read to next whitespace
var start = i;
while (i < n && !char.IsWhiteSpace(s[i]))
{
i++;
}
head = input.Substring(start, i - start);
}
// Skip inter-argument whitespace; tail begins at the next non-ws char (or is empty)
while (i < n && char.IsWhiteSpace(s[i]))
{
i++;
}
var tail = i < n ? input[i..] : string.Empty;
return (head, tail);
}
/// <summary>
/// Returns the longest possible head (may include spaces) and the tail that starts at the
/// first *quoted argument*.
///
/// Definition of "quoted argument start":
/// - A token boundary (start-of-line or preceded by whitespace),
/// - followed by zero or more backslashes,
/// - followed by a double-quote ("),
/// - where the number of immediately preceding backslashes is EVEN (so the quote toggles quoting).
///
/// Notes:
/// - Quotes appearing mid-token (e.g., C:\Some\"Path\file.txt) do NOT stop the head.
/// - Trailing spaces before the quoted arg are not included in Head; Tail begins at that quote.
/// - Leading whitespace before the first token is ignored (Head starts from first non-ws).
/// Examples:
/// C:\app exe -p "1" -q -> Head: "C:\app exe -p", Tail: "\"1\" -q"
/// "\\server\share\" with args -> Head: "", Tail: "\"\\\\server\\share\\\" with args"
/// C:\Some\"Path\file.txt -> Head: "C:\\Some\\\"Path\\file.txt", Tail: ""
/// </summary>
public static (string Head, string Tail) SplitLongestHeadBeforeQuotedArg(string input)
{
ArgumentNullException.ThrowIfNull(input);
if (input.Length == 0)
{
return (string.Empty, string.Empty);
}
var s = input.AsSpan();
var n = s.Length;
// Start at first non-whitespace (we don't treat leading ws as part of Head here)
var start = 0;
while (start < n && char.IsWhiteSpace(s[start]))
{
start++;
}
if (start >= n)
{
return (string.Empty, string.Empty);
}
// Scan for a quote that OPENS a quoted argument at a token boundary.
for (var i = start; i < n; i++)
{
if (s[i] != '"')
{
continue;
}
// Count immediate backslashes before this quote
int j = i - 1, backslashes = 0;
while (j >= start && s[j] == '\\')
{
backslashes++;
j--;
}
// The quote is at a token boundary if the char before the backslashes is start-of-line or whitespace.
var atTokenBoundary = j < start || char.IsWhiteSpace(s[j]);
// Even number of backslashes -> this quote toggles quoting (opens if at boundary).
if (atTokenBoundary && (backslashes % 2 == 0))
{
// Trim trailing spaces off Head so Tail starts exactly at the opening quote
var headEnd = i;
while (headEnd > start && char.IsWhiteSpace(s[headEnd - 1]))
{
headEnd--;
}
var head = input[start..headEnd];
var tail = input[headEnd..]; // starts at the opening quote
return (head, tail.Trim());
}
}
// No quoted-arg start found: entire remainder (trimmed right) is the Head
var wholeHead = input[start..].TrimEnd();
return (wholeHead, string.Empty);
}
/// <summary>
/// Attempts to expand the path to full physical path, expanding environment variables and shell: monikers.
/// </summary>
internal static bool ExpandPathToPhysicalFile(string input, bool expandShell, out string full)
{
if (string.IsNullOrEmpty(input))
{
full = string.Empty;
return false;
}
var expanded = Environment.ExpandEnvironmentVariables(input);
var firstSegment = GetFirstPathSegment(expanded);
if (expandShell && HasShellPrefix(firstSegment) && TryExpandShellMoniker(expanded, out var shellExpanded))
{
expanded = shellExpanded;
}
else if (firstSegment is "~" or "." or "..")
{
expanded = ExpandUserRelative(firstSegment, expanded);
}
if (Path.Exists(expanded))
{
full = Path.GetFullPath(expanded);
return true;
}
full = expanded; // return the attempted expansion even if it doesn't exist
return false;
}
private static bool TryExpandShellMoniker(string input, out string expanded)
{
var separatorIndex = input.IndexOfAny(PathSeparators);
var shellFolder = separatorIndex > 0 ? input[..separatorIndex] : input;
var relativePath = separatorIndex > 0 ? input[(separatorIndex + 1)..] : string.Empty;
if (ShellNames.TryGetFileSystemPath(shellFolder, out var fsPath))
{
expanded = Path.GetFullPath(Path.Combine(fsPath, relativePath));
return true;
}
expanded = input;
return false;
}
private static string ExpandUserRelative(string firstSegment, string input)
{
// Treat relative paths as relative to the user home directory.
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (firstSegment == "~")
{
// Remove "~" (+ optional following separator) before combining.
var skip = 1;
if (input.Length > 1 && IsSeparator(input[1]))
{
skip++;
}
input = input[skip..];
}
return Path.GetFullPath(Path.Combine(homeDirectory, input));
}
private static bool IsSeparator(char c) => c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
private static string GetFirstPathSegment(string input)
{
var separatorIndex = input.IndexOfAny(PathSeparators);
return separatorIndex > 0 ? input[..separatorIndex] : input;
}
internal static bool HasShellPrefix(string input)
{
return input.StartsWith("shell:", StringComparison.OrdinalIgnoreCase) || input.StartsWith("::", StringComparison.Ordinal);
}
}

View File

@@ -1,12 +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.Ext.Bookmarks.Helpers;
public enum LaunchMethod
{
ShellExecute, // UseShellExecute = true (Explorer/associations/protocols)
ExplorerOpen, // explorer.exe <folder/shell:uri>
ActivateAppId, // IApplicationActivationManager (AUMID / pkgfamily!app)
}

View File

@@ -1,47 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.Bookmarks.Helpers;
internal static partial class NativeMethods
{
[LibraryImport("shell32.dll", EntryPoint = "SHParseDisplayName", StringMarshalling = StringMarshalling.Utf16)]
internal static partial int SHParseDisplayName(
string pszName,
nint pbc,
out nint ppidl,
uint sfgaoIn,
nint psfgaoOut);
[LibraryImport("shell32.dll", EntryPoint = "SHGetNameFromIDList", StringMarshalling = StringMarshalling.Utf16)]
internal static partial int SHGetNameFromIDList(
nint pidl,
SIGDN sigdnName,
out nint ppszName);
[LibraryImport("ole32.dll")]
internal static partial void CoTaskMemFree(nint pv);
[LibraryImport("shell32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
internal static partial IntPtr CommandLineToArgvW(string lpCmdLine, out int pNumArgs);
[LibraryImport("kernel32.dll")]
internal static partial IntPtr LocalFree(IntPtr hMem);
internal enum SIGDN : uint
{
NORMALDISPLAY = 0x00000000,
DESKTOPABSOLUTEPARSING = 0x80028000,
DESKTOPABSOLUTEEDITING = 0x8004C000,
FILESYSPATH = 0x80058000,
URL = 0x80068000,
PARENTRELATIVE = 0x80080001,
PARENTRELATIVEFORADDRESSBAR = 0x8007C001,
PARENTRELATIVEPARSING = 0x80018001,
PARENTRELATIVEEDITING = 0x80031001,
PARENTRELATIVEFORUI = 0x80094001,
}
}

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